diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 96543b50..e74d2d0a 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -9,10 +9,10 @@ jobs: steps: - name: Check out the repo uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - name: Build and push Docker Image shell: bash env: diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 634f58b0..881b78f0 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -29,10 +29,10 @@ jobs: - name: Run ESLint run: npm run lint working-directory: ui - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - name: Build with Maven run: mvn -B -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN package --file pom.xml diff --git a/README.md b/README.md index c6e2ef38..10d5bfb0 100644 --- a/README.md +++ b/README.md @@ -95,4 +95,5 @@ to create Pull Requests. Some new versions of Galapagos require extra migration effort: +* [Galapagos 2.6.x and 2.7.x to 2.8.0 Migration Guide](./docs/Migration%20Guide%202.8.md) * [Galapagos 2.5.x to 2.6.0 Migration Guide](./docs/Migration%20Guide%202.6.md) diff --git a/application-demo.yml b/application-demo.yml index 49a2030d..3391db57 100644 --- a/application-demo.yml +++ b/application-demo.yml @@ -32,6 +32,7 @@ galapagos: organization-api-key: "${demo.organization-api-key}" organization-api-secret: "${demo.organization-api-secret}" developer-api-key-validity: "P30D" + serviceAccountIdCompatMode: false staging-only: false - id: prod name: "PROD" @@ -45,6 +46,7 @@ galapagos: organization-api-key: "${demo.organization-api-key}" organization-api-secret: "${demo.organization-api-secret}" developer-api-key-validity: "P10D" + serviceAccountIdCompatMode: false staging-only: true # Here, you can configure ACLs which will be assigned to any account created by Galapagos. diff --git a/application-oauth2.properties b/application-oauth2.properties new file mode 100644 index 00000000..38735396 --- /dev/null +++ b/application-oauth2.properties @@ -0,0 +1,27 @@ +spring.security.oauth2.client.registration.keycloak.client-id=${keycloak.client.id} +spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email,offline_access +spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code + +spring.security.oauth2.client.provider.keycloak.issuer-uri=${keycloak.url}/realms/${keycloak.realm:galapagos} +spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username + +spring.security.oauth2.resourceserver.jwt.issuer-uri=${spring.security.oauth2.client.provider.keycloak.issuer-uri} +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs + +# This is the claim in the JWT Access Token to use as username. +galapagos.security.jwt-username-claim=preferred_username + +# This JWT claim is used as the "display name" (usually the full name) of a user. +galapagos.security.jwt-display-name-claim=name + +# This JWT claim is used as the e-mail address of the user. +galapagos.security.jwt-email-claim=email + +# This is the claim in the JWT Access Token to use as roles (e.g. user and admin). Must be either space-separated single +# value, or an array of strings. Can be "sub" for Spring standard behaviour, or a custom claim. In Keycloak, you usually +# configure a role mapper for a client to include client roles in the token. +# See docs/Migration Guide 2.8.md on how to achieve this! +# Roles must include USER and ADMIN, the latter for Galapagos administrators being able e.g. to approve +# application owner requests. +galapagos.security.jwt-role-claim=client_roles + diff --git a/demo-ccloud/galapagos-demo-realm.json b/demo-ccloud/galapagos-demo-realm.json index a288bb02..a4e75bbc 100644 --- a/demo-ccloud/galapagos-demo-realm.json +++ b/demo-ccloud/galapagos-demo-realm.json @@ -652,6 +652,7 @@ "user" ], "redirectUris": [ + "http://localhost:8080", "http://localhost:8080/*", "http://localhost:4200/*" ], @@ -926,6 +927,23 @@ "jsonType.label": "String" } }, + { + "id": "0ebf2c4a-e17b-45b7-840c-7c2bb1a0889e", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "false", + "user.attribute": "foo", + "id.token.claim": "false", + "access.token.claim": "true", + "claim.name": "client_roles", + "jsonType.label": "String", + "usermodel.clientRoleMapping.clientId": "galapagos-webapp-dev" + } + }, { "id": "93805d72-078d-4d3c-9f23-245f7252a51e", "name": "middle name", diff --git a/demo/galapagos-demo-realm.json b/demo/galapagos-demo-realm.json index 6914bc81..90d15dba 100644 --- a/demo/galapagos-demo-realm.json +++ b/demo/galapagos-demo-realm.json @@ -652,6 +652,7 @@ "user" ], "redirectUris": [ + "", "/*" ], "webOrigins": [ @@ -925,6 +926,23 @@ "jsonType.label": "String" } }, + { + "id": "0ebf2c4a-e17b-45b7-840c-7c2bb1a0889e", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "false", + "user.attribute": "foo", + "id.token.claim": "false", + "access.token.claim": "true", + "claim.name": "client_roles", + "jsonType.label": "String", + "usermodel.clientRoleMapping.clientId": "galapagos-webapp-dev" + } + }, { "id": "93805d72-078d-4d3c-9f23-245f7252a51e", "name": "middle name", diff --git a/docs/Migration Guide 2.8.md b/docs/Migration Guide 2.8.md new file mode 100644 index 00000000..ebb7b203 --- /dev/null +++ b/docs/Migration Guide 2.8.md @@ -0,0 +1,127 @@ +# Galapagos 2.6.x-2.7.0 to 2.8.0 Migration Guide + +## Spring Boot 3 + +Galapagos 2.8.0 uses Spring Boot 3 together with Spring Security 6. As Keycloak deprecated most of their adapter +libraries, and the latest one is not fully compatible to Spring Security 6, we had to replace it, using standard +Spring Security OAuth2 mechanisms (which is, by the way, great, as this now allows to use other OAuth2 providers as +well). + +Unfortunately, this required some **breaking changes** in the way authentication and authorization is configured in +Galapagos. + +## Authentication Details + +Previously, core element of configuration was a `keycloak.json` file which could be configured to be used. The Keycloak +adapter library as well as the frontend used this for authentication and authorization via Keycloak. + +Now, configuration is done via standard Spring Security properties, and some Galapagos-specific properties as well. +You will find a good starting point in [application-oauth2.properties](../application-oauth2.properties) in the root +of the project. + +## Authorization + +Galapagos uses OAuth2 not only for **authentication**, but also for **authorization** of the users. Users are expected +to have the roles `user` and, optionally, `admin` listed somewhere in their access token. + +With Keycloak, this was more or less a Keycloak standard you could achieve with some attributes in the `keycloak.json`, +and the roles were mapped automagically to the Spring Security Context. + +Now, we have to extract role information ourselves from the Access Token, and we have to know from where. Spring +Security offers a helper for this, but this helper only supports "top-level" attributes (claims) in the Token as source, +not nested ones like the ones Keycloak is using by default. + +This means we will also have to adjust **Keycloak itself** to provide the user's roles in a top-level claim in the +token. + +# Putting it all together + +To migrate to Galapagos 2.8.0, you will have to perform the following steps (explained in detail below): + +1. Adjust Keycloak to provide user roles in a top-level claim in the access token +2. Configure OAuth2 properties, or at least an environment variable, to match your Identity Provider +3. Remove previous Keycloak-specific configuration properties and files. + +## 1. Adjust Keycloak + +As described above, Galapagos now requires the user's roles (at least `user`, optionally `admin` for Galapagos admins) +to be in a top-level claim in the Access Token. This can be achieved using a "client roles token mapper", which can be +configured on the affected client. + +First of all, open the Admin Console of Keycloak and navigate to your client which is used for Galapagos. In our +example, it is `example-app2`. Click on the "Client scopes" tab. + +Client Scopes Tab + +Click on the (usually) first entry in the scopes list, which should be similar to the ID of the client (in our example, +`example-app2-dedicated`). + +If no mappers are yet defined for your client, the following page will occur. (Otherwise, if you already have one or +more mappers, just click "Add Mappers" on the screen with your mappers.) + +Add Mapper page + +Click on "Add predefined mapper". In the popup, enter `client` as the filter text and click the arrow to filter the +list. Select "client roles" and click the "Add" button. + +Add Mapper dialog + +Now, we have to configure the newly added mapper. In the list of mappers which is now displayed, click the newly added +mapper: + + + +And you will get to the "edit" page of this mapper, where you can control the behaviour of the mapper. Select the +ID of your client as the client ID, and enter e.g. "client_roles" as the name of the claim to be used in the +Access Token. **Important**: Do **not** leave the default value here, which adds some "sub-objects" in the access token. +The roles claim **must** be on top level in the Access Token! + + + +Click "Save" here, and you are done on the Keycloak side. Phew! + +## 2. Configure OAuth2 properties + +Well, depending on your setup, this **could** be an almost no-brainer. See the default new +`application-oauth2.properties` in the project root (comments removed for brevity): + +```properties +spring.security.oauth2.client.registration.keycloak.client-id=${keycloak.client.id} +spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email,offline_access +spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code + +spring.security.oauth2.client.provider.keycloak.issuer-uri=${keycloak.url}/auth/realms/${keycloak.realm:galapagos} +spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username + +spring.security.oauth2.resourceserver.jwt.issuer-uri=${spring.security.oauth2.client.provider.keycloak.issuer-uri} +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs + +galapagos.security.jwt-username-claim=preferred_username +galapagos.security.jwt-display-name-claim=name +galapagos.security.jwt-email-claim=email +galapagos.security.jwt-role-claim=client_roles +``` + +This is how a standard OAuth2 configuration would look like for using Keycloak as Identity Provider. +You can see that some placeholder variables are used here. You now have all the options Spring offers: + +* Use this configuration file and provide variables via e.g. environment variables or program arguments +* Copy this configuration file and replace properties to your needs +* Incorporate this configuration in your existing Galapagos configuration + +For example, you could just copy this properties file to your execution environment, add `oauth2` to the +`spring.profiles.active` configuration, and provide `KEYCLOAK_CLIENT_ID` and `KEYCLOAK_URL` (and, optionally, +`KEYCLOAK_REALM`) as environment variables. + +If you want to use a different Identity Provider than Keycloak, refer +to [Spring Security OAuth2 Resource Server Docs](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html) +for more information. But we highly recommend to first perform the Galapagos 2.8 migration **with** Keycloak, and +**then** switching the Identity Provider, if desired. + +## 3. Remove previous Keycloak-specific configuration + +To complete the migration, remove your `keycloak.json` or whatever the name of your Keycloak config JSON file is, and +remove the configuration property `keycloak.configurationFile` from your configuration. Now try running Galapagos with +your new settings. If you encounter any problems, have a look into +the [Discussions](https://github.com/HermesGermany/galapagos/discussions), or create +a [GitHub issue](https://github.com/HermesGermany/galapagos/issues). diff --git a/docs/keycloak_add_mapper.png b/docs/keycloak_add_mapper.png new file mode 100644 index 00000000..368ee419 Binary files /dev/null and b/docs/keycloak_add_mapper.png differ diff --git a/docs/keycloak_client_roles_added.png b/docs/keycloak_client_roles_added.png new file mode 100644 index 00000000..7920df79 Binary files /dev/null and b/docs/keycloak_client_roles_added.png differ diff --git a/docs/keycloak_client_roles_editor.png b/docs/keycloak_client_roles_editor.png new file mode 100644 index 00000000..bf33ee88 Binary files /dev/null and b/docs/keycloak_client_roles_editor.png differ diff --git a/docs/keycloak_client_roles_mapper.png b/docs/keycloak_client_roles_mapper.png new file mode 100644 index 00000000..2f2224c6 Binary files /dev/null and b/docs/keycloak_client_roles_mapper.png differ diff --git a/docs/keycloak_client_scopes.png b/docs/keycloak_client_scopes.png new file mode 100644 index 00000000..332a6091 Binary files /dev/null and b/docs/keycloak_client_scopes.png differ diff --git a/pom.xml b/pom.xml index fd79b83b..ce3589c3 100644 --- a/pom.xml +++ b/pom.xml @@ -1,336 +1,351 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 2.7.6 - - - com.hermesworld.ais - galapagos - 2.7.0 - Galapagos - A self-service tool for managing Kafka Topics and associated JSON schemas. + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.1 + + + com.hermesworld.ais + galapagos + 2.8.0-SNAPSHOT + Galapagos + A self-service tool for managing Kafka Topics and associated JSON schemas. - - 11 - 19.0.2 - 1.18.24 - 2.7.2 - v16.14.0 - 3.7.0.1746 - src/main/java - src/main/resources/* - 0.8.5 - + + 17 + 1.18.28 + 2.7.2 + v16.14.0 + 3.7.0.1746 + src/main/java + src/main/resources/* + 0.8.10 + - - - org.springframework.boot - spring-boot-starter-web - - - - org.yaml - snakeyaml - 1.33 - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-mail - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-validation - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.kafka - spring-kafka - - - org.springframework.boot - spring-boot-configuration-processor - provided - true - - - net.logstash.logback - logstash-logback-encoder - 6.3 - - - com.ibm.icu - icu4j - 68.2 - + + + org.springframework.boot + spring-boot-starter-web + + + org.yaml + snakeyaml + 1.33 + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + + org.springframework.security + spring-security-config + 6.1.2 + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.kafka + spring-kafka + + + org.springframework.boot + spring-boot-configuration-processor + provided + true + + + net.logstash.logback + logstash-logback-encoder + 7.3 + + + com.ibm.icu + icu4j + 72.1 + - - org.keycloak - keycloak-spring-security-adapter - ${keycloak.version} - + + org.projectlombok + lombok + ${lombok.version} + provided + + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + - - org.projectlombok - lombok - ${lombok.version} - provided - - - com.google.code.findbugs - jsr305 - 3.0.2 - provided - + + org.apache.kafka + kafka-clients + + + + org.thymeleaf + thymeleaf + + + org.thymeleaf + thymeleaf-spring6 + + + org.thymeleaf.extras + thymeleaf-extras-java8time + 3.0.4.RELEASE + + + org.bouncycastle + bcpkix-jdk15on + 1.70 + + + org.bouncycastle + bcprov-jdk15on + 1.70 + + + org.json + json + 20230227 + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.github.erosb + everit-json-schema + 1.14.2 + + + com.auth0 + java-jwt + 4.3.0 + - - org.apache.kafka - kafka-clients - - - - org.thymeleaf - thymeleaf - - - org.thymeleaf - thymeleaf-spring5 - - - org.thymeleaf.extras - thymeleaf-extras-java8time - - - org.bouncycastle - bcpkix-jdk15on - 1.70 - - - org.bouncycastle - bcprov-jdk15on - 1.70 - - - org.json - json - 20220924 - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - - - com.github.erosb - everit-json-schema - 1.14.1 - - - com.auth0 - java-jwt - 4.2.1 - + + org.glassfish.jersey.inject + jersey-hk2 + + + jakarta.activation + jakarta.activation-api + + + jakarta.mail + jakarta.mail-api + + + jakarta.servlet + jakarta.servlet-api + + + org.jsoup + jsoup + 1.15.4 + - - org.glassfish.jersey.inject - jersey-hk2 - - - jakarta.activation - jakarta.activation-api - - - org.jsoup - jsoup - 1.15.3 - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + io.projectreactor + reactor-test + test + + + org.glassfish.jaxb + jaxb-runtime + test + + + org.mockito + mockito-core + 5.2.0 + test + + + com.squareup.okhttp3 + okhttp + test + + + com.squareup.okhttp3 + mockwebserver + test + + - - org.springframework.boot - spring-boot-starter-test - test - - - io.projectreactor - reactor-test - test - - - org.mockito - mockito-core - 3.7.7 - test - - - org.junit.vintage - junit-vintage-engine - test - - - org.hamcrest - hamcrest-core - - - - - com.squareup.okhttp3 - okhttp - test - - - com.squareup.okhttp3 - mockwebserver - test - - + + + + org.springframework.boot + spring-boot-maven-plugin + - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - net.revelc.code.formatter - formatter-maven-plugin - 2.13.0 - - LF - ${project.basedir}/eclipse-code-formatter.xml - - - - validate-formatting - process-sources - - validate - - - - + + net.revelc.code.formatter + formatter-maven-plugin + 2.13.0 + + LF + ${project.basedir}/eclipse-code-formatter.xml + + + + validate-formatting + process-sources + + validate + + + + - - com.github.eirslett - frontend-maven-plugin - 1.8.0 - - - install-node-and-npm - - install-node-and-npm - - prepare-package - - - install-npm-packages - - npm - - prepare-package - - - build-ui - - npm - - - run build-from-maven - - prepare-package - - - - ${node.version} - ${basedir}/ui - ${project.build.directory} - - + + com.github.eirslett + frontend-maven-plugin + 1.8.0 + + + install-node-and-npm + + install-node-and-npm + + prepare-package + + + install-npm-packages + + npm + + prepare-package + + + build-ui + + npm + + + run build-from-maven + + prepare-package + + + + ${node.version} + ${basedir}/ui + ${project.build.directory} + + - - maven-resources-plugin - - - inject-build-version - - copy-resources - - process-classes - - ${project.build.outputDirectory} - - - src/main/resources - true - - application-actuator.properties - - - - true - - - - + + maven-resources-plugin + + + inject-build-version + + copy-resources + + process-classes + + ${project.build.outputDirectory} + + + src/main/resources + true + + application-actuator.properties + + + + true + + + + - - org.sonarsource.scanner.maven - sonar-maven-plugin - ${sonar-maven-plugin.version} - + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar-maven-plugin.version} + - - org.jacoco - jacoco-maven-plugin - ${jacoco-maven-plugin.version} - - - - prepare-agent - - - - report - prepare-package - - report - - - - - + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + + prepare-agent + + + + report + prepare-package + + report + + + + + - - - - com.google.cloud.tools - jib-maven-plugin - 2.7.0 - - - - + + + + com.google.cloud.tools + jib-maven-plugin + 2.7.0 + + + + diff --git a/src/main/java/com/hermesworld/ais/galapagos/HomeController.java b/src/main/java/com/hermesworld/ais/galapagos/HomeController.java index 87b2a2d8..b9ccaa8c 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/HomeController.java +++ b/src/main/java/com/hermesworld/ais/galapagos/HomeController.java @@ -1,6 +1,8 @@ package com.hermesworld.ais.galapagos; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Controller; +import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.GetMapping; /** @@ -13,25 +15,29 @@ @Controller public class HomeController { - // TODO alternative implementation idea: catch /app/**, check if a matching static resource exists. If yes, return - // it. - // If no, forward to index.html (not sure if this is possible with a controller only) - /** * Forwards all calls to an Angular route to the index.html page. Note: If new routes are added to the * frontend, you will have to add them here as part of the mapping as well. * * @return Forward command to the index.html page. */ - @GetMapping({ "/app", "/app/applications", "/app/admin", "/app/topics", "/app/topics/**", "/app/dashboard", + @GetMapping({ "/app/applications", "/app/admin", "/app/topics", "/app/topics/**", "/app/dashboard", "/app/createtopic", "/app/user-settings" }) - public String app() { + public String app(HttpServletRequest request) { + if (!ObjectUtils.isEmpty(request.getQueryString())) { + return "forward:/app/index.html?" + request.getQueryString(); + } return "forward:/app/index.html"; } + @GetMapping("/app") + public String appRoot() { + return "redirect:/app/dashboard"; + } + @GetMapping("/") public String root() { - return "redirect:/app"; + return "redirect:/app/dashboard"; } } diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/CleanupDeveloperAuthenticationsJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/CleanupDeveloperAuthenticationsJob.java index 11609898..4b82726b 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/CleanupDeveloperAuthenticationsJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/CleanupDeveloperAuthenticationsJob.java @@ -2,7 +2,6 @@ import com.hermesworld.ais.galapagos.adminjobs.AdminJob; import com.hermesworld.ais.galapagos.devauth.DeveloperAuthenticationService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; @@ -11,7 +10,6 @@ public class CleanupDeveloperAuthenticationsJob implements AdminJob { private final DeveloperAuthenticationService developerAuthenticationService; - @Autowired public CleanupDeveloperAuthenticationsJob(DeveloperAuthenticationService developerAuthenticationService) { this.developerAuthenticationService = developerAuthenticationService; } diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/CreateBackupJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/CreateBackupJob.java index 0b069b17..80ff8425 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/CreateBackupJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/CreateBackupJob.java @@ -10,7 +10,6 @@ import com.hermesworld.ais.galapagos.util.JsonUtil; import org.json.JSONException; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; @@ -25,7 +24,6 @@ public class CreateBackupJob implements AdminJob { private final ObjectMapper objectMapper = JsonUtil.newObjectMapper(); - @Autowired public CreateBackupJob(KafkaClusters kafkaClusters) { this.kafkaClusters = kafkaClusters; } diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/DeleteAclsJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/DeleteAclsJob.java index de328067..24fc3d6d 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/DeleteAclsJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/DeleteAclsJob.java @@ -8,10 +8,9 @@ import com.hermesworld.ais.galapagos.kafka.KafkaClusters; import com.hermesworld.ais.galapagos.kafka.KafkaUser; import org.apache.kafka.common.acl.AclBinding; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; +import org.springframework.util.ObjectUtils; /** * Admin job to explicitly delete ACLs from a Kafka Cluster. This job is useful if something went terribly wrong with @@ -31,7 +30,6 @@ @Component public class DeleteAclsJob extends SingleClusterAdminJob { - @Autowired public DeleteAclsJob(KafkaClusters kafkaClusters) { super(kafkaClusters); } @@ -46,7 +44,7 @@ public void runOnCluster(KafkaCluster cluster, ApplicationArguments allArguments String certificateDn = Optional.ofNullable(allArguments.getOptionValues("certificate.dn")) .flatMap(ls -> ls.stream().findFirst()).orElse(null); - if (StringUtils.isEmpty(certificateDn)) { + if (ObjectUtils.isEmpty(certificateDn)) { throw new IllegalArgumentException("Please provide --certificate.dn= for DN of certificate."); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingApiKeyJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingApiKeyJob.java index e356819b..5f6b9e31 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingApiKeyJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingApiKeyJob.java @@ -13,7 +13,6 @@ import com.hermesworld.ais.galapagos.naming.ApplicationPrefixes; import com.hermesworld.ais.galapagos.naming.NamingService; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; @@ -42,7 +41,6 @@ public class GenerateToolingApiKeyJob extends SingleClusterAdminJob { private final AclSupport aclSupport; - @Autowired public GenerateToolingApiKeyJob(KafkaClusters kafkaClusters, AclSupport aclSupport, NamingService namingService, KafkaEnvironmentsConfig kafkaConfig) { super(kafkaClusters); diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingCertificateJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingCertificateJob.java index 27dd86e2..410db6df 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingCertificateJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingCertificateJob.java @@ -16,10 +16,9 @@ import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.pkcs.PKCS10CertificationRequest; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; +import org.springframework.util.ObjectUtils; import java.io.ByteArrayInputStream; import java.io.FileOutputStream; @@ -57,7 +56,6 @@ public class GenerateToolingCertificateJob extends SingleClusterAdminJob { private final KafkaEnvironmentsConfig kafkaConfig; - @Autowired public GenerateToolingCertificateJob(KafkaClusters kafkaClusters, AclSupport aclSupport, NamingService namingService, KafkaEnvironmentsConfig kafkaConfig) { super(kafkaClusters); @@ -82,7 +80,7 @@ public void runOnCluster(KafkaCluster cluster, ApplicationArguments allArguments + " does not use certificates for authentication. Cannot generate tooling certificate."); } - if (!StringUtils.isEmpty(outputFilename)) { + if (!ObjectUtils.isEmpty(outputFilename)) { try { new FileOutputStream(outputFilename).close(); } @@ -122,7 +120,7 @@ public void runOnCluster(KafkaCluster cluster, ApplicationArguments allArguments cluster.updateUserAcls(new ToolingUser(toolMetadata, cluster.getId(), authModule, aclSupport)).get(); - if (!StringUtils.isEmpty(outputFilename)) { + if (!ObjectUtils.isEmpty(outputFilename)) { try (FileOutputStream fos = new FileOutputStream(outputFilename)) { fos.write(p12Data); } @@ -135,7 +133,7 @@ public void runOnCluster(KafkaCluster cluster, ApplicationArguments allArguments System.out.println(); System.out.println("==================== Galapagos Tooling Certificate CREATED ===================="); System.out.println(); - if (!StringUtils.isEmpty(outputFilename)) { + if (!ObjectUtils.isEmpty(outputFilename)) { System.out.println("You can now use the certificate in " + outputFilename + " for Galapagos external tooling on " + metadata.getName()); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportBackupJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportBackupJob.java index adb902d6..20ba5f51 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportBackupJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportBackupJob.java @@ -8,7 +8,6 @@ import com.hermesworld.ais.galapagos.util.HasKey; import com.hermesworld.ais.galapagos.util.JsonUtil; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; import org.springframework.util.StreamUtils; @@ -44,7 +43,6 @@ public class ImportBackupJob implements AdminJob { private final ObjectMapper objectMapper; - @Autowired public ImportBackupJob(KafkaClusters kafkaClusters) { this.kafkaClusters = kafkaClusters; this.objectMapper = JsonUtil.newObjectMapper(); diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportKnownApplicationsJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportKnownApplicationsJob.java index 1aeb34ff..5fada7ef 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportKnownApplicationsJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportKnownApplicationsJob.java @@ -19,11 +19,10 @@ import com.hermesworld.ais.galapagos.kafka.util.TopicBasedRepository; import com.hermesworld.ais.galapagos.util.JsonUtil; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; import org.springframework.util.StreamUtils; -import org.springframework.util.StringUtils; /** * Admin job to import known applications from a JSON file (or STDIN) to the global Galapagos topic @@ -46,7 +45,6 @@ public class ImportKnownApplicationsJob implements AdminJob { private KafkaClusters kafkaClusters; - @Autowired public ImportKnownApplicationsJob(KafkaClusters kafkaClusters) { this.kafkaClusters = kafkaClusters; } @@ -65,7 +63,7 @@ public void run(ApplicationArguments allArguments) throws Exception { .map(ls -> ls.stream().findFirst().orElse(null)).map(s -> s == null ? false : Boolean.parseBoolean(s)) .orElse(false); - if (StringUtils.isEmpty(jsonFile)) { + if (ObjectUtils.isEmpty(jsonFile)) { throw new IllegalArgumentException("Please provide --applications.import.file= for JSON to import"); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/MarkTopicApprovalRequiredJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/MarkTopicApprovalRequiredJob.java index dec55bac..64bb1940 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/MarkTopicApprovalRequiredJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/MarkTopicApprovalRequiredJob.java @@ -7,7 +7,6 @@ import com.hermesworld.ais.galapagos.adminjobs.AdminJob; import com.hermesworld.ais.galapagos.kafka.KafkaClusters; import com.hermesworld.ais.galapagos.topics.service.TopicService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; @@ -19,7 +18,6 @@ public class MarkTopicApprovalRequiredJob implements AdminJob { private final TopicService topicService; - @Autowired public MarkTopicApprovalRequiredJob(KafkaClusters kafkaClusters, @Qualifier("nonvalidating") TopicService topicService) { this.kafkaClusters = kafkaClusters; diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ResetApplicationPrefixesJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ResetApplicationPrefixesJob.java index 1c4027f4..8ee15462 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ResetApplicationPrefixesJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ResetApplicationPrefixesJob.java @@ -4,7 +4,6 @@ import com.hermesworld.ais.galapagos.kafka.KafkaCluster; import com.hermesworld.ais.galapagos.kafka.KafkaClusters; import com.hermesworld.ais.galapagos.kafka.util.AclSupport; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; @@ -44,7 +43,6 @@ public class ResetApplicationPrefixesJob extends SingleClusterAdminJob { private final AclSupport aclSupport; - @Autowired public ResetApplicationPrefixesJob(KafkaClusters kafkaClusters, ApplicationsService applicationsService, AclSupport aclSupport) { super(kafkaClusters); diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/SingleClusterAdminJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/SingleClusterAdminJob.java index 2fd324c5..cd8a0360 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/SingleClusterAdminJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/SingleClusterAdminJob.java @@ -27,7 +27,7 @@ public final void run(ApplicationArguments allArguments) throws Exception { String kafkaEnvironment = Optional.ofNullable(allArguments.getOptionValues("kafka.environment")) .flatMap(ls -> ls.stream().findFirst()).orElse(null); - if (StringUtils.isEmpty(kafkaEnvironment)) { + if (!StringUtils.hasLength(kafkaEnvironment)) { throw new IllegalArgumentException( "Please provide --kafka.environment= to specify Kafka Environment to update application ACLs on."); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/UpdateApplicationAclsJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/UpdateApplicationAclsJob.java index e10c4c19..14cd8692 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/UpdateApplicationAclsJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/UpdateApplicationAclsJob.java @@ -8,11 +8,12 @@ import com.hermesworld.ais.galapagos.kafka.KafkaUser; import com.hermesworld.ais.galapagos.kafka.impl.ConnectedKafkaCluster; import com.hermesworld.ais.galapagos.kafka.util.AclSupport; +import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.CreateAclsResult; import org.apache.kafka.clients.admin.DeleteAclsResult; import org.apache.kafka.common.acl.AclBinding; import org.apache.kafka.common.acl.AclBindingFilter; -import org.springframework.beans.factory.annotation.Autowired; +import org.json.JSONException; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -38,13 +39,13 @@ * */ @Component +@Slf4j public class UpdateApplicationAclsJob extends SingleClusterAdminJob { private final AclSupport aclSupport; private final ApplicationsService applicationsService; - @Autowired public UpdateApplicationAclsJob(KafkaClusters kafkaClusters, AclSupport aclSupport, ApplicationsService applicationsService) { super(kafkaClusters); @@ -131,8 +132,13 @@ private void updateApplicationAcl(KafkaCluster cluster, ApplicationMetadata meta throws ExecutionException, InterruptedException { KafkaUser user = new ToolingUser(metadata, cluster.getId(), kafkaClusters.getAuthenticationModule(cluster.getId()).orElseThrow(), aclSupport); - if (StringUtils.hasLength(user.getKafkaUserName())) { - cluster.updateUserAcls(user).get(); + try { + if (StringUtils.hasLength(user.getKafkaUserName())) { + cluster.updateUserAcls(user).get(); + } + } + catch (JSONException e) { + log.error("Could not update ACLs for application {}", metadata.getApplicationId(), e); } } } diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/UpdateConfluentAuthMetadataJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/UpdateConfluentAuthMetadataJob.java index 74498ca2..a66349d7 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/UpdateConfluentAuthMetadataJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/UpdateConfluentAuthMetadataJob.java @@ -11,7 +11,6 @@ import com.hermesworld.ais.galapagos.kafka.auth.KafkaAuthenticationModule; import com.hermesworld.ais.galapagos.kafka.util.TopicBasedRepository; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; @@ -26,7 +25,6 @@ public class UpdateConfluentAuthMetadataJob implements AdminJob { private final DeveloperAuthenticationService devAuthService; - @Autowired public UpdateConfluentAuthMetadataJob(KafkaClusters kafkaClusters, ApplicationsService applicationsService, DeveloperAuthenticationService devAuthService) { this.kafkaClusters = kafkaClusters; diff --git a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ViewAclsJob.java b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ViewAclsJob.java index 1f10bf5b..68939694 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ViewAclsJob.java +++ b/src/main/java/com/hermesworld/ais/galapagos/adminjobs/impl/ViewAclsJob.java @@ -5,7 +5,6 @@ import org.apache.kafka.common.acl.AclBinding; import org.json.JSONArray; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.stereotype.Component; @@ -24,7 +23,6 @@ @Component public class ViewAclsJob extends SingleClusterAdminJob { - @Autowired public ViewAclsJob(KafkaClusters kafkaClusters) { super(kafkaClusters); } @@ -44,7 +42,8 @@ public void runOnCluster(KafkaCluster cluster, ApplicationArguments allArguments System.out.println(); System.out.println(); - System.out.println(acls.toString()); + System.out.println(acls.length() + " ACLs found:"); + System.out.println(acls); System.out.println(); System.out.println(); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/applications/controller/ApplicationsController.java b/src/main/java/com/hermesworld/ais/galapagos/applications/controller/ApplicationsController.java index b13b1104..23956fa8 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/applications/controller/ApplicationsController.java +++ b/src/main/java/com/hermesworld/ais/galapagos/applications/controller/ApplicationsController.java @@ -17,10 +17,10 @@ import lombok.extern.slf4j.Slf4j; import org.json.JSONException; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.annotation.Secured; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; @@ -43,7 +43,6 @@ public class ApplicationsController { private final KafkaClusters kafkaClusters; - @Autowired public ApplicationsController(ApplicationsService applicationsService, StagingService stagingService, KafkaClusters kafkaClusters) { this.applicationsService = applicationsService; @@ -53,7 +52,7 @@ public ApplicationsController(ApplicationsService applicationsService, StagingSe @GetMapping(value = "/api/applications", produces = MediaType.APPLICATION_JSON_VALUE) public List listApplications( - @RequestParam(name = "excludeUserApps", required = false, defaultValue = "false") boolean excludeUserApps) { + @RequestParam(required = false, defaultValue = "false") boolean excludeUserApps) { return applicationsService.getKnownApplications(excludeUserApps).stream().map(app -> toKnownAppDto(app)) .collect(Collectors.toList()); } @@ -159,7 +158,7 @@ public CertificateResponseDto updateApplicationCertificate(@PathVariable String String filename = CertificateUtil.toAppCn(app.getName()) + "_" + environmentId; if (!request.isGenerateKey()) { String csrData = request.getCsrData(); - if (StringUtils.isEmpty(csrData)) { + if (ObjectUtils.isEmpty(csrData)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No CSR (csrData) present! Set generateKey to true if you want the server to generate a private key for you (not recommended)."); } @@ -211,7 +210,7 @@ public CreatedApiKeyDto createApiKeyForApplication(@PathVariable String environm try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JSONObject params = StringUtils.isEmpty(request) ? new JSONObject() : new JSONObject(request); + JSONObject params = ObjectUtils.isEmpty(request) ? new JSONObject() : new JSONObject(request); ApplicationMetadata metadata = applicationsService .registerApplicationOnEnvironment(environmentId, applicationId, params, baos).get(); CreatedApiKeyDto dto = new CreatedApiKeyDto(); @@ -279,7 +278,7 @@ public List performStaging(@PathVariable String environmentId, @P } List stagingFilter = null; - if (!StringUtils.isEmpty(stagingFilterRaw)) { + if (!ObjectUtils.isEmpty(stagingFilterRaw)) { try { stagingFilter = JsonUtil.newObjectMapper().readValue(stagingFilterRaw, TypeFactory.defaultInstance().constructCollectionType(ArrayList.class, Change.class)); diff --git a/src/main/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceImpl.java b/src/main/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceImpl.java index 776472b6..2bcda47a 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceImpl.java @@ -15,9 +15,9 @@ import com.hermesworld.ais.galapagos.util.TimeService; import lombok.extern.slf4j.Slf4j; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import java.io.IOException; @@ -55,7 +55,6 @@ public class ApplicationsServiceImpl implements ApplicationsService, InitPerClus private static final Comparator requestComparator = (r1, r2) -> r2.getLastStatusChangeAt() .compareTo(r1.getLastStatusChangeAt()); - @Autowired public ApplicationsServiceImpl(KafkaClusters kafkaClusters, CurrentUserService currentUserService, TimeService timeService, NamingService namingService, GalapagosEventManager eventManager) { this.kafkaClusters = kafkaClusters; @@ -257,7 +256,7 @@ public CompletableFuture registerApplicationOnEnvironment(S if (existing != null) { String json = existing.getAuthenticationJson(); - if (!StringUtils.isEmpty(json)) { + if (!ObjectUtils.isEmpty(json)) { updateOrCreateFuture = authModule.updateApplicationAuthentication(applicationId, applicationName, registerParams, new JSONObject(json)); } @@ -352,8 +351,8 @@ void removeOldRequests() { ZonedDateTime maxAge = timeService.getTimestamp().minusDays(30); List oldRequests = getRequestsRepository().getObjects().stream() - .filter(req -> (req.getState() == RequestState.REJECTED || req.getState() == RequestState.REVOKED) - && req.getLastStatusChangeAt().isBefore(maxAge)) + .filter(req -> (req.getState() == RequestState.REJECTED || req.getState() == RequestState.REVOKED + || req.getState() == RequestState.RESIGNED) && req.getLastStatusChangeAt().isBefore(maxAge)) .collect(Collectors.toList()); for (ApplicationOwnerRequest request : oldRequests) { diff --git a/src/main/java/com/hermesworld/ais/galapagos/applications/impl/UpdateApplicationAclsListener.java b/src/main/java/com/hermesworld/ais/galapagos/applications/impl/UpdateApplicationAclsListener.java index 6e38fe0a..ff971a84 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/applications/impl/UpdateApplicationAclsListener.java +++ b/src/main/java/com/hermesworld/ais/galapagos/applications/impl/UpdateApplicationAclsListener.java @@ -22,7 +22,6 @@ import org.json.JSONException; import org.json.JSONObject; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thymeleaf.util.StringUtils; @@ -44,7 +43,6 @@ public class UpdateApplicationAclsListener private final AclSupport aclSupport; - @Autowired public UpdateApplicationAclsListener(KafkaClusters kafkaClusters, SubscriptionService subscriptionService, ApplicationsService applicationsService, AclSupport aclSupport) { this.kafkaClusters = kafkaClusters; diff --git a/src/main/java/com/hermesworld/ais/galapagos/ccloud/apiclient/ConfluentCloudApiClient.java b/src/main/java/com/hermesworld/ais/galapagos/ccloud/apiclient/ConfluentCloudApiClient.java index b9831283..9557c66c 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/ccloud/apiclient/ConfluentCloudApiClient.java +++ b/src/main/java/com/hermesworld/ais/galapagos/ccloud/apiclient/ConfluentCloudApiClient.java @@ -71,16 +71,19 @@ public ConfluentCloudApiClient(String apiKey, String apiSecret, boolean idCompat } public Mono> listClusterApiKeys(String clusterId) { + log.debug("List Cluster API Keys"); return doPaginatedGet("/iam/v2/api-keys?spec.resource=" + clusterId, this::readApiKey, "Could not retrieve cluster API Key list"); } public Mono> listServiceAccounts() { + log.debug("List service accounts"); return doPaginatedGet("/iam/v2/service-accounts", obj -> toServiceAccountSpec(obj), "Could not retrieve service accounts"); } public Mono createServiceAccount(String accountName, String accountDescription) { + log.debug("Create Service Account {}", accountName); JSONObject req = new JSONObject(); req.put("display_name", accountName); req.put("description", accountDescription); @@ -91,6 +94,7 @@ public Mono createServiceAccount(String accountName, String public Mono createApiKey(String envId, String clusterId, String description, String serviceAccountResourceId) { + log.debug("Create API Key {}", description); JSONObject spec = new JSONObject(); spec.put("display_name", ""); @@ -103,6 +107,7 @@ public Mono createApiKey(String envId, String clusterId, String desc } public Mono deleteApiKey(ApiKeySpec apiKeySpec) { + log.debug("Delete API Key {}", apiKeySpec.getId()); return doDelete("/iam/v2/api-keys/" + apiKeySpec.getId(), "Could not delete API key").map(o -> true); } @@ -113,6 +118,7 @@ public Mono deleteApiKey(ApiKeySpec apiKeySpec) { * @return A map from Confluent resource IDs (e.g. sa-xy123) to numeric service account IDs (e.g. 123456). */ public Mono> getServiceAccountInternalIds() { + log.debug("Get service account numeric IDs"); return doDirectGet("/service_accounts", "Could not access or read /service_accounts workaround endpoint") .flatMap(response -> toServiceAccountIdMap(response)); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/ccloud/auth/ConfluentCloudAuthenticationModule.java b/src/main/java/com/hermesworld/ais/galapagos/ccloud/auth/ConfluentCloudAuthenticationModule.java index a6afb3cd..e1734c5a 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/ccloud/auth/ConfluentCloudAuthenticationModule.java +++ b/src/main/java/com/hermesworld/ais/galapagos/ccloud/auth/ConfluentCloudAuthenticationModule.java @@ -87,13 +87,17 @@ public void addRequiredKafkaProperties(Properties kafkaProperties) { public CompletableFuture createApplicationAuthentication(String applicationId, String applicationNormalizedName, JSONObject createParams) { String apiKeyDesc = MessageFormat.format(API_KEY_DESC, applicationNormalizedName); + log.info("Creating API Key for application (normalized name) {}", applicationNormalizedName); // reset internal ID cache serviceAccountNumericIds.clear(); + String shortenedAppName = applicationNormalizedName.substring(0, + Math.min(50, applicationNormalizedName.length())); + return findServiceAccountForApp(applicationId) .thenCompose(account -> account.map(a -> CompletableFuture.completedFuture(a)) - .orElseGet(() -> client.createServiceAccount("application-" + applicationNormalizedName, + .orElseGet(() -> client.createServiceAccount("application-" + shortenedAppName, appServiceAccountDescription(applicationId)).toFuture())) .thenCompose(account -> client.createApiKey(config.getEnvironmentId(), config.getClusterId(), apiKeyDesc, account.getResourceId()).toFuture()) @@ -111,6 +115,7 @@ public CompletableFuture updateApplicationAuthentica public CompletableFuture deleteApplicationAuthentication(String applicationId, JSONObject existingAuthData) { String apiKey = existingAuthData.optString(JSON_API_KEY); if (StringUtils.hasLength(apiKey)) { + log.info("Deleting API Key {}", apiKey); return client.listClusterApiKeys(config.getClusterId()).toFuture() .thenCompose(ls -> ls.stream().filter(info -> apiKey.equals(info.getId())).findAny() .map(info -> client.deleteApiKey(info).toFuture().thenApply(o -> (Void) null)) @@ -211,7 +216,7 @@ public boolean supportsDeveloperApiKeys() { /** * Upgrades a given "old" authentication metadata object. Is used by admin job "update-confluent-auth-metadata" to * e.g. add numeric IDs and Service Account Resource IDs. - * + * * @param oldAuthMetadata Potentially "old" authentication metadata (function determines if update is required). * @return The "updated" metadata, or the unchanged metadata if already filled with new fields. As a future as an * API call may be required to determine required information (e.g. internal numeric ID for a service diff --git a/src/main/java/com/hermesworld/ais/galapagos/certificates/auth/CertificatesAuthenticationModule.java b/src/main/java/com/hermesworld/ais/galapagos/certificates/auth/CertificatesAuthenticationModule.java index 6d7eb7dc..b4c50e97 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/certificates/auth/CertificatesAuthenticationModule.java +++ b/src/main/java/com/hermesworld/ais/galapagos/certificates/auth/CertificatesAuthenticationModule.java @@ -11,6 +11,7 @@ import org.bouncycastle.pkcs.PKCSException; import org.json.JSONException; import org.json.JSONObject; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import java.io.*; @@ -89,7 +90,7 @@ public CompletableFuture updateApplicationAuthentica if (extendCertificate) { dn = existingAuthData.optString(DN); - if (StringUtils.isEmpty(dn)) { + if (ObjectUtils.isEmpty(dn)) { return CompletableFuture.failedFuture(new IllegalArgumentException( "Cannot extend certificate - no certificate information available for application")); } @@ -106,7 +107,7 @@ private CompletableFuture createOrUpdateApplicationA if (!generateKey) { String csr = createParameters.optString("csrData"); - if (StringUtils.isEmpty(csr)) { + if (ObjectUtils.isEmpty(csr)) { return CompletableFuture.failedFuture(new IllegalArgumentException( "No CSR (csrData) present! Set generateKey to true if you want the server to generate a private key for you (not recommended).")); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/certificates/controller/TrustStoreController.java b/src/main/java/com/hermesworld/ais/galapagos/certificates/controller/TrustStoreController.java index 1e7a816f..75383aa4 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/certificates/controller/TrustStoreController.java +++ b/src/main/java/com/hermesworld/ais/galapagos/certificates/controller/TrustStoreController.java @@ -4,12 +4,11 @@ import com.hermesworld.ais.galapagos.kafka.auth.KafkaAuthenticationModule; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.config.SslConfigs; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.util.ObjectUtils; import org.springframework.util.StreamUtils; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @@ -29,7 +28,6 @@ public class TrustStoreController { private final Supplier notFound = () -> new ResponseStatusException(HttpStatus.NOT_FOUND); - @Autowired public TrustStoreController(KafkaClusters kafkaClusters) { this.kafkaClusters = kafkaClusters; } @@ -49,7 +47,7 @@ public ResponseEntity getTrustStore(@PathVariable String environmentId) module.addRequiredKafkaProperties(props); String trustStoreLocation = props.getProperty(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG); - if (StringUtils.isEmpty(trustStoreLocation)) { + if (ObjectUtils.isEmpty(trustStoreLocation)) { throw notFound.get(); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderRunner.java b/src/main/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderRunner.java index 874c63db..775bf7fd 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderRunner.java +++ b/src/main/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderRunner.java @@ -11,7 +11,6 @@ import lombok.extern.slf4j.Slf4j; import org.json.JSONException; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -45,7 +44,6 @@ public class CertificateExpiryReminderRunner { private final String timezone; - @Autowired public CertificateExpiryReminderRunner(CertificateExpiryReminderService reminderService, NotificationService notificationService, ApplicationsService applicationsService, KafkaClusters kafkaClusters, @Value("${galapagos.timezone:GMT}") String timezone) { diff --git a/src/main/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderServiceImpl.java b/src/main/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderServiceImpl.java index 567a5683..a3e9239e 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderServiceImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderServiceImpl.java @@ -14,7 +14,6 @@ import lombok.extern.slf4j.Slf4j; import org.json.JSONException; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -31,7 +30,6 @@ public class CertificateExpiryReminderServiceImpl implements CertificateExpiryRe private static final String REPOSITORY_NAME = "reminders"; - @Autowired public CertificateExpiryReminderServiceImpl(KafkaClusters kafkaClusters, ApplicationsService applicationsService) { this.kafkaClusters = kafkaClusters; this.applicationsService = applicationsService; diff --git a/src/main/java/com/hermesworld/ais/galapagos/changes/controller/ChangesController.java b/src/main/java/com/hermesworld/ais/galapagos/changes/controller/ChangesController.java index 525ddcad..bf659649 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/changes/controller/ChangesController.java +++ b/src/main/java/com/hermesworld/ais/galapagos/changes/controller/ChangesController.java @@ -3,7 +3,6 @@ import java.util.ArrayList; import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; @@ -17,7 +16,6 @@ public class ChangesController { private ChangesService changesService; - @Autowired public ChangesController(ChangesService changesService) { this.changesService = changesService; } diff --git a/src/main/java/com/hermesworld/ais/galapagos/changes/impl/ChangesServiceImpl.java b/src/main/java/com/hermesworld/ais/galapagos/changes/impl/ChangesServiceImpl.java index c6ae3383..0d39a05c 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/changes/impl/ChangesServiceImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/changes/impl/ChangesServiceImpl.java @@ -13,7 +13,6 @@ import com.hermesworld.ais.galapagos.topics.TopicType; import com.hermesworld.ais.galapagos.util.JsonUtil; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.ZonedDateTime; @@ -31,7 +30,6 @@ public class ChangesServiceImpl private final KafkaClusters kafkaClusters; - @Autowired public ChangesServiceImpl(KafkaClusters kafkaClusters) { this.kafkaClusters = kafkaClusters; } @@ -150,7 +148,7 @@ private ChangeData toChangeData(Change change, Optional principa ChangeData data = new ChangeData(); data.setId(UUID.randomUUID().toString()); data.setPrincipal(principal.map(p -> p.getName()).orElse("_SYSTEM")); - data.setPrincipalFullName(principal.map(p -> p.getFullName() == null ? data.getPrincipal() : p.getFullName()) + data.setPrincipalFullName(principal.filter(p -> p.getFullName() != null).map(AuditPrincipal::getFullName) .orElse(data.getPrincipal())); data.setTimestamp(ZonedDateTime.now()); data.setChange(change); diff --git a/src/main/java/com/hermesworld/ais/galapagos/devauth/impl/DevUserAclListener.java b/src/main/java/com/hermesworld/ais/galapagos/devauth/impl/DevUserAclListener.java index 9a962b9e..6e46742d 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/devauth/impl/DevUserAclListener.java +++ b/src/main/java/com/hermesworld/ais/galapagos/devauth/impl/DevUserAclListener.java @@ -13,6 +13,7 @@ import com.hermesworld.ais.galapagos.subscriptions.service.SubscriptionService; import com.hermesworld.ais.galapagos.util.FutureUtil; import com.hermesworld.ais.galapagos.util.TimeService; +import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.acl.AclBinding; import org.json.JSONException; import org.json.JSONObject; @@ -28,6 +29,7 @@ import java.util.stream.Stream; @Component +@Slf4j public class DevUserAclListener implements TopicEventsListener, SubscriptionEventsListener, ApplicationEventsListener { private final ApplicationsService applicationsService; @@ -202,6 +204,10 @@ public CompletableFuture handleTopicSchemaDeleted(TopicSchemaRemovedEvent @CheckReturnValue CompletableFuture updateAcls(KafkaCluster cluster, Set metadatas) { + if (log.isDebugEnabled()) { + log.debug("Updating ACLs for {} on cluster {}", metadatas.stream().map(m -> m.getUserName()), + cluster.getId()); + } CompletableFuture result = CompletableFuture.completedFuture(null); for (DevAuthenticationMetadata metadata : metadatas) { result = result.thenCompose( @@ -212,6 +218,10 @@ CompletableFuture updateAcls(KafkaCluster cluster, Set removeAcls(KafkaCluster cluster, Set metadatas) { + if (log.isDebugEnabled()) { + log.debug("Removing ACLs for {} on cluster {}", + metadatas.stream().map(m -> m.getUserName()).collect(Collectors.toList()), cluster.getId()); + } CompletableFuture result = CompletableFuture.completedFuture(null); for (DevAuthenticationMetadata metadata : metadatas) { result = result.thenCompose( @@ -280,9 +290,9 @@ public Collection getRequiredAclBindings() { boolean writeAccess = kafkaClusters.getEnvironmentMetadata(environmentId) .map(m -> m.isDeveloperWriteAccess()).orElse(false); - return getApplicationsOfUser(metadata.getUserName(), environmentId).stream() + return aclSupport.simplify(getApplicationsOfUser(metadata.getUserName(), environmentId).stream() .map(a -> aclSupport.getRequiredAclBindings(environmentId, a, getKafkaUserName(), !writeAccess)) - .flatMap(c -> c.stream()).collect(Collectors.toSet()); + .flatMap(c -> c.stream()).collect(Collectors.toSet())); } private Set getApplicationsOfUser(String userName, String environmentId) { diff --git a/src/main/java/com/hermesworld/ais/galapagos/devauth/impl/DeveloperAuthenticationServiceImpl.java b/src/main/java/com/hermesworld/ais/galapagos/devauth/impl/DeveloperAuthenticationServiceImpl.java index aede819e..a949a30d 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/devauth/impl/DeveloperAuthenticationServiceImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/devauth/impl/DeveloperAuthenticationServiceImpl.java @@ -13,12 +13,14 @@ import com.hermesworld.ais.galapagos.util.TimeService; import lombok.extern.slf4j.Slf4j; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; import java.io.OutputStream; -import java.util.*; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -35,7 +37,6 @@ public class DeveloperAuthenticationServiceImpl implements DeveloperAuthenticati private final TimeService timeService; - @Autowired public DeveloperAuthenticationServiceImpl(KafkaClusters kafkaClusters, CurrentUserService currentUserService, DevUserAclListener aclUpdater, TimeService timeService) { this.kafkaClusters = kafkaClusters; @@ -70,12 +71,9 @@ public CompletableFuture createDeveloperAuthenticatio TopicBasedRepository repository = getRepository(cluster); - CompletableFuture removeFuture = repository - .getObject( - userName) - .map(oldMeta -> aclUpdater.removeAcls(cluster, Collections.singleton(oldMeta)) - .thenCompose(o -> authModule.deleteDeveloperAuthentication(userName, - new JSONObject(oldMeta.getAuthenticationJson())))) + CompletableFuture removeFuture = repository.getObject(userName) + .map(oldMeta -> aclUpdater.removeAcls(cluster, Set.of(oldMeta)).thenCompose(o -> authModule + .deleteDeveloperAuthentication(userName, new JSONObject(oldMeta.getAuthenticationJson())))) .orElse(FutureUtil.noop()); return removeFuture.thenCompose(o -> authModule.createDeveloperAuthentication(userName, new JSONObject())) @@ -95,7 +93,7 @@ public CompletableFuture createDeveloperAuthenticatio return meta; }).thenCompose(meta -> meta == null ? CompletableFuture.failedFuture(new NoSuchElementException("No authentication received")) - : aclUpdater.updateAcls(cluster, Collections.singleton(meta)) + : aclUpdater.updateAcls(cluster, Set.of(meta)) .thenCompose(o -> clearExpiredDeveloperAuthenticationsOnAllClusters()) .thenApply(o -> meta))); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/events/impl/GalapagosEventManagerImpl.java b/src/main/java/com/hermesworld/ais/galapagos/events/impl/GalapagosEventManagerImpl.java index 1aa721b3..d9022d48 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/events/impl/GalapagosEventManagerImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/events/impl/GalapagosEventManagerImpl.java @@ -9,7 +9,6 @@ import com.hermesworld.ais.galapagos.topics.SchemaMetadata; import com.hermesworld.ais.galapagos.topics.TopicMetadata; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -28,7 +27,6 @@ public class GalapagosEventManagerImpl implements GalapagosEventManager { private final List contextSources; - @Autowired public GalapagosEventManagerImpl(@Lazy List topicListeners, @Lazy List subscriptionListeners, @Lazy List applicationListeners, @Lazy List contextSources) { diff --git a/src/main/java/com/hermesworld/ais/galapagos/kafka/controller/EnvironmentsController.java b/src/main/java/com/hermesworld/ais/galapagos/kafka/controller/EnvironmentsController.java index f92b5d16..f4299629 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/kafka/controller/EnvironmentsController.java +++ b/src/main/java/com/hermesworld/ais/galapagos/kafka/controller/EnvironmentsController.java @@ -2,7 +2,6 @@ import com.hermesworld.ais.galapagos.kafka.KafkaClusters; import com.hermesworld.ais.galapagos.kafka.config.KafkaEnvironmentConfig; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -17,7 +16,6 @@ public class EnvironmentsController { private final KafkaClusters kafkaEnvironments; - @Autowired public EnvironmentsController(KafkaClusters kafkaEnvironments) { this.kafkaEnvironments = kafkaEnvironments; } diff --git a/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaCluster.java b/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaCluster.java index 04e7f85b..3154f48d 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaCluster.java +++ b/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaCluster.java @@ -24,7 +24,7 @@ import org.apache.kafka.common.resource.PatternType; import org.apache.kafka.common.resource.ResourcePatternFilter; import org.apache.kafka.common.resource.ResourceType; -import org.springframework.util.StringUtils; +import org.springframework.util.ObjectUtils; import java.time.Duration; import java.util.*; @@ -101,8 +101,8 @@ public CompletableFuture removeUserAcls(KafkaUser user) { if (user.getKafkaUserName() == null) { return FutureUtil.noop(); } - return toCompletableFuture(adminClient - .deleteAcls(Collections.singletonList(userAclFilter(user.getKafkaUserName(), ResourceType.ANY))).all()) + return toCompletableFuture( + adminClient.deleteAcls(List.of(userAclFilter(user.getKafkaUserName(), ResourceType.ANY))).all()) .thenApply(o -> null); } @@ -134,7 +134,7 @@ public CompletableFuture createTopic(String topicName, TopicCreateParams t NewTopic newTopic = new NewTopic(topicName, topicCreateParams.getNumberOfPartitions(), (short) topicCreateParams.getReplicationFactor()).configs(topicCreateParams.getTopicConfigs()); - return toCompletableFuture(this.adminClient.createTopics(Collections.singleton(newTopic)).all()); + return toCompletableFuture(this.adminClient.createTopics(Set.of(newTopic)).all()); } @Override @@ -143,10 +143,10 @@ public CompletableFuture deleteTopic(String topicName) { new ResourcePatternFilter(ResourceType.TOPIC, topicName, PatternType.LITERAL), new AccessControlEntryFilter(null, null, AclOperation.ANY, AclPermissionType.ANY)); - KafkaFuture deleteTopicFuture = this.adminClient.deleteTopics(Collections.singleton(topicName)).all(); + KafkaFuture deleteTopicFuture = this.adminClient.deleteTopics(Set.of(topicName)).all(); return toCompletableFuture(deleteTopicFuture) - .thenCompose(o -> toCompletableFuture(adminClient.deleteAcls(Collections.singleton(aclFilter)).all())) + .thenCompose(o -> toCompletableFuture(adminClient.deleteAcls(Set.of(aclFilter)).all())) .thenApply(o -> null); } @@ -154,7 +154,7 @@ public CompletableFuture deleteTopic(String topicName) { public CompletableFuture> getTopicConfig(String topicName) { ConfigResource cres = new ConfigResource(ConfigResource.Type.TOPIC, topicName); - return toCompletableFuture(adminClient.describeConfigs(Collections.singleton(cres)).all()) + return toCompletableFuture(adminClient.describeConfigs(Set.of(cres)).all()) .thenApply(map -> map.getOrDefault(cres, new Config(Collections.emptyList())).entries().stream() .map(entry -> new TopicConfigEntryImpl(entry)).collect(Collectors.toSet())); } @@ -165,11 +165,10 @@ public CompletableFuture> getDefaultTopicConfig() { if (nodes.isEmpty()) { return CompletableFuture.failedFuture(new KafkaException("No nodes in cluster")); } - return toCompletableFuture( - adminClient - .describeConfigs(Collections.singleton( - new ConfigResource(ConfigResource.Type.BROKER, "" + nodes.iterator().next().id()))) - .all()); + return toCompletableFuture(adminClient + .describeConfigs( + Set.of(new ConfigResource(ConfigResource.Type.BROKER, "" + nodes.iterator().next().id()))) + .all()); }).thenApply(map -> KafkaTopicConfigHelper.getTopicDefaultValues(map.values().iterator().next())); } @@ -179,9 +178,7 @@ public CompletableFuture setTopicConfig(String topicName, Map getActiveBrokerCount() { @Override public CompletableFuture buildTopicCreateParams(String topicName) { - return toCompletableFuture(adminClient.describeTopics(Collections.singleton(topicName)).all()) + return toCompletableFuture(adminClient.describeTopics(Set.of(topicName)).all()) .thenCompose(map -> buildCreateTopicParams(map.get(topicName))); } @@ -203,7 +200,7 @@ public CompletableFuture>> peekTopicData(Str Runnable r = () -> { KafkaConsumer consumer = kafkaConsumerFactory.newConsumer(); - consumer.subscribe(Collections.singleton(topicName), new ConsumerRebalanceListener() { + consumer.subscribe(Set.of(topicName), new ConsumerRebalanceListener() { @Override public void onPartitionsRevoked(Collection partitions) { } @@ -267,7 +264,7 @@ private CompletableFuture buildCreateTopicParams(TopicDescrip } private CompletableFuture> getUserAcls(String username) { - if (StringUtils.isEmpty(username)) { + if (ObjectUtils.isEmpty(username)) { return CompletableFuture.completedFuture(List.of()); } return toCompletableFuture(adminClient.describeAcls(userAclFilter(username, ResourceType.ANY)).values()); @@ -291,7 +288,7 @@ public CompletableFuture getKafkaServerVersion() { String nodeName = coll.iterator().next().idString(); return toCompletableFuture(adminClient.describeConfigs( - Collections.singleton(new ConfigResource(Type.BROKER, nodeName))).all()).thenApply(map -> map + Set.of(new ConfigResource(Type.BROKER, nodeName))).all()).thenApply(map -> map .values().stream() .map(config -> config.get("inter.broker.protocol.version") == null ? "UNKNOWN_VERSION" : config.get("inter.broker.protocol.version").value()) diff --git a/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaClusters.java b/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaClusters.java index 609b2a93..64d5a29e 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaClusters.java +++ b/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaClusters.java @@ -7,7 +7,7 @@ import com.hermesworld.ais.galapagos.kafka.config.KafkaEnvironmentConfig; import com.hermesworld.ais.galapagos.kafka.util.TopicBasedRepository; import com.hermesworld.ais.galapagos.util.HasKey; -import org.springframework.util.StringUtils; +import org.springframework.util.ObjectUtils; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -81,7 +81,7 @@ public String getProductionEnvironmentId() { @Override public Optional getEnvironment(String environmentId) { - if (StringUtils.isEmpty(environmentId)) { + if (ObjectUtils.isEmpty(environmentId)) { return Optional.empty(); } return Optional.ofNullable(clusters.get(environmentId)); diff --git a/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaFutureDecoupler.java b/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaFutureDecoupler.java index 5793986a..a9110d8e 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaFutureDecoupler.java +++ b/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaFutureDecoupler.java @@ -50,6 +50,29 @@ public CompletableFuture toCompletableFuture(KafkaFuture future) { })); } + /** + * Returns a {@link CompletableFuture} which completes when the given {@link CompletableFuture} completes. If the + * ListenableFuture is already complete, a completed Future is returned. Otherwise, the returned Future + * completes on a Thread provided by a fresh ExecutorService of the KafkaExecutorFactory + * provided for this helper class. + * + * @param Type of the value provided by the Future. + * @param completableFuture Future which may be complete, or which may complete on the Kafka Thread. + * + * @return A completable Future which may be already complete if the original Future already was complete, or which + * completes on a Thread decoupled from the Kafka Thread. + */ + public CompletableFuture toCompletableFuture(CompletableFuture completableFuture) { + return toCompletableFuture(completableFuture, cb -> completableFuture.whenComplete((t, ex) -> { + if (ex != null) { + cb.onFailure(ex); + } + else { + cb.onSuccess(t); + } + })); + } + /** * Returns a {@link CompletableFuture} which completes when the given {@link ListenableFuture} completes. If the * ListenableFuture is already complete, a completed Future is returned. Otherwise, the returned Future diff --git a/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaRepositoryContainerImpl.java b/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaRepositoryContainerImpl.java index f1901e48..ae2ccd8a 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaRepositoryContainerImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaRepositoryContainerImpl.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -71,8 +72,13 @@ public KafkaRepositoryContainerImpl(KafkaConnectionManager connectionManager, St public void dispose() { if (this.consumerThread != null) { - this.consumer.wakeup(); - // consumer thread will terminate by the wakeup + if (this.repositories.isEmpty()) { + this.consumerThread.interrupt(); + } + else { + // consumer thread will terminate by the wakeup + this.consumer.wakeup(); + } this.consumerThread = null; } } @@ -111,7 +117,7 @@ private void ensureTopicExists(String topic) { Map desc; try { - desc = this.adminClient.describeTopics(Collections.singleton(topic)).all().get(); + desc = this.adminClient.describeTopics(Set.of(topic)).all().get(); } catch (Exception e) { desc = Collections.emptyMap(); @@ -125,7 +131,7 @@ private void ensureTopicExists(String topic) { NewTopic newTopic = new NewTopic(topic, 1, (short) replicationFactor); newTopic = newTopic .configs(Map.of(TopicConfig.CLEANUP_POLICY_CONFIG, TopicConfig.CLEANUP_POLICY_COMPACT)); - this.adminClient.createTopics(Collections.singleton(newTopic)).all().get(); + this.adminClient.createTopics(Set.of(newTopic)).all().get(); } } catch (InterruptedException e) { diff --git a/src/main/java/com/hermesworld/ais/galapagos/kafka/util/AclSupport.java b/src/main/java/com/hermesworld/ais/galapagos/kafka/util/AclSupport.java index 6d36eea7..ccfd1ada 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/kafka/util/AclSupport.java +++ b/src/main/java/com/hermesworld/ais/galapagos/kafka/util/AclSupport.java @@ -16,7 +16,6 @@ import java.util.*; import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Singleton Component helping with recurring tasks regarding Kafka ACLs; mainly, calculating required ACLs for given @@ -25,11 +24,11 @@ @Component public class AclSupport { - private static final List READ_TOPIC_OPERATIONS = Arrays.asList(AclOperation.DESCRIBE, - AclOperation.DESCRIBE_CONFIGS, AclOperation.READ); + private static final List READ_TOPIC_OPERATIONS = Arrays.asList(allow(AclOperation.READ), + allow(AclOperation.DESCRIBE_CONFIGS)); - private static final List WRITE_TOPIC_OPERATIONS = Arrays.asList(AclOperation.DESCRIBE, - AclOperation.DESCRIBE_CONFIGS, AclOperation.READ, AclOperation.WRITE); + private static final List WRITE_TOPIC_OPERATIONS = Arrays.asList(allow(AclOperation.ALL), + denyDelete()); private final KafkaEnvironmentsConfig kafkaConfig; @@ -50,12 +49,6 @@ public Collection getRequiredAclBindings(String environmentId, Appli String applicationId = applicationMetadata.getApplicationId(); - // every application gets the DESCRIBE CLUSTER right (and also DESCRIBE_CONFIGS, for now) - result.add(new AclBinding(new ResourcePattern(ResourceType.CLUSTER, "kafka-cluster", PatternType.LITERAL), - new AccessControlEntry(kafkaUserName, "*", AclOperation.DESCRIBE, AclPermissionType.ALLOW))); - result.add(new AclBinding(new ResourcePattern(ResourceType.CLUSTER, "kafka-cluster", PatternType.LITERAL), - new AccessControlEntry(kafkaUserName, "*", AclOperation.DESCRIBE_CONFIGS, AclPermissionType.ALLOW))); - // add configured default ACLs, if any if (kafkaConfig.getDefaultAcls() != null) { result.addAll(kafkaConfig.getDefaultAcls().stream() @@ -65,16 +58,17 @@ public Collection getRequiredAclBindings(String environmentId, Appli .collect(Collectors.toList())); } - List internalTopicOps = readOnly ? READ_TOPIC_OPERATIONS : List.of(AclOperation.ALL); + List internalTopicOps = readOnly ? READ_TOPIC_OPERATIONS : ALLOW_ALL; result.addAll(applicationMetadata.getInternalTopicPrefixes().stream() .flatMap(prefix -> prefixAcls(kafkaUserName, ResourceType.TOPIC, prefix, internalTopicOps).stream()) .collect(Collectors.toList())); if (!readOnly) { - result.addAll(applicationMetadata.getTransactionIdPrefixes().stream() - .flatMap(prefix -> transactionAcls(kafkaUserName, prefix).stream()).collect(Collectors.toList())); - result.addAll(applicationMetadata.getConsumerGroupPrefixes().stream().flatMap( - prefix -> prefixAcls(kafkaUserName, ResourceType.GROUP, prefix, List.of(AclOperation.ALL)).stream()) + result.addAll(applicationMetadata.getTransactionIdPrefixes().stream().flatMap( + prefix -> prefixAcls(kafkaUserName, ResourceType.TRANSACTIONAL_ID, prefix, ALLOW_ALL).stream()) + .collect(Collectors.toList())); + result.addAll(applicationMetadata.getConsumerGroupPrefixes().stream() + .flatMap(prefix -> prefixAcls(kafkaUserName, ResourceType.GROUP, prefix, ALLOW_ALL).stream()) .collect(Collectors.toList())); } @@ -98,27 +92,99 @@ public Collection getRequiredAclBindings(String environmentId, Appli return result; } + /** + * "Simplifies" (reduces) the given set of ACLs. For example, if there are two identical ACLs, one allows ALL for a + * resource pattern and principal, and one allows READ for the very same resource pattern and principal, the READ + * ACL can safely be removed. + * + * @param aclBindings Set of ACL Bindings to simplify. + * @return Simplified (potentially identical) set of ACL Bindings. + */ + public Collection simplify(Collection aclBindings) { + Set allowedAllPatterns = aclBindings.stream() + .filter(acl -> acl.entry().permissionType() == AclPermissionType.ALLOW + && acl.entry().operation() == AclOperation.ALL) + .map(acl -> new ResourcePatternAndPrincipal(acl.pattern(), acl.entry().principal())) + .collect(Collectors.toSet()); + + if (allowedAllPatterns.isEmpty()) { + return aclBindings; + } + + return aclBindings.stream() + .filter(acl -> acl.entry().operation() == AclOperation.ALL + || acl.entry().permissionType() == AclPermissionType.DENY + || !allowedAllPatterns + .contains(new ResourcePatternAndPrincipal(acl.pattern(), acl.entry().principal()))) + .collect(Collectors.toSet()); + } + private Collection prefixAcls(String userName, ResourceType resourceType, String prefix, - List ops) { - ResourcePattern pattern = new ResourcePattern(resourceType, prefix, PatternType.PREFIXED); - return ops.stream() - .map(op -> new AclBinding(pattern, new AccessControlEntry(userName, "*", op, AclPermissionType.ALLOW))) + List ops) { + return ops.stream().map(op -> op.toBinding(prefix, resourceType, PatternType.PREFIXED, userName)) .collect(Collectors.toList()); } - private Collection topicAcls(String userName, String topicName, List ops) { - ResourcePattern pattern = new ResourcePattern(ResourceType.TOPIC, topicName, PatternType.LITERAL); - return ops.stream() - .map(op -> new AclBinding(pattern, new AccessControlEntry(userName, "*", op, AclPermissionType.ALLOW))) + private Collection topicAcls(String userName, String topicName, List ops) { + return ops.stream().map(op -> op.toBinding(topicName, ResourceType.TOPIC, PatternType.LITERAL, userName)) .collect(Collectors.toList()); } - private Collection transactionAcls(String userName, String prefix) { - return Stream.of(AclOperation.DESCRIBE, AclOperation.WRITE) - .map(op -> new AclBinding( - new ResourcePattern(ResourceType.TRANSACTIONAL_ID, prefix, PatternType.PREFIXED), - new AccessControlEntry(userName, "*", op, AclPermissionType.ALLOW))) - .collect(Collectors.toSet()); + private static class AclOperationAndType { + + private final AclOperation operation; + + private final AclPermissionType permissionType; + + private AclOperationAndType(AclOperation operation, AclPermissionType permissionType) { + this.operation = operation; + this.permissionType = permissionType; + } + + public AclBinding toBinding(String resourceName, ResourceType resourceType, PatternType patternType, + String principal) { + return new AclBinding(new ResourcePattern(resourceType, resourceName, patternType), + new AccessControlEntry(principal, "*", operation, permissionType)); + } } + private static class ResourcePatternAndPrincipal { + + private final ResourcePattern resourcePattern; + + private final String principal; + + private ResourcePatternAndPrincipal(ResourcePattern resourcePattern, String principal) { + this.resourcePattern = resourcePattern; + this.principal = principal; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ResourcePatternAndPrincipal that = (ResourcePatternAndPrincipal) obj; + return resourcePattern.equals(that.resourcePattern) && principal.equals(that.principal); + } + + @Override + public int hashCode() { + return Objects.hash(resourcePattern, principal); + } + } + + private static AclOperationAndType allow(AclOperation op) { + return new AclOperationAndType(op, AclPermissionType.ALLOW); + } + + private static AclOperationAndType denyDelete() { + return new AclOperationAndType(AclOperation.DELETE, AclPermissionType.DENY); + } + + private static final List ALLOW_ALL = List.of(allow(AclOperation.ALL)); + } diff --git a/src/main/java/com/hermesworld/ais/galapagos/kafka/util/KafkaTopicConfigHelper.java b/src/main/java/com/hermesworld/ais/galapagos/kafka/util/KafkaTopicConfigHelper.java index a27fe952..819417cb 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/kafka/util/KafkaTopicConfigHelper.java +++ b/src/main/java/com/hermesworld/ais/galapagos/kafka/util/KafkaTopicConfigHelper.java @@ -1,7 +1,6 @@ package com.hermesworld.ais.galapagos.kafka.util; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -87,9 +86,9 @@ public class KafkaTopicConfigHelper { .valueOf(TimeUnit.MINUTES.toMillis(Long.valueOf(minutes, 10)))), new SecondaryServerProp("log.retention.hours", hoursToMillis))); SECONDARY_SERVER_PROPS.put(TopicConfig.SEGMENT_MS_CONFIG, - Collections.singletonList(new SecondaryServerProp("log.roll.hours", hoursToMillis))); + List.of(new SecondaryServerProp("log.roll.hours", hoursToMillis))); SECONDARY_SERVER_PROPS.put(TopicConfig.SEGMENT_JITTER_MS_CONFIG, - Collections.singletonList(new SecondaryServerProp("log.roll.jitter.hours", hoursToMillis))); + List.of(new SecondaryServerProp("log.roll.jitter.hours", hoursToMillis))); } private KafkaTopicConfigHelper() { diff --git a/src/main/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategy.java b/src/main/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategy.java index 70c3465b..d9a457b5 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategy.java +++ b/src/main/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategy.java @@ -1,5 +1,6 @@ package com.hermesworld.ais.galapagos.naming.config; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import java.util.List; @@ -45,7 +46,7 @@ public String configValue() { } public boolean matches(String input) { - if (StringUtils.isEmpty(input)) { + if (ObjectUtils.isEmpty(input)) { return false; } return input.matches(validatorRegex); diff --git a/src/main/java/com/hermesworld/ais/galapagos/naming/config/NamingConfig.java b/src/main/java/com/hermesworld/ais/galapagos/naming/config/NamingConfig.java index 66920ddc..4a26b54a 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/naming/config/NamingConfig.java +++ b/src/main/java/com/hermesworld/ais/galapagos/naming/config/NamingConfig.java @@ -52,8 +52,7 @@ public void validate(@NonNull Object target, @NonNull Errors errors) { if (target instanceof String && objName.endsWith("-format")) { checkValidFormat(target.toString(), errors); } - else if (target instanceof AdditionNamingRules) { - AdditionNamingRules rules = (AdditionNamingRules) target; + else if (target instanceof AdditionNamingRules rules) { if (!StringUtils.isEmpty(rules.getAllowedSeparators()) && !rules.getAllowedSeparators().matches(KAFKA_VALID_NAMES_REGEX)) { errors.rejectValue("allowedSeparators", "invalid.value", diff --git a/src/main/java/com/hermesworld/ais/galapagos/naming/impl/NamingServiceImpl.java b/src/main/java/com/hermesworld/ais/galapagos/naming/impl/NamingServiceImpl.java index 1b35b8be..070f9088 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/naming/impl/NamingServiceImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/naming/impl/NamingServiceImpl.java @@ -11,7 +11,6 @@ import com.hermesworld.ais.galapagos.naming.config.TopicNamingConfig; import com.hermesworld.ais.galapagos.topics.TopicType; import com.ibm.icu.text.Transliterator; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -28,7 +27,6 @@ public class NamingServiceImpl implements NamingService { private final NamingConfig config; - @Autowired public NamingServiceImpl(NamingConfig config) { this.config = config; } diff --git a/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/MailConfig.java b/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/MailConfig.java index 25afb2ad..a6b8def6 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/MailConfig.java +++ b/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/MailConfig.java @@ -4,7 +4,7 @@ import org.springframework.context.annotation.Configuration; import org.thymeleaf.TemplateEngine; import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect; -import org.thymeleaf.spring5.SpringTemplateEngine; +import org.thymeleaf.spring6.SpringTemplateEngine; import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; import org.thymeleaf.templateresolver.ITemplateResolver; diff --git a/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationEventListener.java b/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationEventListener.java index 430270d0..6a1ec806 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationEventListener.java +++ b/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationEventListener.java @@ -13,12 +13,11 @@ import com.hermesworld.ais.galapagos.topics.service.TopicService; import com.hermesworld.ais.galapagos.util.FutureUtil; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; @@ -60,7 +59,6 @@ public class NotificationEventListener private static final String IS_ADMIN_KEY = NotificationEventListener.class.getName() + "_isAdmin"; - @Autowired public NotificationEventListener(NotificationService notificationService, ApplicationsService applicationsService, TopicService topicService, CurrentUserService userService, KafkaClusters kafkaClusters) { this.notificationService = notificationService; diff --git a/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationServiceImpl.java b/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationServiceImpl.java index 93bb76fe..72ae08ef 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationServiceImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationServiceImpl.java @@ -11,7 +11,6 @@ import lombok.extern.slf4j.Slf4j; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.task.TaskExecutor; @@ -19,14 +18,15 @@ import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.thymeleaf.ITemplateEngine; import org.thymeleaf.context.Context; -import javax.mail.MessagingException; -import javax.mail.internet.AddressException; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeMessage; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -51,7 +51,6 @@ public class NotificationServiceImpl implements NotificationService { private final List adminMailRecipients; - @Autowired public NotificationServiceImpl(SubscriptionService subscriptionService, ApplicationsService applicationsService, TopicService topicService, JavaMailSender mailSender, TaskExecutor taskExecutor, @Qualifier("emailTemplateEngine") ITemplateEngine templateEngine, @@ -236,7 +235,7 @@ private List safeToRecipientsList(Collection recipients } private static List toRecipientsList(String recipients) throws AddressException { - if (StringUtils.isEmpty(recipients)) { + if (ObjectUtils.isEmpty(recipients)) { return Collections.emptyList(); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/schemas/ConsumerCompatibilityErrorHandler.java b/src/main/java/com/hermesworld/ais/galapagos/schemas/ConsumerCompatibilityErrorHandler.java new file mode 100644 index 00000000..a849c245 --- /dev/null +++ b/src/main/java/com/hermesworld/ais/galapagos/schemas/ConsumerCompatibilityErrorHandler.java @@ -0,0 +1,258 @@ +package com.hermesworld.ais.galapagos.schemas; + +import org.everit.json.schema.ArraySchema; + +import static com.hermesworld.ais.galapagos.schemas.SchemaUtil.fullPropName; +import static com.hermesworld.ais.galapagos.schemas.SchemaUtil.propLocationName; + +public class ConsumerCompatibilityErrorHandler implements SchemaCompatibilityErrorHandler { + + private final boolean allowRemovedOptionalProperties; + + public ConsumerCompatibilityErrorHandler() { + this(false); + } + + public ConsumerCompatibilityErrorHandler(boolean allowRemovedOptionalProperties) { + this.allowRemovedOptionalProperties = allowRemovedOptionalProperties; + } + + @Override + public void handleCombinedSchemaReplacedByIncompatibleSchema(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "Combined schema at " + propLocationName(context) + " is replaced by incompatible schema"); + } + + @Override + public void handleSchemaTypeDiffers(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Schema type differs at " + propLocationName(context) + " (new: " + + context.getCurrentNodeInNewSchema().getClass().getSimpleName() + ", old: " + + context.getCurrentNodeInOldSchema().getClass().getSimpleName() + ")"); + } + + @Override + public void handleAdditionalPropertiesIntroduced(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("additionalProperties must not be introduced " + + "in a newer schema version (old consumers could rely on no additional properties being present)"); + } + + @Override + public void handlePropertyNoLongerRequired(SchemaCompatibilityValidationContext context, String property) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Property " + fullPropName(context, property) + + " is no longer required, but old consumers could rely on it."); + } + + @Override + public void handleMinPropertiesReduced(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Minimum number of properties cannot be reduced at " + + propLocationName(context) + ". Old consumers could rely on having at least N properties."); + } + + @Override + public void handleDefinedPropertyNoLongerDefined(SchemaCompatibilityValidationContext context, String property) + throws IncompatibleSchemaException { + if (!allowRemovedOptionalProperties) { + throw new IncompatibleSchemaException("Property " + fullPropName(context, property) + + " is no longer defined in schema, but had a strong definition before. " + + "Field format could have changed, and old consumers perhaps rely on previous format"); + } + } + + @Override + public void handleRegexpPatternRemoved(SchemaCompatibilityValidationContext context, String pattern) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "Pattern " + pattern + " is no longer defined for new schema at " + propLocationName(context) + + ". Consumers could rely on the schema specification for matching properties."); + } + + @Override + public void handleMultipleOfConstraintRemoved(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "multipleOf constraint must not be removed on number property " + propLocationName(context)); + } + + @Override + public void handleMultipleOfConstraintChanged(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "multipleOf constraint of property " + propLocationName(context) + " changed in an incompatible way"); + } + + @Override + public void handleIntegerChangedToNumber(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Old schema only allowed integer values at property " + + propLocationName(context) + + " while new schema allows non-integer values. Consumers may not be able to handle non-integer values"); + } + + @Override + public void handleNumberAllowsLowerValues(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("New schema allows lower values for number property " + + propLocationName(context) + " than old schema. Consumers may not be prepared for this."); + } + + @Override + public void handleNumberAllowsGreaterValues(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("New schema allows greater values for number property " + + propLocationName(context) + " than old schema. Consumers may not be prepared for this."); + } + + @Override + public void handleArrayUniqueItemsRemoved(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Old schema guaranteed unique items for array at " + + propLocationName(context) + " while new schema removes this guarantee"); + } + + @Override + public void handleArrayAllowsLessItems(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Old schema had a minimum of " + + ((ArraySchema) context.getCurrentNodeInOldSchema()).getMinItems() + " items for array at " + + propLocationName(context) + ", while new schema does not guarantee this number of items in array"); + } + + @Override + public void handleArrayAllowsMoreItems(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "Old schema had a maximum of " + ((ArraySchema) context.getCurrentNodeInOldSchema()).getMaxItems() + + " items for array at " + propLocationName(context) + ", while new schema allows more items."); + } + + @Override + public void handleArrayAllSchemaToContainsSchema(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Array at " + propLocationName(context) + + " cannot be reduced from a schema for ALL items to a schema for at least ONE (contains) item"); + } + + @Override + public void handleArrayItemSchemaRemoved(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Array at " + propLocationName(context) + + " had a strong typing for items before which cannot be removed (old consumers could rely on that format)"); + } + + @Override + public void handleArrayContainsSchemaNotFound(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Array at " + propLocationName(context) + + " had a contains definition before which can not be found for any item of that array in the new schema"); + } + + @Override + public void handleArrayLessItemSchemas(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + ArraySchema oldSchema = (ArraySchema) context.getCurrentNodeInOldSchema(); + ArraySchema newSchema = (ArraySchema) context.getCurrentNodeInNewSchema(); + throw new IncompatibleSchemaException("Array at " + propLocationName(context) + " had a strong typing for " + + oldSchema.getItemSchemas().size() + " elements, but new schema only defines " + + newSchema.getItemSchemas().size() + " here"); + } + + @Override + public void handleArrayMoreItemSchemasThanAllowed(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("New schema defines more items in array at " + propLocationName(context) + + " than were previously allowed here."); + } + + @Override + public void handleArrayLessItemsThanSchemas(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + ArraySchema oldSchema = (ArraySchema) context.getCurrentNodeInOldSchema(); + throw new IncompatibleSchemaException("Old schema defined " + oldSchema.getItemSchemas().size() + + " items for array at " + propLocationName(context) + + ", but new schema does not guarantee this number of items in the array"); + } + + @Override + public void handleArrayMoreItemsThanSchemas(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Old schema did not accept additional items for array at " + + propLocationName(context) + ", but new schema has higher limit for number of items in array"); + } + + @Override + public void handleArrayItemSchemasRemoved(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "Old schema made stronger guarantees for array at " + propLocationName(context) + + " than new schema does. Consumers could rely on previously guaranteed items"); + } + + @Override + public void handleShorterStringsAllowed(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "New schema allows for shorter strings than old schema for string property " + + propLocationName(context)); + } + + @Override + public void handleLongerStringsAllowed(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "New schema allows for longer strings than old schema for string property " + + propLocationName(context)); + } + + @Override + public void handleStringPatternChanged(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "New schema defines no or different pattern for string property " + propLocationName(context)); + } + + @Override + public void handleStringFormatChanged(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("format differs for string property " + propLocationName(context)); + } + + @Override + public void handleCombineOperatorChanged(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "New schema defines differed combination operator at " + propLocationName(context)); + } + + @Override + public void handleCombineSubschemaNoMatch(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "New schema contains at least one incompatible subschema at property " + propLocationName(context)); + } + + @Override + public void handleConstantValueChanged(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "Constant value for property " + propLocationName(context) + " has changed in new schema"); + } + + @Override + public void handleConditionalSchemaDiffers(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Conditional schema differs at " + propLocationName(context)); + } + + @Override + public void handleEnumValueAdded(SchemaCompatibilityValidationContext context, String value) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Schema introduces new enum value for property " + + propLocationName(context) + ": " + value + ". Consumers may not expect this value."); + } + +} diff --git a/src/main/java/com/hermesworld/ais/galapagos/schemas/ProducerCompatibilityErrorHandler.java b/src/main/java/com/hermesworld/ais/galapagos/schemas/ProducerCompatibilityErrorHandler.java new file mode 100644 index 00000000..7463093e --- /dev/null +++ b/src/main/java/com/hermesworld/ais/galapagos/schemas/ProducerCompatibilityErrorHandler.java @@ -0,0 +1,208 @@ +package com.hermesworld.ais.galapagos.schemas; + +import org.everit.json.schema.ArraySchema; + +import static com.hermesworld.ais.galapagos.schemas.SchemaUtil.fullPropName; +import static com.hermesworld.ais.galapagos.schemas.SchemaUtil.propLocationName; + +/** + * As the compatibility validation is always "consumer-view", checks for producer compatibility are performed with + * swapped schemas. This is why this error handler "swaps" the error messages again to make them more helpful.
+ * Also, it allows for some "liberal" settings which are useful in most cases. + */ +public class ProducerCompatibilityErrorHandler extends ConsumerCompatibilityErrorHandler { + + private final boolean allowNewOptionalProperties; + + public ProducerCompatibilityErrorHandler(boolean allowNewOptionalProperties) { + this.allowNewOptionalProperties = allowNewOptionalProperties; + } + + @Override + public void handleDefinedPropertyNoLongerDefined(SchemaCompatibilityValidationContext context, String property) + throws IncompatibleSchemaException { + if (!allowNewOptionalProperties) { + throw new IncompatibleSchemaException("Property " + fullPropName(context, property) + + " is newly defined. Producers could already use this property (as additional properties are allowed) in a different format."); + } + } + + @Override + public void handleCombinedSchemaReplacedByIncompatibleSchema(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "Schemas at " + propLocationName(context) + " have changed in an incompatible way"); + } + + @Override + public void handleSchemaTypeDiffers(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Schema type differs at " + propLocationName(context) + " (old: " + + context.getCurrentNodeInNewSchema().getClass().getSimpleName() + ", new: " + + context.getCurrentNodeInOldSchema().getClass().getSimpleName() + ")"); + } + + @Override + public void handleAdditionalPropertiesIntroduced(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("additionalProperties must not be removed " + + "in a newer schema version (old producers could currently provide additional properties)"); + } + + @Override + public void handlePropertyNoLongerRequired(SchemaCompatibilityValidationContext context, String property) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Property " + fullPropName(context, property) + + " is now required, but old producers may not yet provide it."); + } + + @Override + public void handleMinPropertiesReduced(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Minimum number of properties cannot be increased at " + + propLocationName(context) + ". Old producers could provide too few properties."); + } + + @Override + public void handleRegexpPatternRemoved(SchemaCompatibilityValidationContext context, String pattern) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("New pattern " + pattern + " introduced in new schema at " + + propLocationName(context) + ". Producers may not yet adhere to this pattern."); + } + + @Override + public void handleMultipleOfConstraintRemoved(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("multipleOf constraint added on number property " + + propLocationName(context) + ". Current producers may provide incompatible values."); + } + + @Override + public void handleIntegerChangedToNumber(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "Old schema allowed any numeric values at property " + propLocationName(context) + + " while new schema only allows integer values. Producers may provide incompatible values."); + } + + @Override + public void handleNumberAllowsLowerValues(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("New schema has increased lower boundary for values for number property " + + propLocationName(context) + " than old schema. Producers may provide incompatible values."); + } + + @Override + public void handleNumberAllowsGreaterValues(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("New schema has decreased upper boundary for values for number property " + + propLocationName(context) + " than old schema. Providers may provide incompatible values."); + } + + @Override + public void handleArrayUniqueItemsRemoved(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("New schema requires unique items for array at " + + propLocationName(context) + ". Producers may not fulfill this requirement."); + } + + @Override + public void handleArrayAllowsLessItems(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "New schema requires a minimum of " + ((ArraySchema) context.getCurrentNodeInOldSchema()).getMinItems() + + " items for array at " + propLocationName(context) + + ", while old schema did not require that many items. Producers may provide too few items."); + } + + @Override + public void handleArrayAllowsMoreItems(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "New schema allows a maximum of " + ((ArraySchema) context.getCurrentNodeInOldSchema()).getMaxItems() + + " items for array at " + propLocationName(context) + + ", while old schema allowed more items. Producers may provide too many items."); + } + + @Override + public void handleArrayAllSchemaToContainsSchema(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Array at " + propLocationName(context) + + " cannot be changed from a schema for at least ONE (contains) item to a schema for ALL items."); + } + + @Override + public void handleArrayItemSchemaRemoved(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "Array at " + propLocationName(context) + " cannot introduce a strong typing for items."); + } + + @Override + public void handleArrayContainsSchemaNotFound(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Array at " + propLocationName(context) + + " introduces a contains definition which was previously not present as any schema."); + } + + @Override + public void handleArrayLessItemSchemas(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "Array at " + propLocationName(context) + " defines strong schemas for more items than before."); + } + + @Override + public void handleArrayMoreItemSchemasThanAllowed(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("New schema allows less items in array at " + propLocationName(context) + + " than were previously allowed here."); + } + + @Override + public void handleArrayLessItemsThanSchemas(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + ArraySchema newSchema = (ArraySchema) context.getCurrentNodeInOldSchema(); + throw new IncompatibleSchemaException( + "New schema defines " + newSchema.getItemSchemas().size() + " items for array at " + + propLocationName(context) + ", but old schema did not force this number of items."); + } + + @Override + public void handleArrayMoreItemsThanSchemas(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("New schema does not accept additional items for array at " + + propLocationName(context) + + ", but old schema had higher limit for number of items in array. Producers may provide too many items."); + } + + @Override + public void handleArrayItemSchemasRemoved(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "New schema makes stronger requirements for array at " + propLocationName(context) + + " than old schema does. Producers may not adhere to stronger definition."); + } + + @Override + public void handleShorterStringsAllowed(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "New schema requires longer strings than old schema for string property " + propLocationName(context)); + } + + @Override + public void handleLongerStringsAllowed(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "New schema requires shorter strings than old schema for string property " + propLocationName(context)); + } + + @Override + public void handleEnumValueAdded(SchemaCompatibilityValidationContext context, String value) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Schema removes enum value for property " + propLocationName(context) + + ": " + value + ". Current producers may still use this value."); + } + +} diff --git a/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaCompatibilityErrorHandler.java b/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaCompatibilityErrorHandler.java new file mode 100644 index 00000000..e88bcb1d --- /dev/null +++ b/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaCompatibilityErrorHandler.java @@ -0,0 +1,97 @@ +package com.hermesworld.ais.galapagos.schemas; + +import static com.hermesworld.ais.galapagos.schemas.SchemaUtil.propLocationName; + +public interface SchemaCompatibilityErrorHandler { + + void handleCombinedSchemaReplacedByIncompatibleSchema(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handleSchemaTypeDiffers(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleAdditionalPropertiesIntroduced(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handlePropertyNoLongerRequired(SchemaCompatibilityValidationContext context, String property) + throws IncompatibleSchemaException; + + void handleMinPropertiesReduced(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + default void handleInvalidDependenciesConfiguration(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException( + "Invalid dependencies configuration found at " + propLocationName(context)); + } + + void handleDefinedPropertyNoLongerDefined(SchemaCompatibilityValidationContext context, String property) + throws IncompatibleSchemaException; + + void handleRegexpPatternRemoved(SchemaCompatibilityValidationContext context, String pattern) + throws IncompatibleSchemaException; + + void handleMultipleOfConstraintRemoved(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handleMultipleOfConstraintChanged(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handleIntegerChangedToNumber(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleNumberAllowsLowerValues(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleNumberAllowsGreaterValues(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handleArrayUniqueItemsRemoved(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleArrayAllowsLessItems(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleArrayAllowsMoreItems(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleArrayAllSchemaToContainsSchema(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handleArrayItemSchemaRemoved(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleArrayContainsSchemaNotFound(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handleArrayLessItemSchemas(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleArrayMoreItemSchemasThanAllowed(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handleArrayLessItemsThanSchemas(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handleArrayMoreItemsThanSchemas(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handleArrayItemSchemasRemoved(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleShorterStringsAllowed(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleLongerStringsAllowed(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleStringPatternChanged(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleStringFormatChanged(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleCombineOperatorChanged(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleCombineSubschemaNoMatch(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + default void handleUnresolvedSchemaReference(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException { + throw new IncompatibleSchemaException("Reference at " + propLocationName(context) + " could not be resolved"); + } + + void handleConstantValueChanged(SchemaCompatibilityValidationContext context) throws IncompatibleSchemaException; + + void handleConditionalSchemaDiffers(SchemaCompatibilityValidationContext context) + throws IncompatibleSchemaException; + + void handleEnumValueAdded(SchemaCompatibilityValidationContext context, String value) + throws IncompatibleSchemaException; + +} diff --git a/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaCompatibilityValidationContext.java b/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaCompatibilityValidationContext.java new file mode 100644 index 00000000..66c82303 --- /dev/null +++ b/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaCompatibilityValidationContext.java @@ -0,0 +1,17 @@ +package com.hermesworld.ais.galapagos.schemas; + +import org.everit.json.schema.Schema; + +public interface SchemaCompatibilityValidationContext { + + String getCurrentPrefix(); + + Schema getOldSchema(); + + Schema getNewSchema(); + + Schema getCurrentNodeInOldSchema(); + + Schema getCurrentNodeInNewSchema(); + +} diff --git a/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaCompatibilityValidator.java b/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaCompatibilityValidator.java new file mode 100644 index 00000000..c5ece835 --- /dev/null +++ b/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaCompatibilityValidator.java @@ -0,0 +1,662 @@ +package com.hermesworld.ais.galapagos.schemas; + +import org.everit.json.schema.*; +import org.everit.json.schema.regexp.Regexp; + +import java.lang.reflect.Field; +import java.util.*; + +public class SchemaCompatibilityValidator { + + // Known limitations: + + // - Regex Pattern compatibility is not checked - every difference is treated as incompatible. + // Theoretically, there could be stricter patterns which include the previous patterns. + + private final ValidationContextImpl context = new ValidationContextImpl(); + + private final SchemaCompatibilityErrorHandler errorHandler; + + public SchemaCompatibilityValidator(Schema oldSchema, Schema newSchema, + SchemaCompatibilityErrorHandler errorHandler) { + context.oldSchema = oldSchema; + context.newSchema = newSchema; + this.errorHandler = errorHandler; + } + + public void validate() throws IncompatibleSchemaException { + context.prefixSegments.clear(); + verifySchemasCompatible(context.oldSchema, context.newSchema); + } + + private void verifySchemasCompatible(Schema oldSchema, Schema newSchema) throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + + if (oldSchema.getClass() == EmptySchema.class) { + // always compatible + return; + } + + // special case: "anyOf" or "oneOf" can be replaced by one of its subschemas + if (oldSchema.getClass() == CombinedSchema.class && newSchema.getClass() != CombinedSchema.class) { + verifyCombinedReplacedBySubschema((CombinedSchema) oldSchema, newSchema); + return; + } + + if (newSchema.getClass() != oldSchema.getClass()) { + errorHandler.handleSchemaTypeDiffers(context); + return; + } + + if (newSchema.getClass() == ObjectSchema.class) { + verifySchemasCompatible((ObjectSchema) oldSchema, (ObjectSchema) newSchema); + } + else if (newSchema.getClass() == StringSchema.class) { + verifySchemasCompatible((StringSchema) oldSchema, (StringSchema) newSchema); + } + else if (newSchema.getClass() == EnumSchema.class) { + verifySchemasCompatible((EnumSchema) oldSchema, (EnumSchema) newSchema); + } + else if (newSchema.getClass() == ArraySchema.class) { + verifySchemasCompatible((ArraySchema) oldSchema, (ArraySchema) newSchema); + } + else if (newSchema.getClass() == NumberSchema.class) { + verifySchemasCompatible((NumberSchema) oldSchema, (NumberSchema) newSchema); + } + else if (newSchema.getClass() == CombinedSchema.class) { + verifySchemasCompatible((CombinedSchema) oldSchema, (CombinedSchema) newSchema); + } + else if (newSchema.getClass() == ConditionalSchema.class) { + verifySchemasCompatible((ConditionalSchema) oldSchema, (ConditionalSchema) newSchema); + } + else if (newSchema.getClass() == NotSchema.class) { + verifySchemasCompatible((NotSchema) oldSchema, (NotSchema) newSchema); + } + else if (newSchema.getClass() == ConstSchema.class) { + verifySchemasCompatible((ConstSchema) oldSchema, (ConstSchema) newSchema); + } + else if (newSchema.getClass() == ReferenceSchema.class) { + verifySchemasCompatible((ReferenceSchema) oldSchema, (ReferenceSchema) newSchema); + } + else if (newSchema.getClass() == NullSchema.class || newSchema.getClass() == BooleanSchema.class) { + // noinspection UnnecessaryReturnStatement + return; + } + else { + // unsupported (top) schema type + throw new IncompatibleSchemaException( + "Unsupported schema type used: " + oldSchema.getClass().getSimpleName()); + } + } + + private void verifySchemasCompatible(ObjectSchema oldSchema, ObjectSchema newSchema) + throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + + if (!oldSchema.permitsAdditionalProperties() && newSchema.permitsAdditionalProperties()) { + errorHandler.handleAdditionalPropertiesIntroduced(context); + return; + } + + // required properties must still be required + for (String property : oldSchema.getRequiredProperties()) { + if (!newSchema.getRequiredProperties().contains(property)) { + errorHandler.handlePropertyNoLongerRequired(context, property); + return; + } + } + + // min properties cannot be reduced (consumer could rely on having at least N + // properties) + if (oldSchema.getMinProperties() != null && (newSchema.getMinProperties() == null + || newSchema.getMinProperties() < oldSchema.getMinProperties())) { + errorHandler.handleMinPropertiesReduced(context); + return; + } + + // Previously strongly typed (optional) properties must still be available with + // same format + oldPropsLoop: for (String property : oldSchema.getPropertySchemas().keySet()) { + Schema oldPropSchema = oldSchema.getPropertySchemas().get(property); + + // simple case: still exists directly + Schema newPropSchema = newSchema.getPropertySchemas().get(property); + if (newPropSchema != null) { + pushPrefix(".", property); + verifySchemasCompatible(oldPropSchema, newPropSchema); + popPrefix(); + continue; + } + + // could now be inside the dependencies (verify ALL occurrences to be compatible + // to previous schema) + boolean found = false; + for (Schema newDepSchema : newSchema.getSchemaDependencies().values()) { + if (!(newDepSchema instanceof ObjectSchema)) { + errorHandler.handleInvalidDependenciesConfiguration(context); + return; + } + newPropSchema = ((ObjectSchema) newDepSchema).getPropertySchemas().get(property); + if (newPropSchema != null) { + pushPrefix(".", property); + verifySchemasCompatible(oldPropSchema, newPropSchema); + popPrefix(); + found = true; + } + } + if (found) { + continue; + } + + // could now be covered by a Pattern + Map newPatternSchemas = getPatternProperties(newSchema); + for (Map.Entry entry : newPatternSchemas.entrySet()) { + if (entry.getKey().patternMatchingFailure(property).isEmpty()) { + pushPrefix(".", property); + verifySchemasCompatible(oldPropSchema, entry.getValue()); + popPrefix(); + // JSON Schema logic: First matching pattern wins... + continue oldPropsLoop; + } + } + + // if no additionalProperties allowed in new schema, we are fine, because will not occur in different format + if (!newSchema.permitsAdditionalProperties() && !oldSchema.getRequiredProperties().contains(property)) { + continue; + } + + // OK, defined nowhere, so we can't be sure that it will still be generated in same form + errorHandler.handleDefinedPropertyNoLongerDefined(context, property); + } + + // strong assumption: If pattern properties were used, all patterns must still + // exist, and still be compatible + Map oldPatternSchemas = getPatternProperties(oldSchema); + Map newPatternSchemas = getPatternProperties(newSchema); + + for (Map.Entry pattern : oldPatternSchemas.entrySet()) { + // lib does not implement equals() for Regexp class, so... + Optional newKey = newPatternSchemas.keySet().stream() + .filter(r -> r.toString().equals(pattern.getKey().toString())).findAny(); + if (newKey.isEmpty()) { + errorHandler.handleRegexpPatternRemoved(context, pattern.getKey().toString()); + return; + } + + // directly compare, while we're here + pushPrefix("", "(" + pattern + ")"); + verifySchemasCompatible(pattern.getValue(), newPatternSchemas.get(newKey.get())); + popPrefix(); + } + } + + private void verifySchemasCompatible(ArraySchema oldSchema, ArraySchema newSchema) + throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + + if (oldSchema.needsUniqueItems() && !newSchema.needsUniqueItems()) { + errorHandler.handleArrayUniqueItemsRemoved(context); + } + + if (oldSchema.getMinItems() != null && minAllowedItems(newSchema) < oldSchema.getMinItems()) { + errorHandler.handleArrayAllowsLessItems(context); + } + + if (oldSchema.getMaxItems() != null && maxAllowedItems(newSchema) > oldSchema.getMaxItems()) { + errorHandler.handleArrayAllowsMoreItems(context); + } + + if (oldSchema.getAllItemSchema() != null) { + if (newSchema.getAllItemSchema() == null) { + if (newSchema.getContainedItemSchema() != null) { + errorHandler.handleArrayAllSchemaToContainsSchema(context); + } + else if (newSchema.getItemSchemas() == null) { + errorHandler.handleArrayItemSchemaRemoved(context); + } + else { + for (int i = 0; i < newSchema.getItemSchemas().size(); i++) { + pushPrefix("[" + i + "]"); + verifySchemasCompatible(oldSchema.getAllItemSchema(), newSchema.getItemSchemas().get(i)); + popPrefix(); + } + } + } + else { + pushPrefix("[all]"); + verifySchemasCompatible(oldSchema.getAllItemSchema(), newSchema.getAllItemSchema()); + popPrefix(); + } + } + else if (oldSchema.getContainedItemSchema() != null) { + if (newSchema.getContainedItemSchema() != null) { + pushPrefix("[contains]"); + verifySchemasCompatible(oldSchema.getContainedItemSchema(), newSchema.getContainedItemSchema()); + popPrefix(); + } + else if (newSchema.getAllItemSchema() != null) { + pushPrefix("[contains/all]"); + verifySchemasCompatible(oldSchema.getContainedItemSchema(), newSchema.getAllItemSchema()); + popPrefix(); + } + else if (newSchema.getItemSchemas() != null) { + // fine if at least ONE item matches! + boolean match = false; + for (int i = 0; i < newSchema.getItemSchemas().size(); i++) { + pushPrefix("[" + i + "]"); + try { + verifySchemasCompatible(oldSchema.getContainedItemSchema(), newSchema.getItemSchemas().get(i)); + match = true; + break; + } + catch (IncompatibleSchemaException ex) { + // ignore here - no match + } + finally { + popPrefix(); + } + } + if (!match) { + errorHandler.handleArrayContainsSchemaNotFound(context); + } + } + } + else if (oldSchema.getItemSchemas() != null) { + if (newSchema.getItemSchemas() != null) { + // must either contain more elements (if oldSchema allows this), or exactly the same + if (newSchema.getItemSchemas().size() < oldSchema.getItemSchemas().size()) { + errorHandler.handleArrayLessItemSchemas(context); + } + else if (newSchema.getItemSchemas().size() > oldSchema.getItemSchemas().size() + && (!oldSchema.permitsAdditionalItems() || (oldSchema.getMaxItems() != null + && oldSchema.getMaxItems() < newSchema.getItemSchemas().size()))) { + errorHandler.handleArrayMoreItemSchemasThanAllowed(context); + } + else { + for (int i = 0; i < oldSchema.getItemSchemas().size(); i++) { + pushPrefix("[" + i + "]"); + verifySchemasCompatible(oldSchema.getItemSchemas().get(i), newSchema.getItemSchemas().get(i)); + popPrefix(); + } + } + } + else if (newSchema.getAllItemSchema() != null) { + // first of all, ensure array size is compatible + int oldSize = oldSchema.getItemSchemas().size(); + if (newSchema.getMinItems() == null || newSchema.getMinItems() < oldSize) { + errorHandler.handleArrayLessItemsThanSchemas(context); + } + else if (!oldSchema.permitsAdditionalItems() + && (newSchema.getMaxItems() == null || newSchema.getMaxItems() > oldSize)) { + errorHandler.handleArrayMoreItemsThanSchemas(context); + } + else { + // must match ALL previous items + for (int i = 0; i < oldSchema.getItemSchemas().size(); i++) { + pushPrefix("[" + i + "]"); + verifySchemasCompatible(oldSchema.getItemSchemas().get(i), newSchema.getAllItemSchema()); + popPrefix(); + } + } + } + else { + errorHandler.handleArrayItemSchemasRemoved(context); + } + } + } + + private void verifySchemasCompatible(CombinedSchema oldSchema, CombinedSchema newSchema) + throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + if (oldSchema.getCriterion() != newSchema.getCriterion()) { + errorHandler.handleCombineOperatorChanged(context); + return; + } + + // every new subschema must be compatible to a previous old schema + int i = 0; + for (Schema schema : newSchema.getSubschemas()) { + boolean match = false; + for (Schema os : oldSchema.getSubschemas()) { + pushPrefix("(" + newSchema.getCriterion() + ")[" + i + "]"); + try { + verifySchemasCompatible(os, schema); + match = true; + break; + } + catch (IncompatibleSchemaException ex) { + // ignore; try next + } + finally { + popPrefix(); + } + } + if (!match) { + errorHandler.handleCombineSubschemaNoMatch(context); + } + i++; + } + } + + private void verifySchemasCompatible(StringSchema oldSchema, StringSchema newSchema) + throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + + if (oldSchema.getMinLength() != null + && (newSchema.getMinLength() == null || newSchema.getMinLength() < oldSchema.getMinLength())) { + errorHandler.handleShorterStringsAllowed(context); + } + if (oldSchema.getMaxLength() != null + && (newSchema.getMaxLength() == null || newSchema.getMaxLength() > oldSchema.getMaxLength())) { + errorHandler.handleLongerStringsAllowed(context); + } + + if (oldSchema.getPattern() != null && (newSchema.getPattern() == null + || !newSchema.getPattern().toString().equals(oldSchema.getPattern().toString()))) { + errorHandler.handleStringPatternChanged(context); + } + + if (oldSchema.getFormatValidator() != null && (newSchema.getFormatValidator() == null + || !oldSchema.getFormatValidator().formatName().equals(newSchema.getFormatValidator().formatName()))) { + errorHandler.handleStringFormatChanged(context); + } + } + + private static final double DELTA = 0.0000001; + + private void verifySchemasCompatible(NumberSchema oldSchema, NumberSchema newSchema) + throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + if (oldSchema.getMultipleOf() != null) { + if (newSchema.getMultipleOf() == null) { + errorHandler.handleMultipleOfConstraintRemoved(context); + } + else { + Number m1 = oldSchema.getMultipleOf(); + Number m2 = newSchema.getMultipleOf(); + + // avoid dumb DIV/0 + if (Math.abs(m1.doubleValue()) > DELTA) { + double testVal = m2.doubleValue() / m1.doubleValue(); + testVal -= Math.round(testVal); + if (testVal > DELTA) { + errorHandler.handleMultipleOfConstraintChanged(context); + } + } + } + } + + if (oldSchema.requiresInteger() && !newSchema.requiresInteger()) { + errorHandler.handleIntegerChangedToNumber(context); + } + + if (allowsForLowerThan(oldSchema, newSchema)) { + errorHandler.handleNumberAllowsLowerValues(context); + } + if (allowsForGreaterThan(oldSchema, newSchema)) { + errorHandler.handleNumberAllowsGreaterValues(context); + } + } + + private void verifySchemasCompatible(NotSchema oldSchema, NotSchema newSchema) throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + + // intentionally swapped parameters, because negated schema must get more liberal + // to have the effect of "stricter" for the not-schema. + pushPrefix("(not)"); + verifySchemasCompatible(newSchema.getMustNotMatch(), oldSchema.getMustNotMatch()); + popPrefix(); + } + + private void verifySchemasCompatible(ReferenceSchema oldSchema, ReferenceSchema newSchema) + throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + + if (oldSchema.getReferredSchema() != null) { + if (newSchema.getReferredSchema() == null) { + errorHandler.handleUnresolvedSchemaReference(context); + } + else { + pushPrefix(oldSchema.getReferenceValue()); + verifySchemasCompatible(oldSchema.getReferredSchema(), newSchema.getReferredSchema()); + popPrefix(); + } + } + } + + private void verifySchemasCompatible(ConstSchema oldSchema, ConstSchema newSchema) + throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + if (oldSchema.getPermittedValue() == null ? newSchema.getPermittedValue() != null + : !Objects.deepEquals(oldSchema.getPermittedValue(), newSchema.getPermittedValue())) { + errorHandler.handleConstantValueChanged(context); + } + } + + private void verifySchemasCompatible(ConditionalSchema oldSchema, ConditionalSchema newSchema) + throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + if (oldSchema.getIfSchema().isPresent() != newSchema.getIfSchema().isPresent() + || oldSchema.getThenSchema().isPresent() != newSchema.getThenSchema().isPresent() + || oldSchema.getElseSchema().isPresent() != newSchema.getElseSchema().isPresent()) { + errorHandler.handleConditionalSchemaDiffers(context); + } + + if (oldSchema.getIfSchema().isPresent()) { + verifySchemasCompatible(oldSchema.getIfSchema().get(), newSchema.getIfSchema().orElseThrow()); + } + if (oldSchema.getThenSchema().isPresent()) { + verifySchemasCompatible(oldSchema.getThenSchema().get(), newSchema.getThenSchema().orElseThrow()); + } + if (oldSchema.getElseSchema().isPresent()) { + verifySchemasCompatible(oldSchema.getElseSchema().get(), newSchema.getElseSchema().orElseThrow()); + } + } + + private void verifySchemasCompatible(EnumSchema oldSchema, EnumSchema newSchema) + throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + + // no new values must have been added + for (Object o : newSchema.getPossibleValues()) { + if (!oldSchema.getPossibleValues().contains(o)) { + errorHandler.handleEnumValueAdded(context, o.toString()); + return; + } + } + } + + private void verifyCombinedReplacedBySubschema(CombinedSchema oldSchema, Schema newSchema) + throws IncompatibleSchemaException { + setCurrentNodes(oldSchema, newSchema); + + String crit = oldSchema.getCriterion().toString(); + if (!"oneOf".equals(crit) && !"anyOf".equals(crit)) { + errorHandler.handleCombinedSchemaReplacedByIncompatibleSchema(context); + // if that is fine for error handler, no need to check further + return; + } + + for (Schema os : oldSchema.getSubschemas()) { + try { + verifySchemasCompatible(os, newSchema); + return; + } + catch (IncompatibleSchemaException ex) { + // next + } + } + + errorHandler.handleCombinedSchemaReplacedByIncompatibleSchema(context); + } + + private static boolean allowsForLowerThan(NumberSchema oldSchema, NumberSchema newSchema) { + Number minOld = oldSchema.getMinimum(); + Number minOldExcl = oldSchema.isExclusiveMinimum() ? minOld : oldSchema.getExclusiveMinimumLimit(); + Number minNew = newSchema.getMinimum(); + Number minNewExcl = newSchema.isExclusiveMinimum() ? minNew : newSchema.getExclusiveMinimumLimit(); + + if (minOld != null) { + if (minNewExcl != null) { + if (minNewExcl.doubleValue() >= minOld.doubleValue()) { + return false; + } + return !newSchema.requiresInteger() || minNewExcl.intValue() < minOld.intValue() - 1; + } + else { + return minNew == null || minNew.doubleValue() < minOld.doubleValue() - DELTA; + } + } + + if (minOldExcl != null) { + if (minNew != null) { + if (minNew.doubleValue() < minOldExcl.doubleValue()) { + return true; + } + if (minNew.doubleValue() > minOldExcl.doubleValue() + 1) { + return false; + } + if (!newSchema.requiresInteger()) { + return true; + } + + return minNew.intValue() != minOldExcl.intValue() + 1; + } + else { + return minNewExcl == null || minNewExcl.doubleValue() < minOldExcl.doubleValue() - DELTA; + } + } + + // old schema had no min limit + return false; + } + + private static boolean allowsForGreaterThan(NumberSchema oldSchema, NumberSchema newSchema) { + Number maxOld = oldSchema.getMaximum(); + Number maxOldExcl = oldSchema.isExclusiveMaximum() ? maxOld : oldSchema.getExclusiveMaximumLimit(); + Number maxNew = newSchema.getMaximum(); + Number maxNewExcl = newSchema.isExclusiveMaximum() ? maxNew : newSchema.getExclusiveMaximumLimit(); + + if (maxOld != null) { + if (maxNewExcl != null) { + if (maxNewExcl.doubleValue() <= maxOld.doubleValue()) { + return false; + } + return !newSchema.requiresInteger() || maxNewExcl.intValue() > maxOld.intValue() + 1; + } + else { + return maxNew == null || maxNew.doubleValue() > maxOld.doubleValue() + DELTA; + } + } + + if (maxOldExcl != null) { + if (maxNew != null) { + if (maxNew.doubleValue() > maxOldExcl.doubleValue()) { + return true; + } + if (maxNew.doubleValue() < maxOldExcl.doubleValue() - 1) { + return false; + } + if (!newSchema.requiresInteger()) { + return true; + } + + return maxNew.intValue() != maxOldExcl.intValue() - 1; + } + else { + return maxNewExcl == null || maxNewExcl.doubleValue() > maxOldExcl.doubleValue() + DELTA; + } + } + + // old schema had no max limit + return false; + } + + private static int minAllowedItems(ArraySchema schema) { + if (schema.getMinItems() != null) { + return schema.getMinItems(); + } + if (schema.getItemSchemas() != null) { + return schema.getItemSchemas().size(); + } + return 0; + } + + private static int maxAllowedItems(ArraySchema schema) { + if (schema.getMaxItems() != null) { + return schema.getMaxItems(); + } + if (schema.getItemSchemas() != null && !schema.permitsAdditionalItems()) { + return schema.getItemSchemas().size(); + } + return Integer.MAX_VALUE; + } + + @SuppressWarnings("unchecked") + private static Map getPatternProperties(ObjectSchema schema) { + try { + Field field = ObjectSchema.class.getDeclaredField("patternProperties"); + field.setAccessible(true); + Map result = (Map) field.get(schema); + return result == null ? Collections.emptyMap() : result; + } + catch (SecurityException | NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private void pushPrefix(String segment) { + pushPrefix("", segment); + } + + private void pushPrefix(String separator, String segment) { + context.prefixSegments.push(context.prefixSegments.isEmpty() ? segment : separator + segment); + } + + private void popPrefix() { + context.prefixSegments.pop(); + } + + private void setCurrentNodes(Schema currentOldNode, Schema currentNewNode) { + context.currentOldNode = currentOldNode; + context.currentNewNode = currentNewNode; + } + + private static class ValidationContextImpl implements SchemaCompatibilityValidationContext { + + private Schema oldSchema; + + private Schema newSchema; + + private final Stack prefixSegments = new Stack<>(); + + private Schema currentOldNode; + + private Schema currentNewNode; + + @Override + public String getCurrentPrefix() { + return prefixSegments.isEmpty() ? null : String.join("", prefixSegments); + } + + @Override + public Schema getOldSchema() { + return oldSchema; + } + + @Override + public Schema getNewSchema() { + return newSchema; + } + + @Override + public Schema getCurrentNodeInOldSchema() { + return currentOldNode; + } + + @Override + public Schema getCurrentNodeInNewSchema() { + return currentNewNode; + } + } +} diff --git a/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaUtil.java b/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaUtil.java index 85493d5a..38a80c20 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaUtil.java +++ b/src/main/java/com/hermesworld/ais/galapagos/schemas/SchemaUtil.java @@ -1,596 +1,37 @@ package com.hermesworld.ais.galapagos.schemas; -import org.everit.json.schema.*; -import org.everit.json.schema.regexp.Regexp; +import org.everit.json.schema.Schema; -import java.lang.reflect.Field; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; import java.util.Optional; +/** + * Utility class to compare two schemas for Consumer compatibility. Guideline is that a potential consumer which + * currently consumes data in oldSchema format should be able to also handle data in newSchema + * format without any adjustments.
+ */ public final class SchemaUtil { - // Known limitations: - - // - Regex Pattern compatibility is not checked - every difference is treated as incompatible. - // Theoretically, there could be stricter patterns which include the previous patterns. - private SchemaUtil() { } + /** + * Checks if two JSON schemas are equal. Equality is defined by the org.everit JSON schema library. + * + * @param schema1 Schema to compare. + * @param schema2 Schema to compare. + * @return true if both schemas are equal according to the definition of the library, + * false otherwise. + */ public static boolean areEqual(Schema schema1, Schema schema2) { return schema1.getClass() == schema2.getClass() && schema1.equals(schema2); } - public static void verifyCompatibleTo(Schema oldSchema, Schema newSchema) throws IncompatibleSchemaException { - verifyCompatibleTo(oldSchema, newSchema, null); - } - - private static void verifyCompatibleTo(Schema oldSchema, Schema newSchema, String prefix) - throws IncompatibleSchemaException { - if (oldSchema.getClass() == EmptySchema.class) { - // always compatible - return; - } - - // special case: "anyOf" or "oneOf" can be replaced by one of its subschemas - if (oldSchema.getClass() == CombinedSchema.class && newSchema.getClass() != CombinedSchema.class) { - verifyCombinedReplacedBySubschema((CombinedSchema) oldSchema, newSchema, prefix); - return; - } - - if (newSchema.getClass() != oldSchema.getClass()) { - throw new IncompatibleSchemaException("Schema type differs at " + propLocationName(prefix) + " (new: " - + newSchema.getClass().getSimpleName() + ", old: " + oldSchema.getClass().getSimpleName() + ")"); - } - - if (newSchema.getClass() == ObjectSchema.class) { - verifyCompatibleTo((ObjectSchema) oldSchema, (ObjectSchema) newSchema, prefix); - } - else if (newSchema.getClass() == StringSchema.class) { - verifyCompatibleTo((StringSchema) oldSchema, (StringSchema) newSchema, prefix); - } - else if (newSchema.getClass() == EnumSchema.class) { - verifyCompatibleTo((EnumSchema) oldSchema, (EnumSchema) newSchema, prefix); - } - else if (newSchema.getClass() == ArraySchema.class) { - verifyCompatibleTo((ArraySchema) oldSchema, (ArraySchema) newSchema, prefix); - } - else if (newSchema.getClass() == NumberSchema.class) { - verifyCompatibleTo((NumberSchema) oldSchema, (NumberSchema) newSchema, prefix); - } - else if (newSchema.getClass() == CombinedSchema.class) { - verifyCompatibleTo((CombinedSchema) oldSchema, (CombinedSchema) newSchema, prefix); - } - else if (newSchema.getClass() == ConditionalSchema.class) { - verifyCompatibleTo((ConditionalSchema) oldSchema, (ConditionalSchema) newSchema, prefix); - } - else if (newSchema.getClass() == NotSchema.class) { - verifyCompatibleTo((NotSchema) oldSchema, (NotSchema) newSchema, prefix); - } - else if (newSchema.getClass() == ConstSchema.class) { - verifyCompatibleTo((ConstSchema) oldSchema, (ConstSchema) newSchema, prefix); - } - else if (newSchema.getClass() == ReferenceSchema.class) { - verifyCompatibleTo((ReferenceSchema) oldSchema, (ReferenceSchema) newSchema, prefix); - } - else if (newSchema.getClass() == NullSchema.class || newSchema.getClass() == BooleanSchema.class) { - // noinspection UnnecessaryReturnStatement - return; - } - else { - // unsupported (top) schema type - throw new IncompatibleSchemaException( - "Unsupported schema type used: " + oldSchema.getClass().getSimpleName()); - } - } - - private static void verifyCompatibleTo(ObjectSchema oldSchema, ObjectSchema newSchema, String prefix) - throws IncompatibleSchemaException { - if (!oldSchema.permitsAdditionalProperties() && newSchema.permitsAdditionalProperties()) { - throw new IncompatibleSchemaException("additionalProperties must not be introduced " - + "in a newer schema version (old consumers could rely on no additional properties being present)"); - } - - // required properties must still be required - for (String property : oldSchema.getRequiredProperties()) { - if (!newSchema.getRequiredProperties().contains(property)) { - throw new IncompatibleSchemaException("Property " + fullPropName(prefix, property) - + " is no longer required, but old consumers could rely on it."); - } - } - - // min properties cannot be reduced (consumer could rely on having at least N - // properties) - if (oldSchema.getMinProperties() != null && (newSchema.getMinProperties() == null - || newSchema.getMinProperties() < oldSchema.getMinProperties())) { - throw new IncompatibleSchemaException("Minimum number of properties cannot be reduced at " - + propLocationName(prefix) + ". Old consumers could rely on having at least N properties."); - } - - // Previously strongly typed (optional) properties must still be available with - // same format - oldPropsLoop: for (String property : oldSchema.getPropertySchemas().keySet()) { - Schema oldPropSchema = oldSchema.getPropertySchemas().get(property); - - // simple case: still exists directly - Schema newPropSchema = newSchema.getPropertySchemas().get(property); - if (newPropSchema != null) { - verifyCompatibleTo(oldPropSchema, newPropSchema, fullPropName(prefix, property)); - continue; - } - - // could now be inside the dependencies (verify ALL occurrences to be compatible - // to previous schema) - boolean found = false; - for (Schema newDepSchema : newSchema.getSchemaDependencies().values()) { - if (!(newDepSchema instanceof ObjectSchema)) { - throw new IncompatibleSchemaException( - "Invalid dependencies configuration found at " + propLocationName(prefix)); - } - newPropSchema = ((ObjectSchema) newDepSchema).getPropertySchemas().get(property); - if (newPropSchema != null) { - verifyCompatibleTo(oldPropSchema, newPropSchema, fullPropName(prefix, property)); - found = true; - } - } - if (found) { - continue; - } - - // could now be covered by a Pattern - Map newPatternSchemas = getPatternProperties(newSchema); - for (Map.Entry entry : newPatternSchemas.entrySet()) { - if (entry.getKey().patternMatchingFailure(property).isEmpty()) { - verifyCompatibleTo(oldPropSchema, entry.getValue(), fullPropName(prefix, property)); - // JSON Schema logic: First matching pattern wins... - continue oldPropsLoop; - } - } - - // if no additionalProperties allowed in new schema, we are fine, because will not occur in different format - if (!newSchema.permitsAdditionalProperties() && !oldSchema.getRequiredProperties().contains(property)) { - continue; - } - - // OK, defined nowhere, so we can't be sure that it will still be generated in - // same form - throw new IncompatibleSchemaException("Property " + fullPropName(prefix, property) - + " is no longer defined in schema, but had a strong definition before. " - + "Field format could have changed, and old consumers perhaps rely on previous format"); - } - - // strong assumption: If pattern properties were used, all patterns must still - // exist, and still be compatible - Map oldPatternSchemas = getPatternProperties(oldSchema); - Map newPatternSchemas = getPatternProperties(newSchema); - - for (Map.Entry pattern : oldPatternSchemas.entrySet()) { - // lib does not implement equals() for Regexp class, so... - Optional newKey = newPatternSchemas.keySet().stream() - .filter(r -> r.toString().equals(pattern.getKey().toString())).findAny(); - if (newKey.isEmpty()) { - throw new IncompatibleSchemaException("Pattern " + pattern.getKey().toString() - + " is no longer defined for new schema. Consumers could rely on the schema specification for matching properties."); - } - - // directly compare, while we're here - verifyCompatibleTo(pattern.getValue(), newPatternSchemas.get(newKey.get()), - fullPropName(prefix, "(" + pattern + ")")); - } - } - - private static void verifyCompatibleTo(ArraySchema oldSchema, ArraySchema newSchema, String prefix) - throws IncompatibleSchemaException { - if (oldSchema.needsUniqueItems() && !newSchema.needsUniqueItems()) { - throw new IncompatibleSchemaException("Old schema guaranteed unique items for array at " - + propLocationName(prefix) + " while new schema removes this guarantee"); - } - - if (oldSchema.getMinItems() != null && minAllowedItems(newSchema) < oldSchema.getMinItems()) { - throw new IncompatibleSchemaException("Old schema had a minimum of " + oldSchema.getMinItems() - + " items for array at " + propLocationName(prefix) - + ", while new schema does not guarantee this number of items in array"); - } - - if (oldSchema.getMaxItems() != null && maxAllowedItems(newSchema) > oldSchema.getMaxItems()) { - throw new IncompatibleSchemaException("Old schema had a maximum of " + oldSchema.getMaxItems() - + " items for array at " + propLocationName(prefix) + ", while new schema allows more items."); - } - - if (oldSchema.getAllItemSchema() != null) { - if (newSchema.getAllItemSchema() == null) { - if (newSchema.getContainedItemSchema() != null) { - throw new IncompatibleSchemaException("Array at " + propLocationName(prefix) - + " cannot be reduced from a schema for ALL items to a schema for at least ONE (contains) item"); - } - if (newSchema.getItemSchemas() == null) { - throw new IncompatibleSchemaException("Array at " + propLocationName(prefix) - + " had a strong typing for items before which cannot be removed (old consumers could rely on that format)"); - } - for (int i = 0; i < newSchema.getItemSchemas().size(); i++) { - verifyCompatibleTo(oldSchema.getAllItemSchema(), newSchema.getItemSchemas().get(i), - prefix + "[" + i + "]"); - } - } - else { - verifyCompatibleTo(oldSchema.getAllItemSchema(), newSchema.getAllItemSchema(), prefix + "[all]"); - } - } - else if (oldSchema.getContainedItemSchema() != null) { - if (newSchema.getContainedItemSchema() != null) { - verifyCompatibleTo(oldSchema.getContainedItemSchema(), newSchema.getContainedItemSchema(), - prefix + "[contains]"); - } - else if (newSchema.getAllItemSchema() != null) { - verifyCompatibleTo(oldSchema.getContainedItemSchema(), newSchema.getAllItemSchema(), - prefix + "[contains/all]"); - } - else if (newSchema.getItemSchemas() != null) { - // fine if at least ONE item matches! - boolean match = false; - for (int i = 0; i < newSchema.getItemSchemas().size(); i++) { - try { - verifyCompatibleTo(oldSchema.getContainedItemSchema(), newSchema.getItemSchemas().get(i), - prefix + "[" + i + "]"); - match = true; - break; - } - catch (IncompatibleSchemaException ex) { - // ignore here - no match - } - } - if (!match) { - throw new IncompatibleSchemaException("Array at " + propLocationName(prefix) - + " had a contains definition before which can not be found for any item of that array in the new schema"); - } - } - } - else if (oldSchema.getItemSchemas() != null) { - if (newSchema.getItemSchemas() != null) { - // must either contain more elements (if oldSchema allows this), or exactly the - // same - if (newSchema.getItemSchemas().size() < oldSchema.getItemSchemas().size()) { - throw new IncompatibleSchemaException("Array at " + propLocationName(prefix) - + " had a strong typing for " + oldSchema.getItemSchemas().size() - + " elements, but new schema only defines " + newSchema.getItemSchemas().size() + " here"); - } - if (newSchema.getItemSchemas().size() > oldSchema.getItemSchemas().size() - && (!oldSchema.permitsAdditionalItems() || (oldSchema.getMaxItems() != null - && oldSchema.getMaxItems() < newSchema.getItemSchemas().size()))) { - throw new IncompatibleSchemaException("New schema defines more items in array at " - + propLocationName(prefix) + " than were previously allowed here."); - } - for (int i = 0; i < oldSchema.getItemSchemas().size(); i++) { - verifyCompatibleTo(oldSchema.getItemSchemas().get(i), newSchema.getItemSchemas().get(i), - prefix + "[" + i + "]"); - } - } - else if (newSchema.getAllItemSchema() != null) { - // first of all, ensure array size is compatible - int oldSize = oldSchema.getItemSchemas().size(); - if (newSchema.getMinItems() == null || newSchema.getMinItems() < oldSize) { - throw new IncompatibleSchemaException( - "Old schema defined " + oldSize + " items for array at " + propLocationName(prefix) - + ", but new schema does not guarantee this number of items in the array"); - } - if (!oldSchema.permitsAdditionalItems() - && (newSchema.getMaxItems() == null || newSchema.getMaxItems() > oldSize)) { - throw new IncompatibleSchemaException("Old schema did not accept additional items for array at " - + propLocationName(prefix) + ", but new schema does not limit number of items in array"); - } - - // must match ALL previous items - for (int i = 0; i < oldSchema.getItemSchemas().size(); i++) { - verifyCompatibleTo(oldSchema.getItemSchemas().get(i), newSchema.getAllItemSchema(), - prefix + "[" + i + "]"); - } - } - else { - throw new IncompatibleSchemaException( - "Old schema made stronger guarantees for array at " + propLocationName(prefix) - + " than new schema does. Consumers could rely on previously guaranteed items"); - } - } - } - - private static final double DELTA = 0.0000001; - - private static void verifyCompatibleTo(NumberSchema oldSchema, NumberSchema newSchema, String prefix) - throws IncompatibleSchemaException { - if (oldSchema.getMultipleOf() != null) { - if (newSchema.getMultipleOf() == null) { - throw new IncompatibleSchemaException( - "multipleOf constraint must not be removed on number property " + propLocationName(prefix)); - } - Number m1 = oldSchema.getMultipleOf(); - Number m2 = newSchema.getMultipleOf(); - - // avoid dumb DIV/0 - if (Math.abs(m1.doubleValue()) > DELTA) { - double testVal = m2.doubleValue() / m1.doubleValue(); - testVal -= Math.round(testVal); - if (testVal > DELTA) { - throw new IncompatibleSchemaException("multipleOf constraint of property " - + propLocationName(prefix) + " changed in an incompatible way"); - } - } - } - - if (oldSchema.requiresInteger() && !newSchema.requiresInteger()) { - throw new IncompatibleSchemaException("Old schema only allowed integer values at property " - + propLocationName(prefix) - + " while new schema allows non-integer values. Consumers may not be able to handle non-integer values"); - } - - if (allowsForLowerThan(oldSchema, newSchema)) { - throw new IncompatibleSchemaException("New schema allows lower values for number property " - + propLocationName(prefix) + " than old schema. Consumers may not be prepared for this."); - } - if (allowsForGreaterThan(oldSchema, newSchema)) { - throw new IncompatibleSchemaException("New schema allows greater values for number property " - + propLocationName(prefix) + " than old schema. Consumers may not be prepared for this."); - } - } - - private static void verifyCompatibleTo(StringSchema oldSchema, StringSchema newSchema, String prefix) - throws IncompatibleSchemaException { - if (oldSchema.getMinLength() != null - && (newSchema.getMinLength() == null || newSchema.getMinLength() < oldSchema.getMinLength())) { - throw new IncompatibleSchemaException( - "New schema allows for shorter strings than old schema for string property " - + propLocationName(prefix)); - } - if (oldSchema.getMaxLength() != null - && (newSchema.getMaxLength() == null || newSchema.getMaxLength() > oldSchema.getMaxLength())) { - throw new IncompatibleSchemaException( - "New schema allows for longer strings than old schema for string property " - + propLocationName(prefix)); - } - - if (oldSchema.getPattern() != null && (newSchema.getPattern() == null - || !newSchema.getPattern().toString().equals(oldSchema.getPattern().toString()))) { - throw new IncompatibleSchemaException( - "New schema defines no or other pattern for string property " + propLocationName(prefix)); - } - - if (oldSchema.getFormatValidator() != null && (newSchema.getFormatValidator() == null - || !oldSchema.getFormatValidator().formatName().equals(newSchema.getFormatValidator().formatName()))) { - throw new IncompatibleSchemaException("format differs for string property " + propLocationName(prefix)); - } - } - - private static void verifyCompatibleTo(CombinedSchema oldSchema, CombinedSchema newSchema, String prefix) - throws IncompatibleSchemaException { - if (oldSchema.getCriterion() != newSchema.getCriterion()) { - throw new IncompatibleSchemaException( - "New schema defines differed combination operator at " + propLocationName(prefix)); - } - - // every new subschema must be compatible to a previous old schema - int i = 0; - for (Schema schema : newSchema.getSubschemas()) { - boolean match = false; - for (Schema os : oldSchema.getSubschemas()) { - try { - verifyCompatibleTo(os, schema, prefix + "(" + newSchema.getCriterion() + ")[" + i + "]"); - match = true; - break; - } - catch (IncompatibleSchemaException ex) { - // ignore; try next - } - } - if (!match) { - throw new IncompatibleSchemaException( - "New schema contains at least one incompatible subschema at property " - + propLocationName(prefix)); - } - i++; - } - } - - private static void verifyCompatibleTo(NotSchema oldSchema, NotSchema newSchema, String prefix) - throws IncompatibleSchemaException { - // intentionally swapped parameters, because negated schema must get more greedy - // to have the effect of "stricter" for the not-schema. - verifyCompatibleTo(newSchema.getMustNotMatch(), oldSchema.getMustNotMatch(), prefix + "(not)"); - } - - private static void verifyCompatibleTo(ReferenceSchema oldSchema, ReferenceSchema newSchema, String prefix) - throws IncompatibleSchemaException { - if (oldSchema.getReferredSchema() != null) { - if (newSchema.getReferredSchema() == null) { - throw new IncompatibleSchemaException( - "Reference at " + propLocationName(prefix) + " could not be resolved"); - } - - verifyCompatibleTo(oldSchema.getReferredSchema(), newSchema.getReferredSchema(), prefix + oldSchema); - } + static String propLocationName(SchemaCompatibilityValidationContext context) { + return Optional.ofNullable(context.getCurrentPrefix()).orElse("root"); } - private static void verifyCompatibleTo(ConstSchema oldSchema, ConstSchema newSchema, String prefix) - throws IncompatibleSchemaException { - if (oldSchema.getPermittedValue() == null ? newSchema.getPermittedValue() != null - : !Objects.deepEquals(oldSchema.getPermittedValue(), newSchema.getPermittedValue())) { - throw new IncompatibleSchemaException( - "Constant value for property " + propLocationName(prefix) + " has changed in new schema"); - } - } - - private static void verifyCompatibleTo(ConditionalSchema oldSchema, ConditionalSchema newSchema, String prefix) - throws IncompatibleSchemaException { - if (oldSchema.getIfSchema().isPresent() != newSchema.getIfSchema().isPresent() - || oldSchema.getThenSchema().isPresent() != newSchema.getThenSchema().isPresent() - || oldSchema.getElseSchema().isPresent() != newSchema.getElseSchema().isPresent()) { - throw new IncompatibleSchemaException("Conditional schema differs at " + propLocationName(prefix)); - } - - if (oldSchema.getIfSchema().isPresent()) { - verifyCompatibleTo(oldSchema.getIfSchema().get(), newSchema.getIfSchema().orElseThrow(), prefix); - } - if (oldSchema.getThenSchema().isPresent()) { - verifyCompatibleTo(oldSchema.getThenSchema().get(), newSchema.getThenSchema().orElseThrow(), prefix); - } - if (oldSchema.getElseSchema().isPresent()) { - verifyCompatibleTo(oldSchema.getElseSchema().get(), newSchema.getElseSchema().orElseThrow(), prefix); - } + static String fullPropName(SchemaCompatibilityValidationContext context, String property) { + return Optional.ofNullable(context.getCurrentPrefix()).map(s -> s + "." + property).orElse(property); } - private static void verifyCompatibleTo(EnumSchema oldSchema, EnumSchema newSchema, String prefix) - throws IncompatibleSchemaException { - // no new values must have been added - for (Object o : newSchema.getPossibleValues()) { - if (!oldSchema.getPossibleValues().contains(o)) { - throw new IncompatibleSchemaException("Schema introduces new enum value for property " - + propLocationName(prefix) + ": " + o + ". Consumers may not expect this value."); - } - } - } - - private static void verifyCombinedReplacedBySubschema(CombinedSchema oldSchema, Schema newSchema, String prefix) - throws IncompatibleSchemaException { - String crit = oldSchema.getCriterion().toString(); - if (!"oneOf".equals(crit) && !"anyOf".equals(crit)) { - throw new IncompatibleSchemaException("Previously combined schema at " + propLocationName(prefix) - + " replaced by " + newSchema.getClass().getSimpleName()); - } - - for (Schema os : oldSchema.getSubschemas()) { - try { - verifyCompatibleTo(os, newSchema, prefix); - return; - } - catch (IncompatibleSchemaException ex) { - // next - } - } - - throw new IncompatibleSchemaException( - "Previously combined schema at " + propLocationName(prefix) + " replaced by incompatible new schema"); - } - - private static String fullPropName(String prefix, String property) { - return prefix == null ? property : (prefix + "." + property); - } - - private static String propLocationName(String prefix) { - return prefix == null ? "root" : prefix; - } - - private static int minAllowedItems(ArraySchema schema) { - if (schema.getMinItems() != null) { - return schema.getMinItems(); - } - if (schema.getItemSchemas() != null) { - return schema.getItemSchemas().size(); - } - return 0; - } - - private static int maxAllowedItems(ArraySchema schema) { - if (schema.getMaxItems() != null) { - return schema.getMaxItems(); - } - if (schema.getItemSchemas() != null && !schema.permitsAdditionalItems()) { - return schema.getItemSchemas().size(); - } - return Integer.MAX_VALUE; - } - - private static boolean allowsForLowerThan(NumberSchema oldSchema, NumberSchema newSchema) { - Number minOld = oldSchema.getMinimum(); - Number minOldExcl = oldSchema.isExclusiveMinimum() ? minOld : oldSchema.getExclusiveMinimumLimit(); - Number minNew = newSchema.getMinimum(); - Number minNewExcl = newSchema.isExclusiveMinimum() ? minNew : newSchema.getExclusiveMinimumLimit(); - - if (minOld != null) { - if (minNewExcl != null) { - if (minNewExcl.doubleValue() >= minOld.doubleValue()) { - return false; - } - return !newSchema.requiresInteger() || minNewExcl.intValue() < minOld.intValue() - 1; - } - else { - return minNew == null || minNew.doubleValue() < minOld.doubleValue() - DELTA; - } - } - - if (minOldExcl != null) { - if (minNew != null) { - if (minNew.doubleValue() < minOldExcl.doubleValue()) { - return true; - } - if (minNew.doubleValue() > minOldExcl.doubleValue() + 1) { - return false; - } - if (!newSchema.requiresInteger()) { - return true; - } - - return minNew.intValue() != minOldExcl.intValue() + 1; - } - else { - return minNewExcl == null || minNewExcl.doubleValue() < minOldExcl.doubleValue() - DELTA; - } - } - - // old schema had no min limit - return false; - } - - private static boolean allowsForGreaterThan(NumberSchema oldSchema, NumberSchema newSchema) { - Number maxOld = oldSchema.getMaximum(); - Number maxOldExcl = oldSchema.isExclusiveMaximum() ? maxOld : oldSchema.getExclusiveMaximumLimit(); - Number maxNew = newSchema.getMaximum(); - Number maxNewExcl = newSchema.isExclusiveMaximum() ? maxNew : newSchema.getExclusiveMaximumLimit(); - - if (maxOld != null) { - if (maxNewExcl != null) { - if (maxNewExcl.doubleValue() <= maxOld.doubleValue()) { - return false; - } - return !newSchema.requiresInteger() || maxNewExcl.intValue() > maxOld.intValue() + 1; - } - else { - return maxNew == null || maxNew.doubleValue() > maxOld.doubleValue() + DELTA; - } - } - - if (maxOldExcl != null) { - if (maxNew != null) { - if (maxNew.doubleValue() > maxOldExcl.doubleValue()) { - return true; - } - if (maxNew.doubleValue() < maxOldExcl.doubleValue() - 1) { - return false; - } - if (!newSchema.requiresInteger()) { - return true; - } - - return maxNew.intValue() != maxOldExcl.intValue() - 1; - } - else { - return maxNewExcl == null || maxNewExcl.doubleValue() > maxOldExcl.doubleValue() + DELTA; - } - } - - // old schema had no max limit - return false; - } - - @SuppressWarnings("unchecked") - private static Map getPatternProperties(ObjectSchema schema) { - try { - Field field = ObjectSchema.class.getDeclaredField("patternProperties"); - field.setAccessible(true); - Map result = (Map) field.get(schema); - return result == null ? Collections.emptyMap() : result; - } - catch (SecurityException | NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } } diff --git a/src/main/java/com/hermesworld/ais/galapagos/security/AuditPrincipal.java b/src/main/java/com/hermesworld/ais/galapagos/security/AuditPrincipal.java index 30779fa5..fa351bd0 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/security/AuditPrincipal.java +++ b/src/main/java/com/hermesworld/ais/galapagos/security/AuditPrincipal.java @@ -14,4 +14,7 @@ public AuditPrincipal(String name, String fullName) { this.fullName = fullName; } + public String getFullName() { + return fullName; + } } diff --git a/src/main/java/com/hermesworld/ais/galapagos/security/CurrentUserService.java b/src/main/java/com/hermesworld/ais/galapagos/security/CurrentUserService.java index 28a2cc2d..c368975a 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/security/CurrentUserService.java +++ b/src/main/java/com/hermesworld/ais/galapagos/security/CurrentUserService.java @@ -4,12 +4,17 @@ public interface CurrentUserService { - public Optional getCurrentUserName(); + Optional getCurrentUserName(); - public Optional getCurrentPrincipal(); + Optional getCurrentPrincipal(); - public Optional getCurrentUserEmailAddress(); + Optional getCurrentUserEmailAddress(); - public boolean isAdmin(); + /** + * Checks from the Security Context, if the current user has the role of an Administrator + * + * @return true if the current user has the role of an Administrator, false otherwise + */ + boolean isAdmin(); } diff --git a/src/main/java/com/hermesworld/ais/galapagos/security/SecurityConfig.java b/src/main/java/com/hermesworld/ais/galapagos/security/SecurityConfig.java index e3e61aaa..3ea49d31 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/security/SecurityConfig.java +++ b/src/main/java/com/hermesworld/ais/galapagos/security/SecurityConfig.java @@ -1,110 +1,77 @@ package com.hermesworld.ais.galapagos.security; -import org.keycloak.adapters.springsecurity.KeycloakConfiguration; -import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider; -import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter; -import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticatedActionsFilter; -import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter; -import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter; -import org.keycloak.adapters.springsecurity.filter.KeycloakSecurityContextRequestFilter; -import org.keycloak.adapters.springsecurity.management.HttpSessionManager; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.web.servlet.FilterRegistrationBean; +import com.hermesworld.ais.galapagos.security.config.GalapagosSecurityProperties; import org.springframework.context.annotation.Bean; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.NonNull; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper; -import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; -import org.springframework.web.context.request.RequestContextListener; -@KeycloakConfiguration -@EnableGlobalMethodSecurity(securedEnabled = true) -public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter { +import java.util.Collection; +import java.util.Locale; +import java.util.stream.Collectors; - /** - * Registers the KeycloakAuthenticationProvider with the authentication manager. - */ - @Autowired - public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - KeycloakAuthenticationProvider provider = keycloakAuthenticationProvider(); - SimpleAuthorityMapper mapper = new SimpleAuthorityMapper(); - mapper.setConvertToUpperCase(true); - mapper.setConvertToLowerCase(false); - provider.setGrantedAuthoritiesMapper(mapper); - auth.authenticationProvider(provider); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - super.configure(http); - // TODO is CSRF necessary for a stateless backend without sessions? - http.csrf().disable(); - http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); - http.authorizeRequests().antMatchers("/api/**").hasRole("USER").anyRequest().permitAll(); - } - - // The following four beans shall avoid a double registration of the Keycloak filters. - // See https://www.keycloak.org/docs/latest/securing_apps/index.html#spring-boot-integration for details. - // (Google Keywords: keycloak "avoid double bean registration") +@Configuration +@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false) +public class SecurityConfig { @Bean - public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean( - KeycloakAuthenticationProcessingFilter filter) { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>( - filter); - registrationBean.setEnabled(false); - return registrationBean; + SecurityFilterChain filterChain(HttpSecurity http, GalapagosSecurityProperties config) throws Exception { + http.csrf(csrf -> csrf.disable()); + http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + http.authorizeHttpRequests(reg -> reg.requestMatchers("/api/**").hasRole("USER").anyRequest().permitAll()); + http.oauth2ResourceServer(conf -> conf.jwt(jwtCustomizer(config))); + return http.build(); } @Bean - public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean( - KeycloakPreAuthActionsFilter filter) { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(filter); - registrationBean.setEnabled(false); - return registrationBean; + public SessionAuthenticationStrategy sessionAuthenticationStrategy() { + return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl()); } - @Bean - public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean( - KeycloakAuthenticatedActionsFilter filter) { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>( - filter); - registrationBean.setEnabled(false); - return registrationBean; + private Customizer.JwtConfigurer> jwtCustomizer( + GalapagosSecurityProperties config) { + return jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter(config)); } - @Bean - public FilterRegistrationBean keycloakSecurityContextRequestFilterBean( - KeycloakSecurityContextRequestFilter filter) { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>( - filter); - registrationBean.setEnabled(false); - return registrationBean; - } + private JwtAuthenticationConverter jwtAuthenticationConverter(GalapagosSecurityProperties config) { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); - @Override - @Bean - @ConditionalOnMissingBean(HttpSessionManager.class) - protected HttpSessionManager httpSessionManager() { - // this method only exists for the @ConditionalOnMissingBean annotation, which is missing from the base class. - return super.httpSessionManager(); - } + converter.setPrincipalClaimName(config.getJwtUserNameClaim()); - @Bean - public RequestContextListener requestContextListener() { - return new RequestContextListener(); + JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter(); + authoritiesConverter.setAuthoritiesClaimName(config.getJwtRoleClaim()); + authoritiesConverter.setAuthorityPrefix("ROLE_"); + converter.setJwtGrantedAuthoritiesConverter(new UpperCaseJwtGrantedAuthoritiesConverter(authoritiesConverter)); + + return converter; } - @Override - protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { - // this makes sure that no JSESSIONID cookie is sent. Keycloak authentication of the token is done with each - // request, as - // it should be. - return new NullAuthenticatedSessionStrategy(); + private record UpperCaseJwtGrantedAuthoritiesConverter(JwtGrantedAuthoritiesConverter delegate) + implements Converter> { + + @Override + public Collection convert(@NonNull Jwt source) { + return mapToUpperCase(delegate.convert(source)); + } + + private Collection mapToUpperCase(Collection authorities) { + return authorities.stream().map(a -> new SimpleGrantedAuthority(a.getAuthority().toUpperCase(Locale.US))) + .collect(Collectors.toSet()); + } } -} \ No newline at end of file +} diff --git a/src/main/java/com/hermesworld/ais/galapagos/security/WebFrontendConfig.java b/src/main/java/com/hermesworld/ais/galapagos/security/WebFrontendConfig.java index 245541fb..d89ee060 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/security/WebFrontendConfig.java +++ b/src/main/java/com/hermesworld/ais/galapagos/security/WebFrontendConfig.java @@ -1,25 +1,31 @@ package com.hermesworld.ais.galapagos.security; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.context.request.RequestContextListener; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc public class WebFrontendConfig implements WebMvcConfigurer { - @Override - public void configurePathMatch(PathMatchConfigurer configurer) { - // TODO remove when upgraded to spring 5.3 or later - // noinspection deprecation - configurer.setUseSuffixPatternMatch(false); - } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/app/**", "/assets/**").addResourceLocations("classpath:/static/app/", - "classpath:/static/app/assets/"); + registry.addResourceHandler("/app/**").addResourceLocations("classpath:/static/app/"); + registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/static/app/assets/"); + } + + /** + * This ensures that the current HTTP Request is accessible via ThreadLocal accessory, e.g. in + * AuditEventRepositoryImpl. + * + * @return A new RequestContextListener bean. + */ + @Bean + public RequestContextListener requestContextListener() { + return new RequestContextListener(); } } diff --git a/src/main/java/com/hermesworld/ais/galapagos/security/config/GalapagosSecurityProperties.java b/src/main/java/com/hermesworld/ais/galapagos/security/config/GalapagosSecurityProperties.java new file mode 100644 index 00000000..c3bcf2b7 --- /dev/null +++ b/src/main/java/com/hermesworld/ais/galapagos/security/config/GalapagosSecurityProperties.java @@ -0,0 +1,56 @@ +package com.hermesworld.ais.galapagos.security.config; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; + +/** + * Configuration properties to fine-control how verified JWT tokens are mapped to username and roles. See + * application.properties for details. + */ +@Configuration +@ConfigurationProperties("galapagos.security") +@Validated +@Getter +@Setter +@Slf4j +public class GalapagosSecurityProperties implements Validator { + + // @NotEmpty + private String jwtRoleClaim; + + // @NotEmpty + private String jwtUserNameClaim; + + // @NotEmpty + private String jwtDisplayNameClaim; + + // @NotEmpty + private String jwtEmailClaim; + + @Override + public boolean supports(@NonNull Class clazz) { + return GalapagosSecurityProperties.class.isAssignableFrom(clazz); + } + + @Override + public void validate(@NonNull Object target, @NonNull Errors errors) { + // TODO remove, and replace by the NotEmpty annotations, once we do no longer want to provide this message. + if (target instanceof GalapagosSecurityProperties properties) { + if (ObjectUtils.isEmpty(properties.getJwtRoleClaim()) + || ObjectUtils.isEmpty(properties.getJwtUserNameClaim()) + || ObjectUtils.isEmpty(properties.getJwtDisplayNameClaim()) + || ObjectUtils.isEmpty(properties.getJwtEmailClaim())) { + errors.reject("MISSING_OAUTH2_PROPERTIES", + "Missing Galapagos OAuth2 properties. Maybe you did not perform required migration steps for Galapagos 2.8.0?\nPlease refer to https://github.com/HermesGermany/galapagos/blob/main/docs/Migration%20Guide%202.8.md"); + } + } + } +} diff --git a/src/main/java/com/hermesworld/ais/galapagos/security/impl/AuditEventRepositoryImpl.java b/src/main/java/com/hermesworld/ais/galapagos/security/impl/AuditEventRepositoryImpl.java index 90c14351..6422647c 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/security/impl/AuditEventRepositoryImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/security/impl/AuditEventRepositoryImpl.java @@ -1,15 +1,12 @@ package com.hermesworld.ais.galapagos.security.impl; -import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.module.SimpleModule; import com.hermesworld.ais.galapagos.util.JsonUtil; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; -import org.keycloak.KeycloakSecurityContext; import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.boot.actuate.audit.InMemoryAuditEventRepository; @@ -17,9 +14,6 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; -import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -44,7 +38,6 @@ public AuditEventRepositoryImpl() { this.objectMapper.getSerializationConfig().without(SerializationFeature.FAIL_ON_EMPTY_BEANS)); SimpleModule mapperModule = new SimpleModule(); - mapperModule.addSerializer(new KeycloakSecurityContextSerializer()); this.objectMapper.registerModule(mapperModule); } @@ -60,7 +53,7 @@ public void add(AuditEvent event) { } catch (IllegalStateException e) { // OK, no request - request = null; + log.trace("No current request found for Audit log, not including request information"); } Map logEntry = new HashMap<>(); @@ -87,19 +80,4 @@ private static Map toLogMap(HttpServletRequest request) { return result; } - private static class KeycloakSecurityContextSerializer extends JsonSerializer { - - @Override - public void serialize(KeycloakSecurityContext value, JsonGenerator gen, SerializerProvider serializers) - throws IOException { - gen.writeObject(Collections.emptyMap()); - } - - @Override - public Class handledType() { - return KeycloakSecurityContext.class; - } - - } - } diff --git a/src/main/java/com/hermesworld/ais/galapagos/security/impl/AuditEventsListener.java b/src/main/java/com/hermesworld/ais/galapagos/security/impl/AuditEventsListener.java index f49c1c56..65fab8a2 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/security/impl/AuditEventsListener.java +++ b/src/main/java/com/hermesworld/ais/galapagos/security/impl/AuditEventsListener.java @@ -7,7 +7,6 @@ import com.hermesworld.ais.galapagos.subscriptions.SubscriptionMetadata; import com.hermesworld.ais.galapagos.topics.TopicMetadata; import com.hermesworld.ais.galapagos.util.FutureUtil; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.stereotype.Component; @@ -27,7 +26,6 @@ public class AuditEventsListener implements TopicEventsListener, SubscriptionEve private static final String NAME = "name"; - @Autowired public AuditEventsListener(AuditEventRepository auditRepository) { this.auditRepository = auditRepository; } diff --git a/src/main/java/com/hermesworld/ais/galapagos/security/impl/DefaultCurrentUserService.java b/src/main/java/com/hermesworld/ais/galapagos/security/impl/DefaultCurrentUserService.java index 18cbee5e..04e6e2c3 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/security/impl/DefaultCurrentUserService.java +++ b/src/main/java/com/hermesworld/ais/galapagos/security/impl/DefaultCurrentUserService.java @@ -3,13 +3,11 @@ import com.hermesworld.ais.galapagos.events.EventContextSource; import com.hermesworld.ais.galapagos.security.AuditPrincipal; import com.hermesworld.ais.galapagos.security.CurrentUserService; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken; -import org.keycloak.representations.IDToken; +import com.hermesworld.ais.galapagos.security.config.GalapagosSecurityProperties; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import java.util.HashMap; import java.util.Map; @@ -18,6 +16,12 @@ @Component public class DefaultCurrentUserService implements CurrentUserService, EventContextSource { + private final GalapagosSecurityProperties securityConfig; + + public DefaultCurrentUserService(GalapagosSecurityProperties securityConfig) { + this.securityConfig = securityConfig; + } + @Override public Optional getCurrentUserName() { SecurityContext context = SecurityContextHolder.getContext(); @@ -30,29 +34,24 @@ public Optional getCurrentUserName() { @Override public Optional getCurrentPrincipal() { - return getKeycloakIDToken().map(token -> new AuditPrincipal(token.getPreferredUsername(), token.getName())); + return getAuthenticationToken() + .map(t -> new AuditPrincipal(t.getToken().getClaimAsString(securityConfig.getJwtUserNameClaim()), + t.getToken().getClaimAsString(securityConfig.getJwtDisplayNameClaim()))); } @Override public Optional getCurrentUserEmailAddress() { - return getKeycloakIDToken().map(token -> token.getEmail()) - .flatMap(s -> StringUtils.isEmpty(s) ? Optional.empty() : Optional.of(s)); + return getAuthenticationToken() + .map(token -> token.getToken().getClaimAsString(securityConfig.getJwtEmailClaim())); } - private Optional getKeycloakIDToken() { + private Optional getAuthenticationToken() { SecurityContext context = SecurityContextHolder.getContext(); - if (context.getAuthentication() == null || context.getAuthentication().getPrincipal() == null - || !(context.getAuthentication() instanceof KeycloakAuthenticationToken)) { - return Optional.empty(); + if (context.getAuthentication() instanceof JwtAuthenticationToken token) { + return Optional.of(token); } - KeycloakAuthenticationToken principal = (KeycloakAuthenticationToken) context.getAuthentication(); - KeycloakSecurityContext keycloakContext = principal.getAccount().getKeycloakSecurityContext(); - IDToken token = keycloakContext.getIdToken(); - if (token == null) { - token = keycloakContext.getToken(); - } - return Optional.ofNullable(token); + return Optional.empty(); } @Override @@ -65,11 +64,6 @@ public Map getContextValues() { } @Override - /** - * Checks from the Security Context, if the current user has the role of an Administrator - * - * @return true if the current user has the role of an Administrator, else false - */ public boolean isAdmin() { SecurityContext context = SecurityContextHolder.getContext(); if (context.getAuthentication() == null || context.getAuthentication().getAuthorities() == null) { diff --git a/src/main/java/com/hermesworld/ais/galapagos/security/impl/KeycloakConfigController.java b/src/main/java/com/hermesworld/ais/galapagos/security/impl/KeycloakConfigController.java deleted file mode 100644 index 917ce603..00000000 --- a/src/main/java/com/hermesworld/ais/galapagos/security/impl/KeycloakConfigController.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.hermesworld.ais.galapagos.security.impl; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import lombok.extern.slf4j.Slf4j; -import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; -import org.springframework.http.MediaType; -import org.springframework.util.StreamUtils; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@Slf4j -public class KeycloakConfigController { - - @Value("${keycloak.configurationFile}") - private Resource keycloakConfigFile; - - @GetMapping(value = "/keycloak/config.json", produces = MediaType.APPLICATION_JSON_VALUE) - public Map getKeycloakConfig() { - - try (InputStream in = keycloakConfigFile.getInputStream()) { - String contents = new String(StreamUtils.copyToByteArray(in), StandardCharsets.UTF_8); - JSONObject config = new JSONObject(contents); - - Map result = new HashMap<>(); - result.put("url", config.getString("auth-server-url")); - result.put("realm", config.getString("realm")); - result.put("clientId", config.getString("resource")); - return result; - } - catch (IOException e) { - log.error("Could not read Keycloak config resource", e); - return Collections.emptyMap(); - } - } - -} diff --git a/src/main/java/com/hermesworld/ais/galapagos/security/impl/OAuthConfigController.java b/src/main/java/com/hermesworld/ais/galapagos/security/impl/OAuthConfigController.java new file mode 100644 index 00000000..9e630307 --- /dev/null +++ b/src/main/java/com/hermesworld/ais/galapagos/security/impl/OAuthConfigController.java @@ -0,0 +1,60 @@ +package com.hermesworld.ais.galapagos.security.impl; + +import com.hermesworld.ais.galapagos.security.config.GalapagosSecurityProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.registration.ClientRegistrations; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@Slf4j +public class OAuthConfigController { + + private final OAuth2ClientProperties oAuth2ClientProperties; + + private final GalapagosSecurityProperties galapagosSecurityProperties; + + public OAuthConfigController(OAuth2ClientProperties oAuth2ClientProperties, + GalapagosSecurityProperties galapagosSecurityProperties) { + this.oAuth2ClientProperties = oAuth2ClientProperties; + this.galapagosSecurityProperties = galapagosSecurityProperties; + } + + @GetMapping(value = "/oauth2/config.json", produces = MediaType.APPLICATION_JSON_VALUE) + public Map getOauthConfig() { + Map registrations = oAuth2ClientProperties.getRegistration(); + if (registrations.isEmpty()) { + log.error("No Spring Security OAuth2 client registrations found. Cannot provide OAuth2 config to UI."); + return Map.of(); + } + if (registrations.size() > 1) { + log.error( + "More than one Spring Security OAuth2 client registration found. Cannot provide OAuth2 config to UI."); + return Map.of(); + } + var regKey = registrations.entrySet().iterator().next().getKey(); + var registration = registrations.get(regKey); + var provider = oAuth2ClientProperties.getProvider().get(regKey); + + if (provider == null) { + log.error("No Spring Security OAuth2 provider found with id \"{}\". Cannot provide OAuth2 config to UI.", + regKey); + return Map.of(); + } + + // Use .well-known endpoint to retrieve correct token endpoint + var fullReg = ClientRegistrations.fromOidcIssuerLocation(provider.getIssuerUri()) + .clientId(registration.getClientId()).build(); + + return Map.of("clientId", registration.getClientId(), "scope", registration.getScope(), "issuerUri", + provider.getIssuerUri(), "tokenEndpoint", fullReg.getProviderDetails().getTokenUri(), "userNameClaim", + galapagosSecurityProperties.getJwtUserNameClaim(), "displayNameClaim", + galapagosSecurityProperties.getJwtDisplayNameClaim(), "rolesClaim", + galapagosSecurityProperties.getJwtRoleClaim()); + } + +} diff --git a/src/main/java/com/hermesworld/ais/galapagos/staging/impl/StagingServiceImpl.java b/src/main/java/com/hermesworld/ais/galapagos/staging/impl/StagingServiceImpl.java index 30e03fa3..5c558da1 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/staging/impl/StagingServiceImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/staging/impl/StagingServiceImpl.java @@ -8,7 +8,6 @@ import com.hermesworld.ais.galapagos.staging.StagingService; import com.hermesworld.ais.galapagos.subscriptions.service.SubscriptionService; import com.hermesworld.ais.galapagos.topics.service.TopicService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @@ -26,7 +25,6 @@ public class StagingServiceImpl implements StagingService { private final SubscriptionService subscriptionService; - @Autowired public StagingServiceImpl(KafkaClusters kafkaClusters, ApplicationsService applicationsService, @Qualifier("nonvalidating") TopicService topicService, SubscriptionService subscriptionService) { this.kafkaClusters = kafkaClusters; diff --git a/src/main/java/com/hermesworld/ais/galapagos/subscriptions/controller/SubscriptionsController.java b/src/main/java/com/hermesworld/ais/galapagos/subscriptions/controller/SubscriptionsController.java index c025af77..0d66d407 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/subscriptions/controller/SubscriptionsController.java +++ b/src/main/java/com/hermesworld/ais/galapagos/subscriptions/controller/SubscriptionsController.java @@ -14,7 +14,6 @@ import com.hermesworld.ais.galapagos.topics.TopicMetadata; import com.hermesworld.ais.galapagos.topics.service.TopicService; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; @@ -34,7 +33,6 @@ public class SubscriptionsController { private final Supplier notFound = () -> new ResponseStatusException(HttpStatus.NOT_FOUND); - @Autowired public SubscriptionsController(SubscriptionService subscriptionService, ApplicationsService applicationsService, TopicService topicService, KafkaClusters kafkaEnvironments) { this.subscriptionService = subscriptionService; @@ -45,8 +43,7 @@ public SubscriptionsController(SubscriptionService subscriptionService, Applicat @GetMapping(value = "/api/applications/{applicationId}/subscriptions/{environmentId}", produces = MediaType.APPLICATION_JSON_VALUE) public List getApplicationSubscriptions(@PathVariable String applicationId, - @PathVariable String environmentId, - @RequestParam(name = "includeNonApproved", defaultValue = "false") boolean includeNonApproved) { + @PathVariable String environmentId, @RequestParam(defaultValue = "false") boolean includeNonApproved) { applicationsService.getKnownApplication(applicationId).orElseThrow(notFound); kafkaEnvironments.getEnvironmentMetadata(environmentId).orElseThrow(notFound); return subscriptionService.getSubscriptionsOfApplication(environmentId, applicationId, includeNonApproved) @@ -134,8 +131,7 @@ public void deleteApplicationSubscription(@PathVariable String applicationId, @P @GetMapping(value = "/api/topics/{environmentId}/{topicName}/subscriptions", produces = MediaType.APPLICATION_JSON_VALUE) public List getTopicSubscriptions(@PathVariable String environmentId, - @PathVariable String topicName, - @RequestParam(name = "includeNonApproved", defaultValue = "false") boolean includeNonApproved) { + @PathVariable String topicName, @RequestParam(defaultValue = "false") boolean includeNonApproved) { kafkaEnvironments.getEnvironmentMetadata(environmentId).orElseThrow(notFound); topicService.getTopic(environmentId, topicName).orElseThrow(notFound); diff --git a/src/main/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionServiceImpl.java b/src/main/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionServiceImpl.java index 7a7db624..f7e07943 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionServiceImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionServiceImpl.java @@ -15,14 +15,10 @@ import com.hermesworld.ais.galapagos.topics.TopicType; import com.hermesworld.ais.galapagos.topics.service.TopicService; import com.hermesworld.ais.galapagos.util.FutureUtil; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; -import java.util.Collections; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import java.util.function.Predicate; @@ -41,7 +37,6 @@ public class SubscriptionServiceImpl implements SubscriptionService, InitPerClus private static final String TOPIC_NAME = "subscriptions"; - @Autowired public SubscriptionServiceImpl(KafkaClusters kafkaEnvironments, ApplicationsService applicationsService, @Qualifier(value = "nonvalidating") TopicService topicService, GalapagosEventManager eventManager) { this.kafkaEnvironments = kafkaEnvironments; @@ -80,6 +75,15 @@ public CompletableFuture addSubscription(String environmen .failedFuture(new IllegalArgumentException("Cannot subscribe to application internal topics")); } + List subscriptionsForTopic = getSubscriptionsForTopic(environmentId, + subscriptionMetadata.getTopicName(), true); + for (var subscription : subscriptionsForTopic) { + if (Objects.equals(subscription.getClientApplicationId(), subscriptionMetadata.getClientApplicationId())) { + return CompletableFuture.failedFuture(new IllegalArgumentException( + "A subscription of this topic for this application already exists.")); + } + } + SubscriptionMetadata subscription = new SubscriptionMetadata(); subscription.setId(UUID.randomUUID().toString()); subscription.setTopicName(subscriptionMetadata.getTopicName()); diff --git a/src/main/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionTopicListener.java b/src/main/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionTopicListener.java index 7c69d582..fb643449 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionTopicListener.java +++ b/src/main/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionTopicListener.java @@ -6,7 +6,6 @@ import com.hermesworld.ais.galapagos.subscriptions.SubscriptionState; import com.hermesworld.ais.galapagos.subscriptions.service.SubscriptionService; import com.hermesworld.ais.galapagos.util.FutureUtil; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.concurrent.CompletableFuture; @@ -29,7 +28,6 @@ public class SubscriptionTopicListener implements TopicEventsListener { private final SubscriptionService subscriptionService; - @Autowired public SubscriptionTopicListener(SubscriptionService subscriptionService) { this.subscriptionService = subscriptionService; } diff --git a/src/main/java/com/hermesworld/ais/galapagos/topics/config/GalapagosTopicConfig.java b/src/main/java/com/hermesworld/ais/galapagos/topics/config/GalapagosTopicConfig.java index 1eeaf594..9accd182 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/topics/config/GalapagosTopicConfig.java +++ b/src/main/java/com/hermesworld/ais/galapagos/topics/config/GalapagosTopicConfig.java @@ -28,4 +28,6 @@ public class GalapagosTopicConfig { private int criticalReplicationFactor; + private TopicSchemaConfig schemas = new TopicSchemaConfig(); + } diff --git a/src/main/java/com/hermesworld/ais/galapagos/topics/config/TopicSchemaConfig.java b/src/main/java/com/hermesworld/ais/galapagos/topics/config/TopicSchemaConfig.java new file mode 100644 index 00000000..84b7196a --- /dev/null +++ b/src/main/java/com/hermesworld/ais/galapagos/topics/config/TopicSchemaConfig.java @@ -0,0 +1,14 @@ +package com.hermesworld.ais.galapagos.topics.config; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TopicSchemaConfig { + + private boolean allowAddedPropertiesOnCommandTopics; + + private boolean allowRemovedOptionalProperties; + +} diff --git a/src/main/java/com/hermesworld/ais/galapagos/topics/controller/ChangeTopicOwnerDto.java b/src/main/java/com/hermesworld/ais/galapagos/topics/controller/ChangeTopicOwnerDto.java index abcca90e..0939a05d 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/topics/controller/ChangeTopicOwnerDto.java +++ b/src/main/java/com/hermesworld/ais/galapagos/topics/controller/ChangeTopicOwnerDto.java @@ -3,7 +3,7 @@ import lombok.Getter; import lombok.Setter; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; @Getter @Setter diff --git a/src/main/java/com/hermesworld/ais/galapagos/topics/controller/TopicController.java b/src/main/java/com/hermesworld/ais/galapagos/topics/controller/TopicController.java index 44f9ba2a..98df1f91 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/topics/controller/TopicController.java +++ b/src/main/java/com/hermesworld/ais/galapagos/topics/controller/TopicController.java @@ -21,7 +21,6 @@ import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.common.KafkaException; import org.apache.kafka.common.header.Header; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -29,7 +28,7 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; -import javax.validation.Valid; +import jakarta.validation.Valid; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; @@ -61,7 +60,6 @@ public class TopicController { private static final int PEEK_LIMIT = 100; - @Autowired public TopicController(ValidatingTopicService topicService, KafkaClusters kafkaEnvironments, ApplicationsService applicationsService, NamingService namingService, CurrentUserService userService) { this.topicService = topicService; diff --git a/src/main/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImpl.java b/src/main/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImpl.java index 5bc96bb8..3148867e 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImpl.java @@ -12,8 +12,7 @@ import com.hermesworld.ais.galapagos.kafka.util.TopicBasedRepository; import com.hermesworld.ais.galapagos.naming.InvalidTopicNameException; import com.hermesworld.ais.galapagos.naming.NamingService; -import com.hermesworld.ais.galapagos.schemas.IncompatibleSchemaException; -import com.hermesworld.ais.galapagos.schemas.SchemaUtil; +import com.hermesworld.ais.galapagos.schemas.*; import com.hermesworld.ais.galapagos.security.CurrentUserService; import com.hermesworld.ais.galapagos.topics.*; import com.hermesworld.ais.galapagos.topics.config.GalapagosTopicConfig; @@ -26,7 +25,6 @@ import org.everit.json.schema.loader.SchemaLoader; import org.json.JSONException; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; @@ -62,7 +60,6 @@ public class TopicServiceImpl implements TopicService, InitPerCluster { static final String SCHEMA_TOPIC_NAME = "schemas"; - @Autowired public TopicServiceImpl(KafkaClusters kafkaClusters, ApplicationsService applicationsService, NamingService namingService, CurrentUserService userService, GalapagosTopicConfig topicSettings, GalapagosEventManager eventManager) { @@ -360,9 +357,10 @@ public CompletableFuture deleteLatestTopicSchemaVersion(String environment } if (schemaOnNextStage != null) { - return CompletableFuture.failedFuture( - new IllegalStateException("The selected schema already exists on the next stage! To delete " - + "this schema you have to delete it there first!")); + return CompletableFuture.failedFuture(new IllegalStateException(""" + The selected schema already exists on the next stage! To delete \ + this schema you have to delete it there first!\ + """)); } GalapagosEventSink eventSink = eventManager.newEventSink(kafkaCluster); @@ -418,9 +416,11 @@ public CompletableFuture addTopicSchemaVersion(String environmen if (newSchema.definesProperty("data") && (metadata.getType() == TopicType.EVENTS || metadata.getType() == TopicType.COMMANDS)) { - return CompletableFuture.failedFuture( - new IllegalArgumentException("The JSON Schema must not declare a \"data\" object on first level." - + " The JSON Schema must not contain the CloudEvents fields, but only the contents of the \"data\" field.")); + return CompletableFuture.failedFuture(new IllegalArgumentException( + """ + The JSON Schema must not declare a "data" object on first level.\ + The JSON Schema must not contain the CloudEvents fields, but only the contents of the "data" field.\ + """)); } if (existingVersions.isEmpty() && schemaMetadata.getSchemaVersion() != 1) { @@ -446,11 +446,19 @@ public CompletableFuture addTopicSchemaVersion(String environmen "The new schema is identical to the latest schema of this topic.")); } - boolean reverse = metadata.getType() == TopicType.COMMANDS; - - SchemaUtil.verifyCompatibleTo(reverse ? newSchema : previousSchema, - reverse ? previousSchema : newSchema); + SchemaCompatibilityValidator validator; + if (metadata.getType() == TopicType.COMMANDS) { + validator = new SchemaCompatibilityValidator(newSchema, previousSchema, + new ProducerCompatibilityErrorHandler( + topicSettings.getSchemas().isAllowAddedPropertiesOnCommandTopics())); + } + else { + validator = new SchemaCompatibilityValidator(previousSchema, newSchema, + new ConsumerCompatibilityErrorHandler( + topicSettings.getSchemas().isAllowRemovedOptionalProperties())); + } + validator.validate(); } catch (JSONException e) { // how, on earth, did it get into the repo then??? diff --git a/src/main/java/com/hermesworld/ais/galapagos/topics/service/impl/ValidatingTopicServiceImpl.java b/src/main/java/com/hermesworld/ais/galapagos/topics/service/impl/ValidatingTopicServiceImpl.java index a6982d16..8c0df26b 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/topics/service/impl/ValidatingTopicServiceImpl.java +++ b/src/main/java/com/hermesworld/ais/galapagos/topics/service/impl/ValidatingTopicServiceImpl.java @@ -11,7 +11,6 @@ import com.hermesworld.ais.galapagos.topics.service.TopicService; import com.hermesworld.ais.galapagos.topics.service.ValidatingTopicService; import org.apache.kafka.clients.consumer.ConsumerRecord; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; @@ -45,7 +44,6 @@ public class ValidatingTopicServiceImpl implements ValidatingTopicService { private final boolean schemaDeleteWithSub; - @Autowired public ValidatingTopicServiceImpl(@Qualifier(value = "nonvalidating") TopicService topicService, SubscriptionService subscriptionService, ApplicationsService applicationsService, KafkaClusters kafkaClusters, GalapagosTopicConfig topicConfig, diff --git a/src/main/java/com/hermesworld/ais/galapagos/uisupport/controller/UISupportController.java b/src/main/java/com/hermesworld/ais/galapagos/uisupport/controller/UISupportController.java index e7204f87..44158b7a 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/uisupport/controller/UISupportController.java +++ b/src/main/java/com/hermesworld/ais/galapagos/uisupport/controller/UISupportController.java @@ -17,7 +17,6 @@ import com.hermesworld.ais.galapagos.util.CertificateUtil; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.config.TopicConfig; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.StreamUtils; @@ -77,7 +76,6 @@ public class UISupportController { private static final long ONE_WEEK = TimeUnit.DAYS.toMillis(7); - @Autowired public UISupportController(ApplicationsService applicationsService, TopicService topicService, KafkaClusters kafkaClusters, NamingService namingService, GalapagosTopicConfig topicConfig, CustomLinksConfig customLinksConfig, GalapagosChangesConfig changesConfig) { diff --git a/src/main/java/com/hermesworld/ais/galapagos/util/controller/BackupController.java b/src/main/java/com/hermesworld/ais/galapagos/util/controller/BackupController.java index 19358b58..8fe7dd42 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/util/controller/BackupController.java +++ b/src/main/java/com/hermesworld/ais/galapagos/util/controller/BackupController.java @@ -10,7 +10,6 @@ import lombok.extern.slf4j.Slf4j; import org.json.JSONException; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.annotation.Secured; @@ -26,7 +25,6 @@ public class BackupController { private final ObjectMapper objectMapper = JsonUtil.newObjectMapper(); - @Autowired public BackupController(KafkaClusters kafkaClusters) { this.kafkaClusters = kafkaClusters; } diff --git a/src/main/java/com/hermesworld/ais/galapagos/util/impl/StartupRepositoryInitializer.java b/src/main/java/com/hermesworld/ais/galapagos/util/impl/StartupRepositoryInitializer.java index 65680811..f5a4931c 100644 --- a/src/main/java/com/hermesworld/ais/galapagos/util/impl/StartupRepositoryInitializer.java +++ b/src/main/java/com/hermesworld/ais/galapagos/util/impl/StartupRepositoryInitializer.java @@ -10,7 +10,6 @@ import com.hermesworld.ais.galapagos.kafka.KafkaClusters; import com.hermesworld.ais.galapagos.kafka.util.InitPerCluster; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; @@ -35,7 +34,6 @@ public class StartupRepositoryInitializer { private final Duration repositoryLoadIdleTime; - @Autowired public StartupRepositoryInitializer(KafkaClusters kafkaClusters, @Value("${galapagos.initialRepositoryLoadWaitTime:5s}") Duration initialRepositoryLoadWaitTime, @Value("${galapagos.repositoryLoadIdleTime:2s}") Duration repositoryLoadIdleTime) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d33423a0..cc469101 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,4 @@ -# This is usually overridden by local properties files. -keycloak.configurationFile=classpath:keycloak.json +# See application-oauth2.properties in project root for example OAuth2 configuration. # We only use Thymeleaf for e-mail templates, so do not check default location. spring.thymeleaf.check-template-location=false @@ -63,12 +62,29 @@ galapagos.topics.criticalReplicationFactor=3 # P1Y2M10D - 1 year, 2 months, 10 days galapagos.topics.minDeprecationTime=P3M +# Enables "liberal" schema compatibility check for COMMAND topics, allowing for additional (optional) properties in new +# schema versions. Theoretically, a current producer could use that exact property name you are going to add as an +# optional property with different format already in its messages (for whatever reason). But in practice, that is no +# realistic case, so enabling this gives teams more flexibility for extending command topics. +# For backwards compatibility, this is false by default, but we recommend to enable this feature. +galapagos.topics.schemas.allowAddedPropertiesOnCommandTopics=false + +# Enables "liberal" schema compatibility check for EVENT topics, allowing for optional properties to be removed in new +# schema versions. Theoretically, removed properties could, after removal from the schema, still be provided by the +# topic producer, but with a different format than specified before (additionalProperties must be true for this case). +# Consumers relying on the old schema could then break because of the unexpected format change. But usually, this is +# not a realistic case, so you can allow your teams to just remove optional properties. +# For backwards compatibility, this is false by default, but we recommend to enable this feature. +galapagos.topics.schemas.allowRemovedOptionalProperties=false + # In this directory, the truststore and Galapagos client certificates will be generated. # The Kafka client library needs files for the configuration, this is why we need a folder for this. # As private keys are stored here, this directory MUST NOT BE READABLE for other users! galapagos.kafka.certificates-workdir=file:/tmp + # The prefix for Galapagos internal topics galapagos.kafka.metadataTopicsPrefix=galapagos.internal. + # entries specifies the minimum number of changes in the dashboard. # minDays indicates that all changes since X days ago are displayed in the dashboard. # This Setting only impacts the UI and doesn't change the REST endpoint. The bigger value gets used. @@ -76,11 +92,13 @@ galapagos.kafka.metadataTopicsPrefix=galapagos.internal. # The dashboard will show 28 changes. galapagos.changelog.entries=10 galapagos.changelog.minDays=30 + # Specify what Images to use in the Changelog Dashboard. The default picture is only relevant if for some reasons # the profile picture can't be loaded. The custom image url is only relevant if you use the CUSTOM option, in this case you can use a custom # url as image which will be then shown in the UI. The options are GRAVATAR, INITIALS, CUSTOM, NONE. galapagos.changelog.profilePicture=GRAVATAR galapagos.changelog.defaultPicture=INITIALS + # If you choose CUSTOM as picture type, specify a URL where {0} will be replaced by the username stored in Galapagos # (depending on auth config, e.g. e-mail address). # This is an example how you can use it if you have a company-wide SharePoint: @@ -89,26 +107,32 @@ galapagos.changelog.customImageUrl=https://mycompany.sharepoint.com/_layouts/15/ # A list of ACLs which shall be added to every application user in Kafka. # This is useful e.g. for tools like Confluent Managed Connectors, relying on some default rights. # permission=ALLOW and host=* is assumed for every ACL here. -# Note that Cluster DESCRIBE rights are always given to Kafka users by Galapagos. -# galapagos.kafka.defaultAcls[0].name=connect-lcc -# galapagos.kafka.defaultAcls[0].resourceType=GROUP -# galapagos.kafka.defaultAcls[0].operation=READ -# galapagos.kafka.defaultAcls[0].patternType=PREFIXED +# Note that you do not need the CLUSTER DESCRIBE right for standard applications; it is here for backward compatibility. +galapagos.kafka.defaultAcls[0].name=kafka-cluster +galapagos.kafka.defaultAcls[0].resourceType=CLUSTER +galapagos.kafka.defaultAcls[0].operation=DESCRIBE +galapagos.kafka.defaultAcls[0].patternType=LITERAL +galapagos.kafka.defaultAcls[1].name=kafka-cluster +galapagos.kafka.defaultAcls[1].resourceType=CLUSTER +galapagos.kafka.defaultAcls[1].operation=DESCRIBE_CONFIGS +galapagos.kafka.defaultAcls[1].patternType=LITERAL + # The fields of a list of Custom Links # Special ID "naming-convention" is ALSO displayed on "create topic" page, next to topic name field galapagos.customLinks.links[0].id=1 galapagos.customLinks.links[0].href=https://github.com/HermesGermany/galapagos/blob/main/kafka_guidelines.md galapagos.customLinks.links[0].label=Galapagos Kafka Guidelines galapagos.customLinks.links[0].linkType=EDUCATIONAL - galapagos.customLinks.links[1].id=naming-convention galapagos.customLinks.links[1].href=https://wiki.mycompany.company/Kafka+Naming+Conventions galapagos.customLinks.links[1].label=MyAcme Naming Conventions galapagos.customLinks.links[1].linkType=EDUCATIONAL + # Timing parameters to wait for repository initialization at program start. # Should usually not be changed, unless you experience some problems e.g. when running Admin Jobs. galapagos.initialRepositoryLoadWaitTime=5s galapagos.repositoryLoadIdleTime=2s + logging.level.org.apache.kafka=WARN # This is important for the UI and should not be overridden. diff --git a/src/test/java/com/hermesworld/ais/galapagos/ContextStartupTest.java b/src/test/java/com/hermesworld/ais/galapagos/ContextStartupTest.java index 44eeb18d..6f463748 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/ContextStartupTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/ContextStartupTest.java @@ -1,24 +1,22 @@ package com.hermesworld.ais.galapagos; -import static org.junit.Assert.assertNotNull; - -import java.security.Security; - +import com.hermesworld.ais.galapagos.kafka.KafkaClusters; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.ApplicationContext; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.context.annotation.Import; -import com.hermesworld.ais.galapagos.kafka.KafkaClusters; +import java.security.Security; + +import static org.junit.jupiter.api.Assertions.assertNotNull; -@RunWith(SpringRunner.class) @SpringBootTest -public class ContextStartupTest { +@Import(GalapagosTestConfig.class) +class ContextStartupTest { @Autowired private ApplicationContext context; @@ -27,14 +25,14 @@ public class ContextStartupTest { @MockBean private KafkaClusters kafkaClusters; - @BeforeClass - public static void setupSecurity() { + @BeforeAll + static void setupSecurity() { Security.setProperty("crypto.policy", "unlimited"); Security.addProvider(new BouncyCastleProvider()); } @Test - public void testStartupContext() { + void testStartupContext() { assertNotNull(kafkaClusters); assertNotNull(context); } diff --git a/src/test/java/com/hermesworld/ais/galapagos/GalapagosTestConfig.java b/src/test/java/com/hermesworld/ais/galapagos/GalapagosTestConfig.java new file mode 100644 index 00000000..e9b9063a --- /dev/null +++ b/src/test/java/com/hermesworld/ais/galapagos/GalapagosTestConfig.java @@ -0,0 +1,20 @@ +package com.hermesworld.ais.galapagos; + +import com.hermesworld.ais.galapagos.security.SecurityConfig; +import com.hermesworld.ais.galapagos.security.impl.OAuthConfigController; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; + +@Configuration +public class GalapagosTestConfig { + + @MockBean + private OAuthConfigController mockController; + + @MockBean + private SecurityConfig securityConfig; + + @MockBean + private ClientRegistrationRepository clientRegistrationRepository; +} diff --git a/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/CreateBackupJobTest.java b/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/CreateBackupJobTest.java index 99e4b566..bff0b6b9 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/CreateBackupJobTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/CreateBackupJobTest.java @@ -12,9 +12,9 @@ import com.hermesworld.ais.galapagos.topics.TopicMetadata; import com.hermesworld.ais.galapagos.topics.TopicType; import com.hermesworld.ais.galapagos.util.JsonUtil; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.springframework.boot.ApplicationArguments; import java.nio.file.Files; @@ -24,7 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; -public class CreateBackupJobTest { +class CreateBackupJobTest { private KafkaClusters kafkaClusters; @@ -34,8 +34,8 @@ public class CreateBackupJobTest { private ObjectMapper mapper; - @Before - public void setUp() { + @BeforeEach + void setUp() { testRepo.put("topics", new TopicBasedRepositoryMock() { @Override public Class getValueClass() { @@ -187,10 +187,10 @@ public Collection getObjects() { @Test @DisplayName("it should create a backup from all the metadata currently saved within Galapagos") - public void createBackUp_success() throws Exception { + void createBackUp_success() throws Exception { CreateBackupJob job = new CreateBackupJob(kafkaClusters); ApplicationArguments args = mock(ApplicationArguments.class); - when(args.getOptionValues("create.backup.file")).thenReturn(Collections.singletonList("true")); + when(args.getOptionValues("create.backup.file")).thenReturn(List.of("true")); try { job.run(args); @@ -222,21 +222,21 @@ public void createBackUp_success() throws Exception { .toString(); // test data - assertEquals(topicName, "\"topic-1\""); - assertEquals(topicType, "\"EVENTS\""); - assertEquals(clientApplicationIdSub, "\"app-1\""); - assertEquals(subId, "\"123\""); - assertEquals(aorId, "\"1\""); - assertEquals(aorState, "\"APPROVED\""); - assertEquals(username, "\"myUser\""); + assertEquals("\"topic-1\"", topicName); + assertEquals("\"EVENTS\"", topicType); + assertEquals("\"app-1\"", clientApplicationIdSub); + assertEquals("\"123\"", subId); + assertEquals("\"1\"", aorId); + assertEquals("\"APPROVED\"", aorState); + assertEquals("\"myUser\"", username); // prod data - assertEquals(topicNameProd, "\"topic-2\""); - assertEquals(topicTypeProd, "\"EVENTS\""); - assertEquals(clientApplicationIdSubProd, "\"app-12\""); - assertEquals(subIdProd, "\"12323\""); - assertEquals(aorIdProd, "\"2\""); - assertEquals(aorStateProd, "\"APPROVED\""); - assertEquals(usernameProd, "\"myUser2\""); + assertEquals("\"topic-2\"", topicNameProd); + assertEquals("\"EVENTS\"", topicTypeProd); + assertEquals("\"app-12\"", clientApplicationIdSubProd); + assertEquals("\"12323\"", subIdProd); + assertEquals("\"2\"", aorIdProd); + assertEquals("\"APPROVED\"", aorStateProd); + assertEquals("\"myUser2\"", usernameProd); } diff --git a/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingCertificateJobTest.java b/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingCertificateJobTest.java index 3d55df5c..f0f7e5aa 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingCertificateJobTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/GenerateToolingCertificateJobTest.java @@ -32,7 +32,10 @@ import java.security.Security; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.*; +import java.util.Base64; +import java.util.Enumeration; +import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import static org.junit.jupiter.api.Assertions.*; @@ -40,7 +43,7 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -public class GenerateToolingCertificateJobTest { +class GenerateToolingCertificateJobTest { @Mock private KafkaClusters kafkaClusters; @@ -66,7 +69,7 @@ public class GenerateToolingCertificateJobTest { private static final String DATA_MARKER = "CERTIFICATE DATA: "; @BeforeEach - public void feedMocks() throws Exception { + void feedMocks() throws Exception { Security.setProperty("crypto.policy", "unlimited"); Security.addProvider(new BouncyCastleProvider()); @@ -106,20 +109,20 @@ public void feedMocks() throws Exception { } @AfterEach - public void cleanup() { + void cleanup() { // noinspection ResultOfMethodCallIgnored testFile.delete(); System.setOut(oldOut); } @Test - public void testStandard() throws Exception { + void testStandard() throws Exception { GenerateToolingCertificateJob job = new GenerateToolingCertificateJob(kafkaClusters, aclSupport, namingService, kafkaConfig); ApplicationArguments args = mock(ApplicationArguments.class); - when(args.getOptionValues("output.filename")).thenReturn(Collections.singletonList(testFile.getPath())); - when(args.getOptionValues("kafka.environment")).thenReturn(Collections.singletonList("test")); + when(args.getOptionValues("output.filename")).thenReturn(List.of(testFile.getPath())); + when(args.getOptionValues("kafka.environment")).thenReturn(List.of("test")); job.run(args); @@ -144,12 +147,12 @@ public void testStandard() throws Exception { } @Test - public void testDataOnStdout() throws Exception { + void testDataOnStdout() throws Exception { GenerateToolingCertificateJob job = new GenerateToolingCertificateJob(kafkaClusters, aclSupport, namingService, kafkaConfig); ApplicationArguments args = mock(ApplicationArguments.class); - when(args.getOptionValues("kafka.environment")).thenReturn(Collections.singletonList("test")); + when(args.getOptionValues("kafka.environment")).thenReturn(List.of("test")); job.run(args); diff --git a/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportBackupJobTest.java b/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportBackupJobTest.java index 6fc634b2..b5ae9d3f 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportBackupJobTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportBackupJobTest.java @@ -11,7 +11,6 @@ import org.springframework.boot.ApplicationArguments; import java.io.File; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -20,7 +19,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class ImportBackupJobTest { +class ImportBackupJobTest { private ImportBackupJob job; @@ -33,7 +32,7 @@ public class ImportBackupJobTest { private TopicBasedRepositoryMock topicRepository; @BeforeEach - public void setUp() { + void setUp() { kafkaClusters = mock(KafkaClusters.class); testCluster = mock(KafkaCluster.class); when(testCluster.getId()).thenReturn("prod"); @@ -55,12 +54,12 @@ public String getTopicName() { @Test @DisplayName("should import backup from import file") - public void importBackupTest() throws Exception { + void importBackupTest() throws Exception { // given ApplicationArguments args = mock(ApplicationArguments.class); - when(args.getOptionValues("import.file")).thenReturn(Collections.singletonList(testFile.getPath())); - when(args.getOptionValues("clearRepos")).thenReturn(Collections.singletonList("false")); - when(testCluster.getRepositories()).thenReturn(Collections.singletonList(topicRepository)); + when(args.getOptionValues("import.file")).thenReturn(List.of(testFile.getPath())); + when(args.getOptionValues("clearRepos")).thenReturn(List.of("false")); + when(testCluster.getRepositories()).thenReturn(List.of(topicRepository)); // when job.run(args); @@ -70,11 +69,11 @@ public void importBackupTest() throws Exception { @Test @DisplayName("should not clear non-imported environments") - public void importBackup_noClearOnOtherEnv() throws Exception { + void importBackup_noClearOnOtherEnv() throws Exception { ApplicationArguments args = mock(ApplicationArguments.class); - when(args.getOptionValues("import.file")).thenReturn(Collections.singletonList(testFile.getPath())); - when(args.getOptionValues("clearRepos")).thenReturn(Collections.singletonList("true")); - when(testCluster.getRepositories()).thenReturn(Collections.singletonList(topicRepository)); + when(args.getOptionValues("import.file")).thenReturn(List.of(testFile.getPath())); + when(args.getOptionValues("clearRepos")).thenReturn(List.of("true")); + when(testCluster.getRepositories()).thenReturn(List.of(topicRepository)); KafkaCluster devCluster = mock(KafkaCluster.class); when(devCluster.getId()).thenReturn("dev"); @@ -108,14 +107,14 @@ public CompletableFuture delete(TopicMetadata value) { @Test @DisplayName("should import backup from import file and old metadata in repos should still be present") - public void importBackupWithoutClearingExistingRepos() throws Exception { + void importBackupWithoutClearingExistingRepos() throws Exception { // given ApplicationArguments args = mock(ApplicationArguments.class); topicRepository.save(buildTopicMetadata()).get(); // when - when(args.getOptionValues("import.file")).thenReturn(Collections.singletonList(testFile.getPath())); - when(args.getOptionValues("clearRepos")).thenReturn(Collections.singletonList("false")); - when(testCluster.getRepositories()).thenReturn(Collections.singletonList(topicRepository)); + when(args.getOptionValues("import.file")).thenReturn(List.of(testFile.getPath())); + when(args.getOptionValues("clearRepos")).thenReturn(List.of("false")); + when(testCluster.getRepositories()).thenReturn(List.of(topicRepository)); // then job.run(args); @@ -126,14 +125,14 @@ public void importBackupWithoutClearingExistingRepos() throws Exception { @Test @DisplayName("should import backup from import file and old metadata in repos should be not present") - public void importBackupWithClearingExistingRepos() throws Exception { + void importBackupWithClearingExistingRepos() throws Exception { // given ApplicationArguments args = mock(ApplicationArguments.class); topicRepository.save(buildTopicMetadata()).get(); // when - when(args.getOptionValues("import.file")).thenReturn(Collections.singletonList(testFile.getPath())); - when(args.getOptionValues("clearRepos")).thenReturn(Collections.singletonList("true")); - when(testCluster.getRepositories()).thenReturn(Collections.singletonList(topicRepository)); + when(args.getOptionValues("import.file")).thenReturn(List.of(testFile.getPath())); + when(args.getOptionValues("clearRepos")).thenReturn(List.of("true")); + when(testCluster.getRepositories()).thenReturn(List.of(topicRepository)); // then job.run(args); @@ -144,13 +143,13 @@ public void importBackupWithClearingExistingRepos() throws Exception { @Test @DisplayName("should throw exception because no import file is set") - public void importBackupTest_noFileOption() throws Exception { + void importBackupTest_noFileOption() throws Exception { // given ApplicationArguments args = mock(ApplicationArguments.class); topicRepository.save(buildTopicMetadata()).get(); // when - when(args.getOptionValues("clearRepos")).thenReturn(Collections.singletonList("true")); - when(testCluster.getRepositories()).thenReturn(Collections.singletonList(topicRepository)); + when(args.getOptionValues("clearRepos")).thenReturn(List.of("true")); + when(testCluster.getRepositories()).thenReturn(List.of(topicRepository)); // then try { @@ -165,13 +164,13 @@ public void importBackupTest_noFileOption() throws Exception { @Test @DisplayName("should throw exception because no clearRepos option given") - public void importBackupTest_noClearReposOption() throws Exception { + void importBackupTest_noClearReposOption() throws Exception { // given ApplicationArguments args = mock(ApplicationArguments.class); topicRepository.save(buildTopicMetadata()).get(); // when - when(args.getOptionValues("import.file")).thenReturn(Collections.singletonList(testFile.getPath())); - when(testCluster.getRepositories()).thenReturn(Collections.singletonList(topicRepository)); + when(args.getOptionValues("import.file")).thenReturn(List.of(testFile.getPath())); + when(testCluster.getRepositories()).thenReturn(List.of(topicRepository)); // then try { @@ -186,7 +185,7 @@ public void importBackupTest_noClearReposOption() throws Exception { @Test @DisplayName("should return correct job name") - public void importBackupTest_correctJobName() { + void importBackupTest_correctJobName() { assertEquals("import-backup", job.getJobName()); } diff --git a/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportKnownApplicationsJobTest.java b/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportKnownApplicationsJobTest.java index ae3ce136..8ad6dcbb 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportKnownApplicationsJobTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ImportKnownApplicationsJobTest.java @@ -4,7 +4,6 @@ import java.io.File; import java.io.PrintStream; import java.nio.charset.StandardCharsets; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -15,15 +14,18 @@ import com.hermesworld.ais.galapagos.kafka.KafkaClusters; import com.hermesworld.ais.galapagos.kafka.impl.TopicBasedRepositoryMock; import com.hermesworld.ais.galapagos.util.JsonUtil; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import org.junit.Before; -import org.junit.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import org.springframework.boot.ApplicationArguments; -public class ImportKnownApplicationsJobTest { +class ImportKnownApplicationsJobTest { private KafkaClusters kafkaClusters; @@ -35,8 +37,8 @@ public class ImportKnownApplicationsJobTest { private ObjectMapper mapper; - @Before - public void setUp() { + @BeforeEach + void setUp() { mapper = JsonUtil.newObjectMapper(); kafkaClusters = mock(KafkaClusters.class); @@ -50,7 +52,7 @@ public void setUp() { } @Test - public void reImportAfterAppChanges() throws Exception { + void reImportAfterAppChanges() throws Exception { List knownApplications = mapper.readValue(fileWithOutInfoUrl, new TypeReference>() { @@ -58,8 +60,7 @@ public void reImportAfterAppChanges() throws Exception { ImportKnownApplicationsJob job = new ImportKnownApplicationsJob(kafkaClusters); ApplicationArguments args = mock(ApplicationArguments.class); - when(args.getOptionValues("applications.import.file")) - .thenReturn(Collections.singletonList(fileWithInfoUrl.getPath())); + when(args.getOptionValues("applications.import.file")).thenReturn(List.of(fileWithInfoUrl.getPath())); knownApplications.forEach(app -> appRepository.save(app)); // redirect STDOUT to check update count @@ -82,7 +83,7 @@ public void reImportAfterAppChanges() throws Exception { } @Test - public void importApps_alreadyIdentical() throws Exception { + void importApps_alreadyIdentical() throws Exception { List knownApplications = mapper.readValue(fileWithOutInfoUrl, new TypeReference>() { @@ -90,8 +91,7 @@ public void importApps_alreadyIdentical() throws Exception { ImportKnownApplicationsJob job = new ImportKnownApplicationsJob(kafkaClusters); ApplicationArguments args = mock(ApplicationArguments.class); - when(args.getOptionValues("applications.import.file")) - .thenReturn(Collections.singletonList(fileWithOutInfoUrl.getPath())); + when(args.getOptionValues("applications.import.file")).thenReturn(List.of(fileWithOutInfoUrl.getPath())); TopicBasedRepositoryMock appRepository = new TopicBasedRepositoryMock<>(); knownApplications.forEach(app -> appRepository.save(app)); when(kafkaClusters.getGlobalRepository("known-applications", KnownApplicationImpl.class)) @@ -116,12 +116,11 @@ public void importApps_alreadyIdentical() throws Exception { } @Test - public void importApps_positiv() throws Exception { + void importApps_positiv() throws Exception { ImportKnownApplicationsJob job = new ImportKnownApplicationsJob(kafkaClusters); ApplicationArguments args = mock(ApplicationArguments.class); - when(args.getOptionValues("applications.import.file")) - .thenReturn(Collections.singletonList(fileWithOutInfoUrl.getPath())); + when(args.getOptionValues("applications.import.file")).thenReturn(List.of(fileWithOutInfoUrl.getPath())); TopicBasedRepositoryMock appRepository = new TopicBasedRepositoryMock<>(); when(kafkaClusters.getGlobalRepository("known-applications", KnownApplicationImpl.class)) .thenReturn(appRepository); @@ -141,9 +140,9 @@ public void importApps_positiv() throws Exception { String output = new String(buffer.toByteArray(), StandardCharsets.UTF_8); assertTrue(output.contains("\n5 new application(s) imported.")); - assertEquals(appRepository.getObjects().size(), 5); + assertEquals(5, appRepository.getObjects().size()); assertTrue(appRepository.getObject("2222").isPresent()); - assertEquals(appRepository.getObject("F.I.V.E").get().getName(), "High Five"); + assertEquals("High Five", appRepository.getObject("F.I.V.E").get().getName()); } diff --git a/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ViewAclsJobTest.java b/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ViewAclsJobTest.java index b77beaeb..af452924 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ViewAclsJobTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/adminjobs/impl/ViewAclsJobTest.java @@ -1,12 +1,14 @@ package com.hermesworld.ais.galapagos.adminjobs.impl; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; import java.io.PrintStream; -import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -19,34 +21,34 @@ import org.apache.kafka.common.resource.ResourceType; import org.json.JSONArray; import org.json.JSONObject; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.boot.ApplicationArguments; import com.hermesworld.ais.galapagos.kafka.KafkaCluster; import com.hermesworld.ais.galapagos.kafka.KafkaClusters; import com.hermesworld.ais.galapagos.util.FutureUtil; -public class ViewAclsJobTest { +class ViewAclsJobTest { private ByteArrayOutputStream stdoutData = new ByteArrayOutputStream(); private PrintStream oldOut; - @Before - public void setup() { + @BeforeEach + void setup() { oldOut = System.out; System.setOut(new PrintStream(stdoutData)); } - @After - public void cleanup() { + @AfterEach + void cleanup() { System.setOut(oldOut); } @Test - public void testJsonMapping() throws Exception { + void testJsonMapping() throws Exception { KafkaClusters clusters = mock(KafkaClusters.class); KafkaCluster cluster = mock(KafkaCluster.class); @@ -67,7 +69,7 @@ public void testJsonMapping() throws Exception { ViewAclsJob job = new ViewAclsJob(clusters); ApplicationArguments args = mock(ApplicationArguments.class); - when(args.getOptionValues("kafka.environment")).thenReturn(Collections.singletonList("test")); + when(args.getOptionValues("kafka.environment")).thenReturn(List.of("test")); job.run(args); diff --git a/src/test/java/com/hermesworld/ais/galapagos/applications/ApplicationsControllerTest.java b/src/test/java/com/hermesworld/ais/galapagos/applications/ApplicationsControllerTest.java index 048df59e..ba5ab0fa 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/applications/ApplicationsControllerTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/applications/ApplicationsControllerTest.java @@ -28,7 +28,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -public class ApplicationsControllerTest { +class ApplicationsControllerTest { private final ApplicationsService applicationsService = mock(ApplicationsService.class); @@ -37,7 +37,7 @@ public class ApplicationsControllerTest { private final KafkaClusters kafkaClusters = mock(KafkaClusters.class); @Test - public void testUpdateApplicationCertificateDependentOnStageName() { + void testUpdateApplicationCertificateDependentOnStageName() { // Arrange String applicationId = "testapp-1"; String environmentId = "devtest"; @@ -70,7 +70,7 @@ public void testUpdateApplicationCertificateDependentOnStageName() { } @Test - public void testStagingWithoutSchema_include_failure() throws Exception { + void testStagingWithoutSchema_include_failure() throws Exception { TopicService topicService = mock(TopicService.class); SubscriptionService subscriptionService = mock(SubscriptionService.class); diff --git a/src/test/java/com/hermesworld/ais/galapagos/applications/controller/ApplicationsControllerTest.java b/src/test/java/com/hermesworld/ais/galapagos/applications/controller/ApplicationsControllerTest.java index defbb6d1..27f1c26b 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/applications/controller/ApplicationsControllerTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/applications/controller/ApplicationsControllerTest.java @@ -15,7 +15,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class ApplicationsControllerTest { +class ApplicationsControllerTest { private ApplicationsService applicationsService; @@ -24,14 +24,14 @@ public class ApplicationsControllerTest { private KafkaClusters kafkaClusters; @BeforeEach - public void feedMocks() { + void feedMocks() { applicationsService = mock(ApplicationsService.class); stagingService = mock(StagingService.class); kafkaClusters = mock(KafkaClusters.class); } @Test - public void getRegisteredApplications_knownAppMissing() { + void getRegisteredApplications_knownAppMissing() { ApplicationsController controller = new ApplicationsController(applicationsService, stagingService, kafkaClusters); @@ -51,7 +51,7 @@ public void getRegisteredApplications_knownAppMissing() { when(applicationsService.getKnownApplication("nex1")).thenReturn(Optional.empty()); List regApps = controller.getRegisteredApplications("test"); - assertEquals(regApps.size(), 1); + assertEquals(1, regApps.size()); assertEquals("ex1", regApps.get(0).getId()); } } diff --git a/src/test/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceImplTest.java index 7e1c4eef..388a8a2d 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceImplTest.java @@ -37,7 +37,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; -public class ApplicationsServiceImplTest { +class ApplicationsServiceImplTest { private KafkaClusters kafkaClusters; @@ -51,7 +51,7 @@ public class ApplicationsServiceImplTest { private KafkaAuthenticationModule authenticationModule; @BeforeEach - public void feedMocks() { + void feedMocks() { kafkaClusters = mock(KafkaClusters.class); when(kafkaClusters.getGlobalRepository("application-owner-requests", ApplicationOwnerRequest.class)) @@ -81,7 +81,7 @@ public void feedMocks() { } @Test - public void testRemoveOldRequests() { + void testRemoveOldRequests() { List daos = new ArrayList<>(); ZonedDateTime createdAt = ZonedDateTime.of(LocalDateTime.of(2019, 1, 1, 10, 0), ZoneId.systemDefault()); @@ -110,6 +110,14 @@ public void testRemoveOldRequests() { dao.setLastStatusChangeAt(statusChange3); dao.setState(RequestState.SUBMITTED); daos.add(dao); + dao = createRequestDao("req6", createdAt, "testuser"); + dao.setLastStatusChangeAt(statusChange1); + dao.setState(RequestState.RESIGNED); + daos.add(dao); + dao = createRequestDao("req7", createdAt, "testuser"); + dao.setLastStatusChangeAt(statusChange2); + dao.setState(RequestState.RESIGNED); + daos.add(dao); daos.forEach(requestRepository::save); @@ -120,13 +128,15 @@ public void testRemoveOldRequests() { assertTrue(requestRepository.getObject("req2").isEmpty()); assertTrue(requestRepository.getObject("req4").isEmpty()); + assertTrue(requestRepository.getObject("req6").isEmpty()); assertFalse(requestRepository.getObject("req1").isEmpty()); assertFalse(requestRepository.getObject("req3").isEmpty()); assertFalse(requestRepository.getObject("req5").isEmpty()); + assertFalse(requestRepository.getObject("req7").isEmpty()); } @Test - public void testKnownApplicationAfterSubmitting() { + void testKnownApplicationAfterSubmitting() { ZonedDateTime now = ZonedDateTime.of(LocalDateTime.of(2020, 3, 25, 10, 0), ZoneOffset.UTC); String testUserName = "test"; String appId = "42"; @@ -156,7 +166,7 @@ public void testKnownApplicationAfterSubmitting() { } @Test - public void testReplaceCertificate_appChanges() throws Exception { + void testReplaceCertificate_appChanges() throws Exception { // Application changed e.g. in external Architecture system KnownApplicationImpl app = new KnownApplicationImpl("quattro-1", "Quattro"); app.setAliases(List.of("q2")); @@ -213,7 +223,7 @@ public void testReplaceCertificate_appChanges() throws Exception { } @Test - public void testRegisterNewFiresEvent() throws Exception { + void testRegisterNewFiresEvent() throws Exception { KnownApplicationImpl app = new KnownApplicationImpl("quattro-1", "Quattro"); app.setAliases(List.of("q2")); knownApplicationRepository.save(app).get(); @@ -235,7 +245,7 @@ public void testRegisterNewFiresEvent() throws Exception { } @Test - public void testPrefix() throws Exception { + void testPrefix() throws Exception { KnownApplicationImpl app = new KnownApplicationImpl("quattro-1", "Quattro"); app.setAliases(List.of("q2")); knownApplicationRepository.save(app).get(); @@ -266,7 +276,7 @@ public void testPrefix() throws Exception { } @Test - public void testExtendCertificate() throws Exception { + void testExtendCertificate() throws Exception { // TODO move to CertificeAuthenticationModuleTest // KnownApplicationImpl app = new KnownApplicationImpl("quattro-1", "Quattro"); @@ -314,7 +324,7 @@ public void testExtendCertificate() throws Exception { } @Test - public void testUpdateAuthentication() throws Exception { + void testUpdateAuthentication() throws Exception { // WHEN an already registered application is re-registered on an environment... KnownApplicationImpl app = new KnownApplicationImpl("quattro-1", "Quattro"); knownApplicationRepository.save(app).get(); diff --git a/src/test/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceRequestStatesImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceRequestStatesImplTest.java index 3c00e6fb..f0ac3909 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceRequestStatesImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/applications/impl/ApplicationsServiceRequestStatesImplTest.java @@ -10,8 +10,8 @@ import com.hermesworld.ais.galapagos.naming.NamingService; import com.hermesworld.ais.galapagos.security.CurrentUserService; import com.hermesworld.ais.galapagos.util.TimeService; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.time.LocalDateTime; import java.time.ZoneId; @@ -23,7 +23,7 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -41,8 +41,8 @@ public class ApplicationsServiceRequestStatesImplTest { private final TopicBasedRepositoryMock repository = new TopicBasedRepositoryMock<>(); - @Before - public void feedCommonMocks() { + @BeforeEach + void feedCommonMocks() { currentUserService = mock(CurrentUserService.class); when(currentUserService.getCurrentUserName()).thenReturn(Optional.of(testUserName)); @@ -278,52 +278,58 @@ public void testFromRejectedToSubmitted() throws Exception { assertEquals(RequestState.SUBMITTED, savedRequests.get(0).getState()); } - @Test(expected = IllegalStateException.class) + @Test public void testImpossibleTransitionRevoked() throws Throwable { - ZonedDateTime now = ZonedDateTime.of(LocalDateTime.of(2020, 3, 25, 10, 0), ZoneOffset.UTC); - ApplicationOwnerRequest applicationOwnerRequest = createRequest(RequestState.REVOKED, now); - repository.save(applicationOwnerRequest); - - ApplicationsServiceImpl appsServImpl = new ApplicationsServiceImpl(kafkaEnvironments, currentUserService, - mock(TimeService.class), mock(NamingService.class), eventManager); - try { - appsServImpl.cancelUserApplicationOwnerRequest(applicationOwnerRequest.getId()).get(); - } - catch (ExecutionException e) { - throw e.getCause(); - } + assertThrows(IllegalStateException.class, () -> { + ZonedDateTime now = ZonedDateTime.of(LocalDateTime.of(2020, 3, 25, 10, 0), ZoneOffset.UTC); + ApplicationOwnerRequest applicationOwnerRequest = createRequest(RequestState.REVOKED, now); + repository.save(applicationOwnerRequest); + + ApplicationsServiceImpl appsServImpl = new ApplicationsServiceImpl(kafkaEnvironments, currentUserService, + mock(TimeService.class), mock(NamingService.class), eventManager); + try { + appsServImpl.cancelUserApplicationOwnerRequest(applicationOwnerRequest.getId()).get(); + } + catch (ExecutionException e) { + throw e.getCause(); + } + }); } - @Test(expected = IllegalStateException.class) + @Test public void testImpossibleTransitionRejected() throws Throwable { - ZonedDateTime now = ZonedDateTime.of(LocalDateTime.of(2020, 3, 25, 10, 0), ZoneOffset.UTC); - ApplicationOwnerRequest applicationOwnerRequest = createRequest(RequestState.REJECTED, now); - repository.save(applicationOwnerRequest); - - ApplicationsServiceImpl appsServImpl = new ApplicationsServiceImpl(kafkaEnvironments, currentUserService, - mock(TimeService.class), mock(NamingService.class), eventManager); - try { - appsServImpl.cancelUserApplicationOwnerRequest(applicationOwnerRequest.getId()).get(); - } - catch (ExecutionException e) { - throw e.getCause(); - } + assertThrows(IllegalStateException.class, () -> { + ZonedDateTime now = ZonedDateTime.of(LocalDateTime.of(2020, 3, 25, 10, 0), ZoneOffset.UTC); + ApplicationOwnerRequest applicationOwnerRequest = createRequest(RequestState.REJECTED, now); + repository.save(applicationOwnerRequest); + + ApplicationsServiceImpl appsServImpl = new ApplicationsServiceImpl(kafkaEnvironments, currentUserService, + mock(TimeService.class), mock(NamingService.class), eventManager); + try { + appsServImpl.cancelUserApplicationOwnerRequest(applicationOwnerRequest.getId()).get(); + } + catch (ExecutionException e) { + throw e.getCause(); + } + }); } - @Test(expected = IllegalStateException.class) + @Test public void testImpossibleTransitionResigned() throws Throwable { - ZonedDateTime now = ZonedDateTime.of(LocalDateTime.of(2020, 3, 25, 10, 0), ZoneOffset.UTC); - ApplicationOwnerRequest applicationOwnerRequest = createRequest(RequestState.RESIGNED, now); - repository.save(applicationOwnerRequest); - - ApplicationsServiceImpl appsServImpl = new ApplicationsServiceImpl(kafkaEnvironments, currentUserService, - mock(TimeService.class), mock(NamingService.class), eventManager); - try { - appsServImpl.cancelUserApplicationOwnerRequest(applicationOwnerRequest.getId()).get(); - } - catch (ExecutionException e) { - throw e.getCause(); - } + assertThrows(IllegalStateException.class, () -> { + ZonedDateTime now = ZonedDateTime.of(LocalDateTime.of(2020, 3, 25, 10, 0), ZoneOffset.UTC); + ApplicationOwnerRequest applicationOwnerRequest = createRequest(RequestState.RESIGNED, now); + repository.save(applicationOwnerRequest); + + ApplicationsServiceImpl appsServImpl = new ApplicationsServiceImpl(kafkaEnvironments, currentUserService, + mock(TimeService.class), mock(NamingService.class), eventManager); + try { + appsServImpl.cancelUserApplicationOwnerRequest(applicationOwnerRequest.getId()).get(); + } + catch (ExecutionException e) { + throw e.getCause(); + } + }); } private static ApplicationOwnerRequest createRequest(RequestState reqState, ZonedDateTime createdAt) { diff --git a/src/test/java/com/hermesworld/ais/galapagos/applications/impl/UpdateApplicationAclsListenerTest.java b/src/test/java/com/hermesworld/ais/galapagos/applications/impl/UpdateApplicationAclsListenerTest.java index 6a914f55..e88a064c 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/applications/impl/UpdateApplicationAclsListenerTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/applications/impl/UpdateApplicationAclsListenerTest.java @@ -40,7 +40,7 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -public class UpdateApplicationAclsListenerTest { +class UpdateApplicationAclsListenerTest { @Mock private KafkaClusters kafkaClusters; @@ -63,7 +63,7 @@ public class UpdateApplicationAclsListenerTest { private AclBinding dummyBinding; @BeforeEach - public void feedMocks() { + void feedMocks() { when(cluster.getId()).thenReturn("_test"); lenient().when(kafkaClusters.getEnvironment("_test")).thenReturn(Optional.of(cluster)); lenient().when(kafkaClusters.getAuthenticationModule("_test")).thenReturn(Optional.of(authenticationModule)); @@ -80,7 +80,7 @@ public void feedMocks() { } @Test - public void testUpdateApplicationAcls() throws InterruptedException, ExecutionException { + void testUpdateApplicationAcls() throws InterruptedException, ExecutionException { when(authenticationModule .extractKafkaUserName(ArgumentMatchers.argThat(obj -> obj.getString("dn").equals("CN=testapp")))) .thenReturn("User:CN=testapp"); @@ -115,7 +115,7 @@ public void testUpdateApplicationAcls() throws InterruptedException, ExecutionEx } @Test - public void testHandleTopicCreated() throws ExecutionException, InterruptedException { + void testHandleTopicCreated() throws ExecutionException, InterruptedException { GalapagosEventContext context = mock(GalapagosEventContext.class); when(context.getKafkaCluster()).thenReturn(cluster); when(cluster.getId()).thenReturn("_test"); @@ -149,7 +149,7 @@ public void testHandleTopicCreated() throws ExecutionException, InterruptedExcep } @Test - public void testHandleAddProducer() throws ExecutionException, InterruptedException { + void testHandleAddProducer() throws ExecutionException, InterruptedException { GalapagosEventContext context = mock(GalapagosEventContext.class); when(context.getKafkaCluster()).thenReturn(cluster); when(cluster.getId()).thenReturn("_test"); @@ -182,7 +182,7 @@ public void testHandleAddProducer() throws ExecutionException, InterruptedExcept } @Test - public void testHandleRemoveProducer() throws ExecutionException, InterruptedException { + void testHandleRemoveProducer() throws ExecutionException, InterruptedException { // more or less, this is same as add() - updateUserAcls for the removed producer must be called. GalapagosEventContext context = mock(GalapagosEventContext.class); when(context.getKafkaCluster()).thenReturn(cluster); @@ -211,7 +211,7 @@ public void testHandleRemoveProducer() throws ExecutionException, InterruptedExc } @Test - public void testSubscriptionCreated() throws ExecutionException, InterruptedException { + void testSubscriptionCreated() throws ExecutionException, InterruptedException { GalapagosEventContext context = mock(GalapagosEventContext.class); when(context.getKafkaCluster()).thenReturn(cluster); when(cluster.getId()).thenReturn("_test"); @@ -245,7 +245,7 @@ public void testSubscriptionCreated() throws ExecutionException, InterruptedExce } @Test - public void testNoDeleteAclsWhenUserNameIsSame() throws Exception { + void testNoDeleteAclsWhenUserNameIsSame() throws Exception { // tests that, when an AuthenticationChanged event occurs but the resulting Kafka User Name is the same, the // listener does not delete the ACLs of the user after updating them (because that would result in zero ACLs). GalapagosEventContext context = mock(GalapagosEventContext.class); @@ -276,7 +276,7 @@ public void testNoDeleteAclsWhenUserNameIsSame() throws Exception { } @Test - public void testNoDeleteAclsWhenNoPreviousUser() throws Exception { + void testNoDeleteAclsWhenNoPreviousUser() throws Exception { GalapagosEventContext context = mock(GalapagosEventContext.class); when(context.getKafkaCluster()).thenReturn(cluster); @@ -305,7 +305,7 @@ public void testNoDeleteAclsWhenNoPreviousUser() throws Exception { } @Test - public void testNoApplicationAclUpdates() throws Exception { + void testNoApplicationAclUpdates() throws Exception { // GIVEN a configuration where noUpdateApplicationAcls flag is active KafkaEnvironmentConfig config = mock(KafkaEnvironmentConfig.class); when(config.isNoUpdateApplicationAcls()).thenReturn(true); diff --git a/src/test/java/com/hermesworld/ais/galapagos/ccloud/ConfluentCloudApiClientTest.java b/src/test/java/com/hermesworld/ais/galapagos/ccloud/ConfluentCloudApiClientTest.java index a876b97e..07f0c22c 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/ccloud/ConfluentCloudApiClientTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/ccloud/ConfluentCloudApiClientTest.java @@ -29,7 +29,7 @@ import static org.junit.jupiter.api.Assertions.*; -public class ConfluentCloudApiClientTest { +class ConfluentCloudApiClientTest { private static MockWebServer mockBackEnd; @@ -47,12 +47,12 @@ static void tearDown() throws IOException { } @BeforeEach - public void init() { - baseUrl = String.format("http://localhost:%s", mockBackEnd.getPort()); + void init() { + baseUrl = "http://localhost:%s".formatted(mockBackEnd.getPort()); } @AfterEach - public void consumeRequests() throws Exception { + void consumeRequests() throws Exception { RecordedRequest request; while ((request = mockBackEnd.takeRequest(10, TimeUnit.MILLISECONDS)) != null) { System.err.println( @@ -71,7 +71,7 @@ private String readTestResource(String resourceName) throws IOException { } @Test - public void testListServiceAccounts() throws Exception { + void testListServiceAccounts() throws Exception { mockBackEnd.enqueue(new MockResponse().setBody(readTestResource("ccloud/service-accounts.json")) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); @@ -93,7 +93,7 @@ public void testListServiceAccounts() throws Exception { } @Test - public void testPagination() throws Exception { + void testPagination() throws Exception { mockBackEnd.enqueue(new MockResponse() .setBody(readTestResource("ccloud/service-accounts-page1.json").replace("${baseurl}", baseUrl)) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); @@ -128,7 +128,7 @@ public void testPagination() throws Exception { } @Test - public void testListApiKeys() throws Exception { + void testListApiKeys() throws Exception { mockBackEnd.enqueue(new MockResponse().setBody(readTestResource("ccloud/api-keys.json")) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); @@ -152,7 +152,7 @@ public void testListApiKeys() throws Exception { } @Test - public void testCreateServiceAccount() throws Exception { + void testCreateServiceAccount() throws Exception { mockBackEnd.enqueue(new MockResponse().setBody(readTestResource("ccloud/service-account.json")) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .setResponseCode(HttpStatus.CREATED.value())); @@ -178,7 +178,7 @@ public void testCreateServiceAccount() throws Exception { } @Test - public void testCreateServiceAccount_withNumericId() throws Exception { + void testCreateServiceAccount_withNumericId() throws Exception { mockBackEnd.enqueue(new MockResponse().setBody(readTestResource("ccloud/service-account.json")) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .setResponseCode(HttpStatus.CREATED.value())); @@ -203,7 +203,7 @@ public void testCreateServiceAccount_withNumericId() throws Exception { } @Test - public void testCreateApiKey() throws Exception { + void testCreateApiKey() throws Exception { mockBackEnd.enqueue(new MockResponse().setBody(readTestResource("ccloud/api-key.json")) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .setResponseCode(HttpStatus.ACCEPTED.value())); @@ -240,7 +240,7 @@ public void testCreateApiKey() throws Exception { } @Test - public void testDeleteApiKey() throws Exception { + void testDeleteApiKey() throws Exception { mockBackEnd.enqueue(new MockResponse().setResponseCode(HttpStatus.NO_CONTENT.value())); ConfluentCloudApiClient apiClient = new ConfluentCloudApiClient(baseUrl, "myKey", "mySecret", false); @@ -258,7 +258,7 @@ public void testDeleteApiKey() throws Exception { } @Test - public void testErrorStatusCode() throws Exception { + void testErrorStatusCode() throws Exception { mockBackEnd.enqueue(new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.value())); ConfluentCloudApiClient apiClient = new ConfluentCloudApiClient(baseUrl, "myKey", "mySecret", false); @@ -272,7 +272,7 @@ public void testErrorStatusCode() throws Exception { } @Test - public void testErrorMessage_singleError() throws Exception { + void testErrorMessage_singleError() throws Exception { JSONObject errorObj = new JSONObject(Map.of("error", "something went wrong")); mockBackEnd.enqueue(new MockResponse().setResponseCode(HttpStatus.BAD_REQUEST.value()) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(errorObj.toString())); @@ -288,7 +288,7 @@ public void testErrorMessage_singleError() throws Exception { } @Test - public void testErrorMessage_errorsArray() throws Exception { + void testErrorMessage_errorsArray() throws Exception { JSONObject errorObj = new JSONObject(Map.of("detail", "something went wrong")); JSONObject errorObj2 = new JSONObject(Map.of("detail", "all is broken")); JSONArray errors = new JSONArray(); @@ -310,7 +310,7 @@ public void testErrorMessage_errorsArray() throws Exception { } @Test - public void testError_textOnlyResponse() throws Exception { + void testError_textOnlyResponse() throws Exception { mockBackEnd.enqueue(new MockResponse().setResponseCode(HttpStatus.BAD_REQUEST.value()) .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE) .setBody("This is your friendly error message in text only.")); diff --git a/src/test/java/com/hermesworld/ais/galapagos/ccloud/ConfluentCloudAuthenticationModuleTest.java b/src/test/java/com/hermesworld/ais/galapagos/ccloud/ConfluentCloudAuthenticationModuleTest.java index db6df417..52b1f79c 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/ccloud/ConfluentCloudAuthenticationModuleTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/ccloud/ConfluentCloudAuthenticationModuleTest.java @@ -14,6 +14,7 @@ import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.test.util.ReflectionTestUtils; import reactor.core.publisher.Mono; @@ -27,16 +28,17 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.*; -public class ConfluentCloudAuthenticationModuleTest { +class ConfluentCloudAuthenticationModuleTest { private KafkaAuthenticationModule authenticationModule; private ConfluentCloudApiClient client; @BeforeEach - public void init() { + void init() { authenticationModule = new ConfluentCloudAuthenticationModule(basicConfig()); client = mock(ConfluentCloudApiClient.class); @@ -81,19 +83,19 @@ private String toAuthJson(Map fields) { } @Test - public void extractKafkaUserNameTest_positive() { + void extractKafkaUserNameTest_positive() { String kafkaUserName = authenticationModule.extractKafkaUserName(new JSONObject("{userId:1234}")); assertEquals("User:1234", kafkaUserName); } @Test - public void extractKafkaUserNameTest_negative() { + void extractKafkaUserNameTest_negative() { assertThrows(JSONException.class, () -> authenticationModule.extractKafkaUserName(new JSONObject("{}"))); } @Test - public void fillCorrectProps_positive() { + void fillCorrectProps_positive() { ConfluentCloudAuthConfig config = basicConfig(); config.setClusterApiSecret("secretPassword"); config.setClusterApiKey("someApiKey"); @@ -108,7 +110,7 @@ public void fillCorrectProps_positive() { } @Test - public void fillCorrectProps_negative() { + void fillCorrectProps_negative() { Properties props = new Properties(); authenticationModule.addRequiredKafkaProperties(props); @@ -117,7 +119,7 @@ public void fillCorrectProps_negative() { } @Test - public void testDeleteApplicationAuthentication_positive() throws ExecutionException, InterruptedException { + void testDeleteApplicationAuthentication_positive() throws ExecutionException, InterruptedException { ApiKeySpec apiKey1 = newApiKey("someKey1", "someSecret1", "sa-xy123"); ApiKeySpec apiKey2 = newApiKey("someKey2", "someSecret2", "sa-xy124"); @@ -131,7 +133,7 @@ public void testDeleteApplicationAuthentication_positive() throws ExecutionExcep } @Test - public void deleteApplicationAuthenticationTest_negativeNoApiKeyObjectInAuthJson() + void deleteApplicationAuthenticationTest_negativeNoApiKeyObjectInAuthJson() throws ExecutionException, InterruptedException { ApiKeySpec apiKey1 = newApiKey("someKey1", "someSecret1", "sa-xy123"); ApiKeySpec apiKey2 = newApiKey("someKey2", "someSecret2", "sa-xy124"); @@ -144,7 +146,7 @@ public void deleteApplicationAuthenticationTest_negativeNoApiKeyObjectInAuthJson } @Test - public void createApplicationAuthenticationTest_createServiceAccForAppThatHasNoAcc() + void createApplicationAuthenticationTest_createServiceAccForAppThatHasNoAcc() throws ExecutionException, InterruptedException { ApiKeySpec apiKey1 = newApiKey("someKey1", "someSecret1", "sa-xy123"); ApiKeySpec apiKey2 = newApiKey("someKey2", "someSecret2", "sa-xy124"); @@ -174,8 +176,7 @@ public void createApplicationAuthenticationTest_createServiceAccForAppThatHasNoA } @Test - public void createApplicationAuthenticationTest_reuseServiceAccIfExists() - throws ExecutionException, InterruptedException { + void createApplicationAuthenticationTest_reuseServiceAccIfExists() throws ExecutionException, InterruptedException { ApiKeySpec apiKey1 = newApiKey("someKey1", "someSecret1", "sa-xy123"); ApiKeySpec apiKey2 = newApiKey("someKey2", "someSecret2", "sa-xy124"); ApiKeySpec apiKey3 = newApiKey("someKey3", "someSecret3", "sa-xy125"); @@ -193,13 +194,13 @@ public void createApplicationAuthenticationTest_reuseServiceAccIfExists() authenticationModule.createApplicationAuthentication("quattro-1", "normalizedAppNameTest", new JSONObject()) .get(); - verify(client, times(0)).createServiceAccount(anyString(), anyString()); + verify(client, times(0)).createServiceAccount(nullable(String.class), nullable(String.class)); verify(client, times(1)).createApiKey("testEnv", "testCluster", "Application normalizedAppNameTest", "sa-xy125"); } @Test - public void createApplicationAuthenticationTest_queryNumericId() throws ExecutionException, InterruptedException { + void createApplicationAuthenticationTest_queryNumericId() throws ExecutionException, InterruptedException { useIdCompatMode(); ApiKeySpec apiKey1 = newApiKey("someKey1", "someSecret1", "sa-xy123"); @@ -222,14 +223,14 @@ public void createApplicationAuthenticationTest_queryNumericId() throws Executio assertEquals("sa-xy123", result.getPublicAuthenticationData().getString("userId")); assertEquals("12345", result.getPublicAuthenticationData().getString("numericId")); - verify(client, times(0)).createServiceAccount(anyString(), anyString()); + verify(client, times(0)).createServiceAccount(nullable(String.class), nullable(String.class)); verify(client, times(1)).createApiKey("testEnv", "testCluster", "Application normalizedAppNameTest", "sa-xy123"); verify(client, times(1)).getServiceAccountInternalIds(); } @Test - public void updateApplicationAuthenticationTest() throws ExecutionException, InterruptedException { + void updateApplicationAuthenticationTest() throws ExecutionException, InterruptedException { ApiKeySpec apiKey1 = newApiKey("someKey1", "someSecret1", "sa-xy123"); ApiKeySpec apiKey2 = newApiKey("someKey2", "someSecret2", "sa-xy124"); ApiKeySpec apiKey3 = newApiKey("someKey3", "someSecret3", "sa-xy123"); @@ -255,13 +256,13 @@ public void updateApplicationAuthenticationTest() throws ExecutionException, Int new JSONObject(auth)).get(); verify(client).deleteApiKey(apiKey1); - verify(client, times(0)).createServiceAccount(anyString(), anyString()); + verify(client, times(0)).createServiceAccount(nullable(String.class), nullable(String.class)); verify(client, times(1)).createApiKey("testEnv", "testCluster", "Application normalizedAppNameTest", "sa-xy123"); } @Test - public void testLookupNumericId_positive() { + void testLookupNumericId_positive() { useIdCompatMode(); JSONObject authData = new JSONObject(Map.of("userId", "sa-xy125", "apiKey", "ABC123")); @@ -273,7 +274,7 @@ public void testLookupNumericId_positive() { } @Test - public void testLookupNumericId_noLookup_noCompatMode() { + void testLookupNumericId_noLookup_noCompatMode() { // idCompatMode is by default false in config! JSONObject authData = new JSONObject(Map.of("userId", "sa-xy125", "apiKey", "ABC123")); Map internalIdMapping = Map.of("sa-xy123", "12399", "sa-xy125", "12345"); @@ -285,7 +286,7 @@ public void testLookupNumericId_noLookup_noCompatMode() { } @Test - public void testLookupNumericId_noLookup_numericUserId() { + void testLookupNumericId_noLookup_numericUserId() { useIdCompatMode(); JSONObject authData = new JSONObject(Map.of("userId", "12345", "apiKey", "ABC123")); @@ -298,7 +299,7 @@ public void testLookupNumericId_noLookup_numericUserId() { } @Test - public void testLookupNumericId_noLookup_explicitNumericId() { + void testLookupNumericId_noLookup_explicitNumericId() { useIdCompatMode(); JSONObject authData = new JSONObject(Map.of("userId", "sa-xy123", "apiKey", "ABC123", "numericId", "12345")); @@ -311,7 +312,7 @@ public void testLookupNumericId_noLookup_explicitNumericId() { } @Test - public void testLookupNumericId_noUseOfNumericId() { + void testLookupNumericId_noUseOfNumericId() { // idCompatMode is by default false in config! JSONObject authData = new JSONObject(Map.of("userId", "sa-xy123", "apiKey", "ABC123", "numericId", "12345")); Map internalIdMapping = Map.of("sa-xy123", "12399", "sa-xy125", "12346"); @@ -323,7 +324,7 @@ public void testLookupNumericId_noUseOfNumericId() { } @Test - public void testDevAuth_positive() throws Exception { + void testDevAuth_positive() throws Exception { Instant now = Instant.now(); // make sure that there really is slight delay Thread.sleep(100); @@ -361,7 +362,7 @@ public void testDevAuth_positive() throws Exception { } @Test - public void testDevAuth_notEnabled() throws Exception { + void testDevAuth_notEnabled() throws Exception { ServiceAccountSpec testServiceAccount = new ServiceAccountSpec(); testServiceAccount.setDisplayName("Test Display Name"); testServiceAccount.setResourceId("sa-xy123"); @@ -384,4 +385,25 @@ public void testDevAuth_notEnabled() throws Exception { } } + @Test + public void testShortenAppNames() throws Exception { + ServiceAccountSpec testServiceAccount = new ServiceAccountSpec(); + testServiceAccount.setDisplayName("Test Display Name"); + testServiceAccount.setResourceId("sa-xy123"); + testServiceAccount.setDescription("Test description"); + ApiKeySpec apiKey = newApiKey("TESTKEY", "testSecret", "sa-xy123"); + + when(client.listServiceAccounts()).thenReturn(Mono.just(List.of())); + ArgumentCaptor displayNameCaptor = ArgumentCaptor.forClass(String.class); + when(client.createServiceAccount(displayNameCaptor.capture(), anyString())) + .thenReturn(Mono.just(testServiceAccount)); + when(client.createApiKey(anyString(), anyString(), anyString(), anyString())).thenReturn(Mono.just(apiKey)); + + authenticationModule.createApplicationAuthentication("app-1", + "A_very_long_strange_application_name_which_likely_exceeds_50_characters_whoever_names_such_applications", + new JSONObject()).get(); + + assertTrue(displayNameCaptor.getValue().length() <= 64); + } + } diff --git a/src/test/java/com/hermesworld/ais/galapagos/certificates/impl/CaManagerImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/certificates/impl/CaManagerImplTest.java index 5fd9d659..f2c4771e 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/certificates/impl/CaManagerImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/certificates/impl/CaManagerImplTest.java @@ -16,7 +16,7 @@ import static org.junit.jupiter.api.Assertions.*; -public class CaManagerImplTest { +class CaManagerImplTest { private final static String testAppId = "four"; @@ -27,13 +27,13 @@ public class CaManagerImplTest { private CertificatesAuthenticationConfig authConfig; @BeforeEach - public void init() { + void init() { Security.addProvider(new BouncyCastleProvider()); authConfig = mockAuthConfig(); } @Test - public void testCreateApplicationFromCsrWithValidCn() throws Exception { + void testCreateApplicationFromCsrWithValidCn() throws Exception { String testCsrData = StreamUtils.copyToString( new ClassPathResource("/certificates/test_quattroValidCn.csr").getInputStream(), StandardCharsets.UTF_8); @@ -48,7 +48,7 @@ public void testCreateApplicationFromCsrWithValidCn() throws Exception { } @Test - public void testCreateApplicationFromCsrWithInvalidCn() throws Exception { + void testCreateApplicationFromCsrWithInvalidCn() throws Exception { String testCsrData = StreamUtils.copyToString( new ClassPathResource("/certificates/test_quattroInvalidCn.csr").getInputStream(), StandardCharsets.UTF_8); @@ -64,7 +64,7 @@ public void testCreateApplicationFromCsrWithInvalidCn() throws Exception { } @Test - public void testCreateApplicationFromCsrWithInvalidAppId() throws Exception { + void testCreateApplicationFromCsrWithInvalidAppId() throws Exception { String testCsrData = StreamUtils.copyToString( new ClassPathResource("/certificates/test_quattroInvalidAppId.csr").getInputStream(), StandardCharsets.UTF_8); @@ -80,7 +80,7 @@ public void testCreateApplicationFromCsrWithInvalidAppId() throws Exception { } @Test - public void testCreateApplicationFromInvalidCsr() throws Exception { + void testCreateApplicationFromInvalidCsr() throws Exception { CaManagerImpl testCaManagerImpl = new CaManagerImpl("test", authConfig); try { testCaManagerImpl.createApplicationCertificateFromCsr(testAppId, "testCsrData", testAppName).get(); @@ -92,7 +92,7 @@ public void testCreateApplicationFromInvalidCsr() throws Exception { } @Test - public void testExtendCertificate() throws Exception { + void testExtendCertificate() throws Exception { CaManagerImpl testCaManagerImpl = new CaManagerImpl("test", authConfig); String testCsrData = StreamUtils.copyToString( @@ -108,7 +108,7 @@ public void testExtendCertificate() throws Exception { } @Test - public void testExtendCertificate_wrongDn() throws Exception { + void testExtendCertificate_wrongDn() throws Exception { CaManagerImpl testCaManagerImpl = new CaManagerImpl("test", authConfig); String testCsrData = StreamUtils.copyToString( diff --git a/src/test/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderRunnerIntegrationTest.java b/src/test/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderRunnerIntegrationTest.java index 2217bef5..b53748cd 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderRunnerIntegrationTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderRunnerIntegrationTest.java @@ -1,5 +1,6 @@ package com.hermesworld.ais.galapagos.certificates.reminders.impl; +import com.hermesworld.ais.galapagos.GalapagosTestConfig; import com.hermesworld.ais.galapagos.applications.ApplicationMetadata; import com.hermesworld.ais.galapagos.applications.ApplicationOwnerRequest; import com.hermesworld.ais.galapagos.applications.ApplicationsService; @@ -10,25 +11,24 @@ import com.hermesworld.ais.galapagos.certificates.reminders.CertificateExpiryReminderService; import com.hermesworld.ais.galapagos.certificates.reminders.ReminderType; import com.hermesworld.ais.galapagos.kafka.KafkaClusters; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.mail.MailHealthContributorAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.test.context.junit4.SpringRunner; -import javax.mail.MessagingException; -import javax.mail.internet.MimeMessage; import java.io.ByteArrayInputStream; import java.util.*; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; @@ -37,10 +37,10 @@ * This test mainly focuses on the "notification integration" part, i.e., that the mail templates do render correctly * and the correct e-mail recipients are determined and passed to the mail engine. */ -@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) @EnableAutoConfiguration(exclude = { MailHealthContributorAutoConfiguration.class }) -public class CertificateExpiryReminderRunnerIntegrationTest { +@Import(GalapagosTestConfig.class) +class CertificateExpiryReminderRunnerIntegrationTest { @MockBean private KafkaClusters kafkaClusters; @@ -54,13 +54,14 @@ public class CertificateExpiryReminderRunnerIntegrationTest { @MockBean private JavaMailSender mailSender; + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") @Autowired private CertificateExpiryReminderRunner runner; private final List sentMessages = new ArrayList<>(); - @Before - public void initMocks() throws MessagingException { + @BeforeEach + void initMocks() throws MessagingException { when(kafkaClusters.getEnvironmentIds()).thenReturn(List.of("test")); doAnswer(inv -> sentMessages.add(inv.getArgument(0))).when(mailSender).send((MimeMessage) any()); @@ -83,7 +84,7 @@ public void initMocks() throws MessagingException { } @Test - public void testSendNotification_threeMonths_success() throws Exception { + void testSendNotification_threeMonths_success() throws Exception { CertificateExpiryReminder reminder = new CertificateExpiryReminder("123", "test", ReminderType.THREE_MONTHS); when(reminderService.calculateDueCertificateReminders()).thenReturn(List.of(reminder)); @@ -95,7 +96,7 @@ public void testSendNotification_threeMonths_success() throws Exception { } @Test - public void testSendNotification_oneMonth_success() throws Exception { + void testSendNotification_oneMonth_success() throws Exception { CertificateExpiryReminder reminder = new CertificateExpiryReminder("123", "test", ReminderType.ONE_MONTH); when(reminderService.calculateDueCertificateReminders()).thenReturn(List.of(reminder)); @@ -107,7 +108,7 @@ public void testSendNotification_oneMonth_success() throws Exception { } @Test - public void testSendNotification_oneWeek_success() throws Exception { + void testSendNotification_oneWeek_success() throws Exception { CertificateExpiryReminder reminder = new CertificateExpiryReminder("123", "test", ReminderType.ONE_WEEK); when(reminderService.calculateDueCertificateReminders()).thenReturn(List.of(reminder)); diff --git a/src/test/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderServiceTest.java b/src/test/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderServiceTest.java index 5e383706..96584f85 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderServiceTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/certificates/reminders/impl/CertificateExpiryReminderServiceTest.java @@ -11,19 +11,18 @@ import com.hermesworld.ais.galapagos.kafka.config.KafkaEnvironmentConfig; import com.hermesworld.ais.galapagos.kafka.impl.TopicBasedRepositoryMock; import org.json.JSONObject; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.*; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class CertificateExpiryReminderServiceTest { +class CertificateExpiryReminderServiceTest { private KafkaClusters clusters; @@ -33,8 +32,8 @@ public class CertificateExpiryReminderServiceTest { private final TopicBasedRepositoryMock reminderRepository = new TopicBasedRepositoryMock<>(); - @Before - public void initClusters() { + @BeforeEach + void initClusters() { clusters = mock(KafkaClusters.class); KafkaCluster cluster = mock(KafkaCluster.class); applicationsService = mock(ApplicationsService.class); @@ -57,7 +56,7 @@ public void initClusters() { } @Test - public void testNoReminder() { + void testNoReminder() { ApplicationMetadata metadata = new ApplicationMetadata(); metadata.setApplicationId("123"); metadata.setAuthenticationJson(authJson("CN=abc", 365)); @@ -69,7 +68,7 @@ public void testNoReminder() { } @Test - public void testSimpleCase() { + void testSimpleCase() { ApplicationMetadata metadata = new ApplicationMetadata(); metadata.setApplicationId("123"); metadata.setAuthenticationJson(authJson("CN=abc", 40)); @@ -80,11 +79,11 @@ public void testSimpleCase() { assertEquals(1, reminders.size()); assertEquals("123", reminders.get(0).getApplicationId()); assertEquals("test", reminders.get(0).getEnvironmentId()); - Assert.assertEquals(ReminderType.THREE_MONTHS, reminders.get(0).getReminderType()); + assertEquals(ReminderType.THREE_MONTHS, reminders.get(0).getReminderType()); } @Test - public void testMultipleCallsWithoutMarkMustReturnSameReminders() { + void testMultipleCallsWithoutMarkMustReturnSameReminders() { ApplicationMetadata metadata = new ApplicationMetadata(); metadata.setApplicationId("123"); metadata.setAuthenticationJson(authJson("CN=abc", 40)); @@ -100,7 +99,7 @@ public void testMultipleCallsWithoutMarkMustReturnSameReminders() { } @Test - public void testSimpleMark() { + void testSimpleMark() { List applications = new ArrayList<>(); ApplicationMetadata metadata = new ApplicationMetadata(); metadata.setApplicationId("123"); @@ -126,7 +125,7 @@ public void testSimpleMark() { } @Test - public void testShortTimeAlreadySentShouldNotSendLongerTimeReminder() throws Exception { + void testShortTimeAlreadySentShouldNotSendLongerTimeReminder() throws Exception { List applications = new ArrayList<>(); ApplicationMetadata metadata = new ApplicationMetadata(); metadata.setApplicationId("123"); @@ -148,7 +147,7 @@ public void testShortTimeAlreadySentShouldNotSendLongerTimeReminder() throws Exc } @Test - public void testMultipleEnvironmentsWithExpiredEach() { + void testMultipleEnvironmentsWithExpiredEach() { ApplicationMetadata metadata = new ApplicationMetadata(); metadata.setApplicationId("123"); metadata.setAuthenticationJson(authJson("CN=abc", 5)); @@ -189,7 +188,7 @@ public void testMultipleEnvironmentsWithExpiredEach() { } @Test - public void testMultipleEnvironmentsWithOnlyOneExpired() { + void testMultipleEnvironmentsWithOnlyOneExpired() { List applications = new ArrayList<>(); ApplicationMetadata metadata = new ApplicationMetadata(); metadata.setApplicationId("123"); diff --git a/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangeBaseTest.java b/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangeBaseTest.java index 5ee1ef97..6193db03 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangeBaseTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangeBaseTest.java @@ -11,14 +11,16 @@ import com.hermesworld.ais.galapagos.subscriptions.SubscriptionMetadata; import com.hermesworld.ais.galapagos.subscriptions.SubscriptionState; import com.hermesworld.ais.galapagos.subscriptions.service.SubscriptionService; -import static org.junit.Assert.*; -import org.junit.Test; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import org.springframework.beans.BeanUtils; -public class ChangeBaseTest { +class ChangeBaseTest { /** * Tests that each private field in all of the subclasses of ChangeBase have a Getter for all of their private @@ -26,7 +28,7 @@ public class ChangeBaseTest { * use of Reflection! */ @Test - public void testGettersForFields() { + void testGettersForFields() { String packageName = ChangeBase.class.getPackageName(); ClassLoader cl = ChangeBaseTest.class.getClassLoader(); @@ -67,7 +69,7 @@ private void assertGettersForFields(Class clazz) { } @Test - public void testStageSubscription() throws Exception { + void testStageSubscription() throws Exception { SubscriptionMetadata sub1 = new SubscriptionMetadata(); sub1.setId("123"); sub1.setClientApplicationId("app-1"); diff --git a/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangeDeserializerTest.java b/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangeDeserializerTest.java index 683f1b61..ce269f12 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangeDeserializerTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangeDeserializerTest.java @@ -8,18 +8,18 @@ import com.hermesworld.ais.galapagos.topics.TopicMetadata; import com.hermesworld.ais.galapagos.topics.TopicType; import com.hermesworld.ais.galapagos.util.JsonUtil; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.List; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class ChangeDeserializerTest { +class ChangeDeserializerTest { @Test - public void testCompoundChangeDeser() throws Exception { + void testCompoundChangeDeser() throws Exception { // ChangesDeserializer is registered in the ObjectMapper by this method ObjectMapper mapper = JsonUtil.newObjectMapper(); diff --git a/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangesServiceImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangesServiceImplTest.java index bceba3d8..234c56eb 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangesServiceImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/changes/impl/ChangesServiceImplTest.java @@ -22,14 +22,15 @@ import com.hermesworld.ais.galapagos.topics.TopicMetadata; import com.hermesworld.ais.galapagos.topics.TopicType; import com.hermesworld.ais.galapagos.util.HasKey; -import static org.junit.Assert.assertEquals; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class ChangesServiceImplTest { +class ChangesServiceImplTest { private GalapagosEventContext context; @@ -37,8 +38,8 @@ public class ChangesServiceImplTest { private AuditPrincipal principal; - @Before - public void buildMocks() { + @BeforeEach + void buildMocks() { KafkaClusters clusters = mock(KafkaClusters.class); impl = new ChangesServiceImpl(clusters); @@ -52,7 +53,7 @@ public void buildMocks() { } @Test - public void testChangeListener_createTopic() { + void testChangeListener_createTopic() { TopicMetadata metadata = buildTopicMetadata(); TopicCreateParams params = new TopicCreateParams(2, 2); @@ -68,7 +69,7 @@ public void testChangeListener_createTopic() { } @Test - public void testChangeListener_deleteTopic() { + void testChangeListener_deleteTopic() { TopicMetadata metadata = buildTopicMetadata(); TopicEvent event = new TopicEvent(context, metadata); impl.handleTopicDeleted(event); @@ -82,7 +83,7 @@ public void testChangeListener_deleteTopic() { } @Test - public void testChangeListener_topicDescriptionChanged() { + void testChangeListener_topicDescriptionChanged() { TopicMetadata metadata = buildTopicMetadata(); TopicEvent event = new TopicEvent(context, metadata); impl.handleTopicDescriptionChanged(event); @@ -96,7 +97,7 @@ public void testChangeListener_topicDescriptionChanged() { } @Test - public void testChangeListener_topicDeprecated() { + void testChangeListener_topicDeprecated() { TopicMetadata metadata = buildTopicMetadata(); metadata.setDeprecated(true); metadata.setDeprecationText("do not use"); @@ -114,7 +115,7 @@ public void testChangeListener_topicDeprecated() { } @Test - public void testChangeListener_topicUndeprecated() { + void testChangeListener_topicUndeprecated() { TopicMetadata metadata = buildTopicMetadata(); TopicEvent event = new TopicEvent(context, metadata); impl.handleTopicUndeprecated(event); @@ -128,7 +129,7 @@ public void testChangeListener_topicUndeprecated() { } @Test - public void testChangeListener_topicSchemaVersionPublished() { + void testChangeListener_topicSchemaVersionPublished() { TopicMetadata metadata = buildTopicMetadata(); SchemaMetadata schema = new SchemaMetadata(); schema.setId("99"); diff --git a/src/test/java/com/hermesworld/ais/galapagos/devauth/impl/DevUserAclListenerTest.java b/src/test/java/com/hermesworld/ais/galapagos/devauth/impl/DevUserAclListenerTest.java index 2cb341b5..87f36f12 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/devauth/impl/DevUserAclListenerTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/devauth/impl/DevUserAclListenerTest.java @@ -18,7 +18,6 @@ import com.hermesworld.ais.galapagos.subscriptions.service.SubscriptionService; import com.hermesworld.ais.galapagos.util.FutureUtil; import com.hermesworld.ais.galapagos.util.TimeService; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -33,10 +32,11 @@ import java.util.Optional; import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -public class DevUserAclListenerTest { +class DevUserAclListenerTest { @Mock private ApplicationsService applicationsService; @@ -58,7 +58,7 @@ public class DevUserAclListenerTest { private ZonedDateTime timestamp; @BeforeEach - public void initMocks() { + void initMocks() { MockitoAnnotations.openMocks(this); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -80,7 +80,7 @@ public void initMocks() { } @Test - public void testApplicationRegistered_invalidCertificate() throws Exception { + void testApplicationRegistered_invalidCertificate() throws Exception { DevAuthenticationMetadata devAuth = new DevAuthenticationMetadata(); devAuth.setUserName("testuser"); devAuth.setAuthenticationJson("{\"expiresAt\":\"2017-02-03T10:37:30Z\"}"); @@ -106,7 +106,7 @@ public void testApplicationRegistered_invalidCertificate() throws Exception { } @Test - public void testApplicationRegistered() throws Exception { + void testApplicationRegistered() throws Exception { DevAuthenticationMetadata devAuth = new DevAuthenticationMetadata(); devAuth.setUserName("testuser"); devAuth.setAuthenticationJson( @@ -138,11 +138,11 @@ public void testApplicationRegistered() throws Exception { verify(cluster, times(1)).updateUserAcls(userCaptor.capture()); KafkaUser user = userCaptor.getValue(); - Assertions.assertEquals("User:CN=testuser", user.getKafkaUserName()); + assertEquals("User:CN=testuser", user.getKafkaUserName()); } @Test - public void testWriteAccessFlag() throws Exception { + void testWriteAccessFlag() throws Exception { KafkaEnvironmentConfig config = mock(KafkaEnvironmentConfig.class); when(config.isDeveloperWriteAccess()).thenReturn(true); when(cluster.getId()).thenReturn("test"); diff --git a/src/test/java/com/hermesworld/ais/galapagos/devauth/impl/DeveloperAuthenticationServiceImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/devauth/impl/DeveloperAuthenticationServiceImplTest.java index ae4833f4..b37bb508 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/devauth/impl/DeveloperAuthenticationServiceImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/devauth/impl/DeveloperAuthenticationServiceImplTest.java @@ -32,7 +32,7 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) -public class DeveloperAuthenticationServiceImplTest { +class DeveloperAuthenticationServiceImplTest { @Mock private KafkaClusters kafkaClusters; @@ -61,7 +61,7 @@ public class DeveloperAuthenticationServiceImplTest { private final TopicBasedRepositoryMock metaRepo = new TopicBasedRepositoryMock<>(); @Test - public void testCreateDeveloperAuthentication_positive() throws Exception { + void testCreateDeveloperAuthentication_positive() throws Exception { // given when(kafkaClusters.getEnvironment("test")).thenReturn(Optional.of(testCluster)); when(testCluster.getRepository("devauth", DevAuthenticationMetadata.class)).thenReturn(metaRepo); @@ -98,7 +98,7 @@ public void testCreateDeveloperAuthentication_positive() throws Exception { } @Test - public void testCreateDeveloperAuthentication_noUser() { + void testCreateDeveloperAuthentication_noUser() { // given when(userService.getCurrentUserName()).thenReturn(Optional.empty()); @@ -113,7 +113,7 @@ public void testCreateDeveloperAuthentication_noUser() { } @Test - public void testCreateDeveloperAuthentication_invalidEnvironmentId() { + void testCreateDeveloperAuthentication_invalidEnvironmentId() { // given lenient().when(kafkaClusters.getEnvironment("test")).thenReturn(Optional.of(testCluster)); lenient().when(testCluster.getRepository("devauth", DevAuthenticationMetadata.class)).thenReturn(metaRepo); @@ -131,7 +131,7 @@ public void testCreateDeveloperAuthentication_invalidEnvironmentId() { } @Test - public void testCreateDeveloperAuthentication_deletePreviousAuth() throws Exception { + void testCreateDeveloperAuthentication_deletePreviousAuth() throws Exception { // given when(kafkaClusters.getEnvironment("test")).thenReturn(Optional.of(testCluster)); when(testCluster.getRepository("devauth", DevAuthenticationMetadata.class)).thenReturn(metaRepo); @@ -175,7 +175,7 @@ public void testCreateDeveloperAuthentication_deletePreviousAuth() throws Except } @Test - public void testGetDeveloperAuthenticationForCurrentUser_positive() throws Exception { + void testGetDeveloperAuthenticationForCurrentUser_positive() throws Exception { // given when(kafkaClusters.getEnvironment("test")).thenReturn(Optional.of(testCluster)); when(testCluster.getRepository("devauth", DevAuthenticationMetadata.class)).thenReturn(metaRepo); @@ -204,7 +204,7 @@ public void testGetDeveloperAuthenticationForCurrentUser_positive() throws Excep } @Test - public void testGetDeveloperAuthenticationForCurrentUser_wrongUserName() throws Exception { + void testGetDeveloperAuthenticationForCurrentUser_wrongUserName() throws Exception { // given when(kafkaClusters.getEnvironment("test")).thenReturn(Optional.of(testCluster)); when(testCluster.getRepository("devauth", DevAuthenticationMetadata.class)).thenReturn(metaRepo); @@ -229,7 +229,7 @@ public void testGetDeveloperAuthenticationForCurrentUser_wrongUserName() throws } @Test - public void testGetDeveloperAuthenticationForCurrentUser_noCurrentUser() throws Exception { + void testGetDeveloperAuthenticationForCurrentUser_noCurrentUser() throws Exception { // given when(kafkaClusters.getEnvironment("test")).thenReturn(Optional.of(testCluster)); @@ -255,7 +255,7 @@ public void testGetDeveloperAuthenticationForCurrentUser_noCurrentUser() throws } @Test - public void testGetDeveloperAuthenticationForCurrentUser_expired() throws Exception { + void testGetDeveloperAuthenticationForCurrentUser_expired() throws Exception { // given when(kafkaClusters.getEnvironment("test")).thenReturn(Optional.of(testCluster)); when(testCluster.getRepository("devauth", DevAuthenticationMetadata.class)).thenReturn(metaRepo); @@ -283,7 +283,7 @@ public void testGetDeveloperAuthenticationForCurrentUser_expired() throws Except } @Test - public void testClearExpiredDeveloperAuthenticationsOnAllClusters_positive() throws Exception { + void testClearExpiredDeveloperAuthenticationsOnAllClusters_positive() throws Exception { // given KafkaCluster cluster2 = mock(KafkaCluster.class); TopicBasedRepositoryMock metaRepo2 = new TopicBasedRepositoryMock<>(); diff --git a/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaClusterTest.java b/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaClusterTest.java index 26843a08..f0d8e97c 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaClusterTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/ConnectedKafkaClusterTest.java @@ -1,8 +1,6 @@ package com.hermesworld.ais.galapagos.kafka.impl; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import java.util.ArrayList; @@ -21,15 +19,14 @@ import org.apache.kafka.common.resource.PatternType; import org.apache.kafka.common.resource.ResourcePattern; import org.apache.kafka.common.resource.ResourceType; -import org.junit.Test; - +import org.junit.jupiter.api.Test; import com.hermesworld.ais.galapagos.kafka.KafkaExecutorFactory; import com.hermesworld.ais.galapagos.kafka.KafkaUser; -public class ConnectedKafkaClusterTest { +class ConnectedKafkaClusterTest { @Test - public void testUpdateAcls() throws Exception { + void testUpdateAcls() throws Exception { List deletedAcls = new ArrayList<>(); List createdAcls = new ArrayList<>(); diff --git a/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaFutureDecouplerTest.java b/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaFutureDecouplerTest.java index 3b6e8226..7736f20f 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaFutureDecouplerTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaFutureDecouplerTest.java @@ -1,9 +1,6 @@ package com.hermesworld.ais.galapagos.kafka.impl; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.*; import java.util.ArrayList; import java.util.HashSet; @@ -25,9 +22,9 @@ import org.apache.kafka.common.resource.PatternType; import org.apache.kafka.common.resource.ResourcePattern; import org.apache.kafka.common.resource.ResourceType; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.kafka.KafkaException; import org.springframework.util.concurrent.FailureCallback; import org.springframework.util.concurrent.ListenableFuture; @@ -36,7 +33,7 @@ import com.hermesworld.ais.galapagos.kafka.KafkaExecutorFactory; -public class KafkaFutureDecouplerTest { +class KafkaFutureDecouplerTest { private static ThreadFactory tfAdminClient = new ThreadFactory() { @Override @@ -58,19 +55,19 @@ public Thread newThread(Runnable r) { private AdminClientStub adminClient; - @Before - public void initAdminClient() { + @BeforeEach + void initAdminClient() { adminClient = new AdminClientStub(); adminClient.setKafkaThreadFactory(tfAdminClient); } - @After - public void closeAdminClient() { + @AfterEach + void closeAdminClient() { adminClient.close(); } @Test - public void testDecoupling_kafkaFuture() throws Exception { + void testDecoupling_kafkaFuture() throws Exception { // first, test that the futures usually would complete on our Threads AtomicBoolean onAdminClientThread = new AtomicBoolean(); adminClient.describeCluster().nodes().thenApply(c -> { @@ -93,7 +90,7 @@ public void testDecoupling_kafkaFuture() throws Exception { } @Test - public void testDecoupling_concatenation() throws Exception { + void testDecoupling_concatenation() throws Exception { List threadNames = new ArrayList(); KafkaFutureDecoupler decoupler = new KafkaFutureDecoupler(executorFactory); @@ -115,11 +112,11 @@ public void testDecoupling_concatenation() throws Exception { } // must be two different Threads! - assertTrue(new HashSet<>(threadNames).size() == 2); + assertEquals(2, new HashSet<>(threadNames).size()); } @Test - public void testDecoupling_listenableFuture() throws Exception { + void testDecoupling_listenableFuture() throws Exception { AtomicBoolean onAdminClientThread = new AtomicBoolean(); // after decoupling, future should complete on another Thread @@ -136,7 +133,7 @@ public void testDecoupling_listenableFuture() throws Exception { } @Test - public void testDecoupling_doneFuture() throws Exception { + void testDecoupling_doneFuture() throws Exception { AtomicInteger factoryInvocations = new AtomicInteger(); KafkaExecutorFactory countingExecutorFactory = () -> { @@ -157,7 +154,7 @@ public void testDecoupling_doneFuture() throws Exception { } @Test - public void testDecoupling_failingFuture() throws Exception { + void testDecoupling_failingFuture() throws Exception { AtomicInteger factoryInvocations = new AtomicInteger(); KafkaExecutorFactory countingExecutorFactory = () -> { @@ -184,7 +181,7 @@ public void testDecoupling_failingFuture() throws Exception { } @Test - public void testDecoupling_failedFuture_direct() throws Exception { + void testDecoupling_failedFuture_direct() throws Exception { AtomicInteger factoryInvocations = new AtomicInteger(); KafkaExecutorFactory countingExecutorFactory = () -> { diff --git a/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaSenderImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaSenderImplTest.java index 7ba673f9..a445ea93 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaSenderImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/KafkaSenderImplTest.java @@ -1,6 +1,7 @@ package com.hermesworld.ais.galapagos.kafka.impl; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -13,13 +14,13 @@ import org.apache.kafka.clients.producer.Producer; import org.apache.kafka.clients.producer.RecordMetadata; import org.apache.kafka.common.TopicPartition; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.core.ProducerFactory; import com.hermesworld.ais.galapagos.kafka.KafkaExecutorFactory; -public class KafkaSenderImplTest { +class KafkaSenderImplTest { private static ThreadFactory tfDecoupled = new ThreadFactory() { @Override @@ -33,7 +34,7 @@ public Thread newThread(Runnable r) { }; @Test - public void testSendDecoupling() throws Exception { + void testSendDecoupling() throws Exception { KafkaFutureDecoupler decoupler = new KafkaFutureDecoupler(executorFactory); @SuppressWarnings("unchecked") diff --git a/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/TopicBasedRepositoryImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/TopicBasedRepositoryImplTest.java index 12df0072..436a5e50 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/TopicBasedRepositoryImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/kafka/impl/TopicBasedRepositoryImplTest.java @@ -5,8 +5,8 @@ import com.hermesworld.ais.galapagos.util.FutureUtil; import com.hermesworld.ais.galapagos.util.JsonUtil; import org.json.JSONObject; -import org.junit.After; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import java.time.Duration; import java.util.Map; @@ -16,25 +16,25 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class TopicBasedRepositoryImplTest { +class TopicBasedRepositoryImplTest { private final KafkaSender sender = mock(KafkaSender.class); private final ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1); - @After - public void shutdownExecutor() throws Exception { + @AfterEach + void shutdownExecutor() throws Exception { executorService.shutdown(); executorService.awaitTermination(1, TimeUnit.SECONDS); } @Test - public void testMessageReceived() throws Exception { + void testMessageReceived() throws Exception { TopicBasedRepositoryImpl repository = new TopicBasedRepositoryImpl<>("galapagos.testtopic", "testtopic", ApplicationMetadata.class, sender); assertEquals(ApplicationMetadata.class, repository.getValueClass()); @@ -54,7 +54,7 @@ public void testMessageReceived() throws Exception { } @Test - public void testMessageReceived_wrongTopic() throws Exception { + void testMessageReceived_wrongTopic() throws Exception { TopicBasedRepositoryImpl repository = new TopicBasedRepositoryImpl<>("galapagos.testtopic", "testtopic", ApplicationMetadata.class, sender); @@ -70,7 +70,7 @@ public void testMessageReceived_wrongTopic() throws Exception { } @Test - public void testWaitForInitialization_emptyRepository() throws Exception { + void testWaitForInitialization_emptyRepository() throws Exception { TopicBasedRepositoryImpl repository = new TopicBasedRepositoryImpl<>("galapagos.testtopic", "testtopic", ApplicationMetadata.class, sender); @@ -79,13 +79,13 @@ public void testWaitForInitialization_emptyRepository() throws Exception { Duration.ofMillis(100), executorService); future.get(); - assertTrue("Implementation only waited " + (System.currentTimeMillis() - startTime) + " ms instead of 300 ms", - System.currentTimeMillis() >= startTime + 300); + assertTrue(System.currentTimeMillis() >= startTime + 300, + "Implementation only waited " + (System.currentTimeMillis() - startTime) + " ms instead of 300 ms"); assertFalse(System.currentTimeMillis() >= startTime + 1000); } @Test - public void testWaitForInitialization_positive() throws Exception { + void testWaitForInitialization_positive() throws Exception { TopicBasedRepositoryImpl repository = new TopicBasedRepositoryImpl<>("galapagos.testtopic", "testtopic", ApplicationMetadata.class, sender); @@ -99,13 +99,13 @@ public void testWaitForInitialization_positive() throws Exception { future.get(); - assertTrue("Implementation only waited " + (System.currentTimeMillis() - startTime) + " ms instead of 350 ms", - System.currentTimeMillis() >= startTime + 350); + assertTrue(System.currentTimeMillis() >= startTime + 350, + "Implementation only waited " + (System.currentTimeMillis() - startTime) + " ms instead of 350 ms"); assertFalse(System.currentTimeMillis() >= startTime + 1000); } @Test - public void testWaitForInitialization_tooLateDataStart() throws Exception { + void testWaitForInitialization_tooLateDataStart() throws Exception { TopicBasedRepositoryImpl repository = new TopicBasedRepositoryImpl<>("galapagos.testtopic", "testtopic", ApplicationMetadata.class, sender); @@ -119,11 +119,11 @@ public void testWaitForInitialization_tooLateDataStart() throws Exception { future.get(); - assertFalse("Repository waited too long", System.currentTimeMillis() >= startTime + 349); + assertFalse(System.currentTimeMillis() >= startTime + 349, "Repository waited too long"); } @Test - public void testWaitForInitialization_tooLateData() throws Exception { + void testWaitForInitialization_tooLateData() throws Exception { TopicBasedRepositoryImpl repository = new TopicBasedRepositoryImpl<>("galapagos.testtopic", "testtopic", ApplicationMetadata.class, sender); @@ -139,20 +139,20 @@ public void testWaitForInitialization_tooLateData() throws Exception { future.get(); - assertTrue("Implementation only waited " + (System.currentTimeMillis() - startTime) + " ms instead of 350 ms", - System.currentTimeMillis() >= startTime + 350); - assertFalse("Repository waited too long", System.currentTimeMillis() >= startTime + 399); + assertTrue(System.currentTimeMillis() >= startTime + 350, + "Implementation only waited " + (System.currentTimeMillis() - startTime) + " ms instead of 350 ms"); + assertFalse(System.currentTimeMillis() >= startTime + 399, "Repository waited too long"); } @Test - public void testGetTopicName() { + void testGetTopicName() { TopicBasedRepositoryImpl repository = new TopicBasedRepositoryImpl<>("galapagos.testtopic", "testtopic", ApplicationMetadata.class, sender); assertEquals("testtopic", repository.getTopicName()); } @Test - public void testSave() throws Exception { + void testSave() throws Exception { TopicBasedRepositoryImpl repository = new TopicBasedRepositoryImpl<>("galapagos.testtopic", "testtopic", ApplicationMetadata.class, sender); @@ -182,7 +182,7 @@ public void testSave() throws Exception { } @Test - public void testDelete() throws Exception { + void testDelete() throws Exception { TopicBasedRepositoryImpl repository = new TopicBasedRepositoryImpl<>("galapagos.testtopic", "testtopic", ApplicationMetadata.class, sender); diff --git a/src/test/java/com/hermesworld/ais/galapagos/kafka/util/AclSupportTest.java b/src/test/java/com/hermesworld/ais/galapagos/kafka/util/AclSupportTest.java index c82eccfe..db7dbf6c 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/kafka/util/AclSupportTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/kafka/util/AclSupportTest.java @@ -8,10 +8,12 @@ import com.hermesworld.ais.galapagos.topics.TopicMetadata; import com.hermesworld.ais.galapagos.topics.TopicType; import com.hermesworld.ais.galapagos.topics.service.TopicService; +import org.apache.kafka.common.acl.AccessControlEntry; import org.apache.kafka.common.acl.AclBinding; import org.apache.kafka.common.acl.AclOperation; import org.apache.kafka.common.acl.AclPermissionType; import org.apache.kafka.common.resource.PatternType; +import org.apache.kafka.common.resource.ResourcePattern; import org.apache.kafka.common.resource.ResourceType; import org.json.JSONObject; import org.junit.jupiter.api.BeforeEach; @@ -27,13 +29,15 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -public class AclSupportTest { +class AclSupportTest { - private static final List WRITE_TOPIC_OPERATIONS = Arrays.asList(AclOperation.DESCRIBE, - AclOperation.DESCRIBE_CONFIGS, AclOperation.READ, AclOperation.WRITE); + private static final List WRITE_TOPIC_OPERATIONS = Arrays.asList( + new AclOperationAndType(AclOperation.ALL, AclPermissionType.ALLOW), + new AclOperationAndType(AclOperation.DELETE, AclPermissionType.DENY)); - private static final List READ_TOPIC_OPERATIONS = Arrays.asList(AclOperation.DESCRIBE, - AclOperation.DESCRIBE_CONFIGS, AclOperation.READ); + private static final List READ_TOPIC_OPERATIONS = Arrays.asList( + new AclOperationAndType(AclOperation.READ, AclPermissionType.ALLOW), + new AclOperationAndType(AclOperation.DESCRIBE_CONFIGS, AclPermissionType.ALLOW)); @Mock private KafkaEnvironmentsConfig kafkaConfig; @@ -45,12 +49,12 @@ public class AclSupportTest { private SubscriptionService subscriptionService; @BeforeEach - public void initMocks() { + void initMocks() { } @Test - public void testGetRequiredAclBindings_simple() { + void testGetRequiredAclBindings_simple() { ApplicationMetadata metadata = new ApplicationMetadata(); metadata.setApplicationId("app01"); metadata.setConsumerGroupPrefixes(List.of("group.myapp.", "group2.myapp.")); @@ -77,34 +81,13 @@ public void testGetRequiredAclBindings_simple() { when(topicService.listTopics("_test")).thenReturn(List.of(topic1, topic2)); when(topicService.getTopic("_test", "topic2")).thenReturn(Optional.of(topic2)); - when(subscriptionService.getSubscriptionsOfApplication("_test", "app01", false)) - .thenReturn(Collections.singletonList(sub)); + when(subscriptionService.getSubscriptionsOfApplication("_test", "app01", false)).thenReturn(List.of(sub)); AclSupport aclSupport = new AclSupport(kafkaConfig, topicService, subscriptionService); Collection acls = aclSupport.getRequiredAclBindings("_test", metadata, "User:CN=testapp", false); - assertEquals(15, acls.size()); - - // check that cluster DESCRIBE and DESCRIBE_CONFIGS right is included - assertNotNull( - acls.stream() - .filter(b -> b.pattern().resourceType() == ResourceType.CLUSTER - && b.pattern().patternType() == PatternType.LITERAL - && b.pattern().name().equals("kafka-cluster") - && b.entry().permissionType() == AclPermissionType.ALLOW - && b.entry().operation() == AclOperation.DESCRIBE_CONFIGS) - .findAny().orElse(null), - "No DESCRIBE_CONFIGS right for cluster included"); - assertNotNull( - acls.stream() - .filter(b -> b.pattern().resourceType() == ResourceType.CLUSTER - && b.pattern().patternType() == PatternType.LITERAL - && b.pattern().name().equals("kafka-cluster") - && b.entry().permissionType() == AclPermissionType.ALLOW - && b.entry().operation() == AclOperation.DESCRIBE) - .findAny().orElse(null), - "No DESCRIBE right for cluster included"); + assertEquals(9, acls.size()); // two ACL for groups and two for topic prefixes must have been created assertNotNull(acls.stream() @@ -135,30 +118,26 @@ public void testGetRequiredAclBindings_simple() { .filter(binding -> binding.pattern().resourceType() == ResourceType.TRANSACTIONAL_ID && binding.pattern().patternType() == PatternType.PREFIXED && binding.pattern().name().equals("de.myapp.") - && binding.entry().operation() == AclOperation.DESCRIBE) - .findAny().orElse(null)); - assertNotNull(acls.stream() - .filter(binding -> binding.pattern().resourceType() == ResourceType.TRANSACTIONAL_ID - && binding.pattern().patternType() == PatternType.PREFIXED - && binding.pattern().name().equals("de.myapp.") - && binding.entry().operation() == AclOperation.WRITE) + && binding.entry().operation() == AclOperation.ALL) .findAny().orElse(null)); // Write rights for owned topic must also be present WRITE_TOPIC_OPERATIONS.forEach(op -> assertNotNull(acls.stream() .filter(binding -> binding.pattern().resourceType() == ResourceType.TOPIC && binding.pattern().patternType() == PatternType.LITERAL - && binding.pattern().name().equals("topic1") && binding.entry().operation() == op))); + && binding.pattern().name().equals("topic1") && binding.entry().operation() == op.operation + && binding.entry().permissionType() == op.permissionType))); // and read rights for subscribed topic READ_TOPIC_OPERATIONS.forEach(op -> assertNotNull(acls.stream() .filter(binding -> binding.pattern().resourceType() == ResourceType.TOPIC && binding.pattern().patternType() == PatternType.LITERAL - && binding.pattern().name().equals("topic2") && binding.entry().operation() == op))); + && binding.pattern().name().equals("topic2") && binding.entry().operation() == op.operation + && binding.entry().permissionType() == op.permissionType))); } @Test - public void testNoWriteAclsForInternalTopics() { + void testNoWriteAclsForInternalTopics() { ApplicationMetadata app1 = new ApplicationMetadata(); app1.setApplicationId("app-1"); app1.setConsumerGroupPrefixes(List.of("groups.")); @@ -173,12 +152,12 @@ public void testNoWriteAclsForInternalTopics() { AclSupport aclSupport = new AclSupport(kafkaConfig, topicService, subscriptionService); Collection bindings = aclSupport.getRequiredAclBindings("_test", app1, "User:CN=testapp", false); - assertEquals(3, bindings.size()); + assertEquals(1, bindings.size()); assertFalse(bindings.stream().anyMatch(binding -> binding.pattern().resourceType() == ResourceType.TOPIC)); } @Test - public void testAdditionalProducerWriteAccess() { + void testAdditionalProducerWriteAccess() { ApplicationMetadata app1 = new ApplicationMetadata(); app1.setApplicationId("app-1"); @@ -198,17 +177,16 @@ public void testAdditionalProducerWriteAccess() { false); WRITE_TOPIC_OPERATIONS.forEach(op -> assertNotNull( - bindings.stream() - .filter(binding -> binding.pattern().resourceType() == ResourceType.TOPIC - && binding.pattern().patternType() == PatternType.LITERAL - && binding.pattern().name().equals("topic1") && binding.entry().operation() == op - && binding.entry().principal().equals("User:CN=producer1")) - .findAny().orElse(null), - "Did not find expected write ACL for topic (operation " + op.name() + " is missing)")); + bindings.stream().filter(binding -> binding.pattern().resourceType() == ResourceType.TOPIC + && binding.pattern().patternType() == PatternType.LITERAL + && binding.pattern().name().equals("topic1") && binding.entry().operation() == op.operation + && binding.entry().permissionType() == op.permissionType + && binding.entry().principal().equals("User:CN=producer1")).findAny().orElse(null), + "Did not find expected write ACL for topic (operation " + op.operation.name() + " is missing)")); } @Test - public void testDefaultAcls() { + void testDefaultAcls() { ApplicationMetadata app1 = new ApplicationMetadata(); app1.setApplicationId("app-1"); app1.setAuthenticationJson(new JSONObject(Map.of("dn", "CN=testapp")).toString()); @@ -236,7 +214,7 @@ public void testDefaultAcls() { } @Test - public void testReadOnlyAcls() { + void testReadOnlyAcls() { ApplicationMetadata metadata = new ApplicationMetadata(); metadata.setApplicationId("app01"); metadata.setConsumerGroupPrefixes(List.of("group.myapp.", "group2.myapp.")); @@ -263,34 +241,13 @@ public void testReadOnlyAcls() { when(topicService.listTopics("_test")).thenReturn(List.of(topic1, topic2)); when(topicService.getTopic("_test", "topic2")).thenReturn(Optional.of(topic2)); - when(subscriptionService.getSubscriptionsOfApplication("_test", "app01", false)) - .thenReturn(Collections.singletonList(sub)); + when(subscriptionService.getSubscriptionsOfApplication("_test", "app01", false)).thenReturn(List.of(sub)); AclSupport aclSupport = new AclSupport(kafkaConfig, topicService, subscriptionService); Collection acls = aclSupport.getRequiredAclBindings("_test", metadata, "User:CN=testapp", true); - assertEquals(14, acls.size()); - - // check that cluster DESCRIBE and DESCRIBE_CONFIGS right is included - assertNotNull( - acls.stream() - .filter(b -> b.pattern().resourceType() == ResourceType.CLUSTER - && b.pattern().patternType() == PatternType.LITERAL - && b.pattern().name().equals("kafka-cluster") - && b.entry().permissionType() == AclPermissionType.ALLOW - && b.entry().operation() == AclOperation.DESCRIBE_CONFIGS) - .findAny().orElse(null), - "No DESCRIBE_CONFIGS right for cluster included"); - assertNotNull( - acls.stream() - .filter(b -> b.pattern().resourceType() == ResourceType.CLUSTER - && b.pattern().patternType() == PatternType.LITERAL - && b.pattern().name().equals("kafka-cluster") - && b.entry().permissionType() == AclPermissionType.ALLOW - && b.entry().operation() == AclOperation.DESCRIBE) - .findAny().orElse(null), - "No DESCRIBE right for cluster included"); + assertEquals(8, acls.size()); // NO group ACLs must have been created assertEquals(List.of(), acls.stream().filter(binding -> binding.pattern().resourceType() == ResourceType.GROUP) @@ -307,25 +264,50 @@ public void testReadOnlyAcls() { assertEquals(List.of(), acls.stream().filter(binding -> binding.entry().operation() == AclOperation.WRITE) .collect(Collectors.toList())); - // for internal, owned, and subscribed topics, DESCRIBE, DESCRIBE_CONFIGS, and READ must exist - for (AclOperation op : READ_TOPIC_OPERATIONS) { - assertTrue(acls.stream() - .anyMatch(binding -> binding.pattern().resourceType() == ResourceType.TOPIC - && binding.pattern().patternType() == PatternType.PREFIXED - && binding.pattern().name().equals("de.myapp.") && binding.entry().operation() == op)); + // for internal, owned, and subscribed topics, DESCRIBE_CONFIGS and READ must exist + for (AclOperationAndType op : READ_TOPIC_OPERATIONS) { + assertTrue(acls.stream().anyMatch(binding -> binding.pattern().resourceType() == ResourceType.TOPIC + && binding.pattern().patternType() == PatternType.PREFIXED + && binding.pattern().name().equals("de.myapp.") && binding.entry().operation() == op.operation + && binding.entry().permissionType() == op.permissionType)); assertTrue(acls.stream() .anyMatch(binding -> binding.pattern().resourceType() == ResourceType.TOPIC && binding.pattern().patternType() == PatternType.LITERAL - && binding.pattern().name().equals("topic1") && binding.entry().operation() == op)); + && binding.pattern().name().equals("topic1") && binding.entry().operation() == op.operation + && binding.entry().permissionType() == op.permissionType)); assertTrue(acls.stream() .anyMatch(binding -> binding.pattern().resourceType() == ResourceType.TOPIC && binding.pattern().patternType() == PatternType.LITERAL - && binding.pattern().name().equals("topic2") && binding.entry().operation() == op)); + && binding.pattern().name().equals("topic2") && binding.entry().operation() == op.operation + && binding.entry().permissionType() == op.permissionType)); } } + @Test + void testSimplify() { + AclSupport support = new AclSupport(kafkaConfig, topicService, subscriptionService); + + AclBinding superfluousBinding = new AclBinding( + new ResourcePattern(ResourceType.TOPIC, "test", PatternType.PREFIXED), + new AccessControlEntry("me", "*", AclOperation.READ, AclPermissionType.ALLOW)); + + List bindings = List.of(superfluousBinding, + new AclBinding(new ResourcePattern(ResourceType.TOPIC, "test", PatternType.PREFIXED), + new AccessControlEntry("me", "*", AclOperation.ALL, AclPermissionType.ALLOW)), + new AclBinding(new ResourcePattern(ResourceType.TOPIC, "2test", PatternType.LITERAL), + new AccessControlEntry("me", "*", AclOperation.ALL, AclPermissionType.ALLOW)), + new AclBinding(new ResourcePattern(ResourceType.TOPIC, "2test", PatternType.PREFIXED), + new AccessControlEntry("me", "*", AclOperation.READ, AclPermissionType.ALLOW)), + new AclBinding(new ResourcePattern(ResourceType.TOPIC, "2test", PatternType.PREFIXED), + new AccessControlEntry("me", "*", AclOperation.CREATE, AclPermissionType.ALLOW))); + + Collection reducedBindings = support.simplify(bindings); + assertEquals(4, reducedBindings.size()); + assertFalse(reducedBindings.contains(superfluousBinding)); + } + private DefaultAclConfig defaultAclConfig(String name, ResourceType resourceType, PatternType patternType, AclOperation operation) { DefaultAclConfig config = new DefaultAclConfig(); @@ -335,4 +317,16 @@ private DefaultAclConfig defaultAclConfig(String name, ResourceType resourceType config.setOperation(operation); return config; } + + private static class AclOperationAndType { + + private final AclOperation operation; + + private final AclPermissionType permissionType; + + private AclOperationAndType(AclOperation operation, AclPermissionType permissionType) { + this.operation = operation; + this.permissionType = permissionType; + } + } } diff --git a/src/test/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategyConverterBindingIntegrationTest.java b/src/test/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategyConverterBindingIntegrationTest.java index c79d8721..088293e9 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategyConverterBindingIntegrationTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategyConverterBindingIntegrationTest.java @@ -1,29 +1,30 @@ package com.hermesworld.ais.galapagos.naming.config; +import com.hermesworld.ais.galapagos.GalapagosTestConfig; import com.hermesworld.ais.galapagos.kafka.KafkaClusters; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit4.SpringRunner; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; -@RunWith(SpringRunner.class) @SpringBootTest @TestPropertySource(locations = "classpath:test-case-strategies.properties") -public class CaseStrategyConverterBindingIntegrationTest { +@Import(GalapagosTestConfig.class) +class CaseStrategyConverterBindingIntegrationTest { @Autowired private NamingConfig config; + @SuppressWarnings("unused") @MockBean private KafkaClusters clusters; @Test - public void testConversion() { + void testConversion() { assertEquals(CaseStrategy.PASCAL_CASE, config.getNormalizationStrategy()); } diff --git a/src/test/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategyConverterBindingTest.java b/src/test/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategyConverterBindingTest.java index 5782057c..4be87c2c 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategyConverterBindingTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/naming/config/CaseStrategyConverterBindingTest.java @@ -1,13 +1,14 @@ package com.hermesworld.ais.galapagos.naming.config; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; -public class CaseStrategyConverterBindingTest { +class CaseStrategyConverterBindingTest { @Test - public void testValidValues() { + void testValidValues() { CaseStrategyConverterBinding converter = new CaseStrategyConverterBinding(); assertEquals(CaseStrategy.PASCAL_CASE, converter.convert("PascalCase")); assertEquals(CaseStrategy.CAMEL_CASE, converter.convert("camelCase")); @@ -16,22 +17,28 @@ public void testValidValues() { assertEquals(CaseStrategy.LOWERCASE, converter.convert("lowercase")); } - @Test(expected = IllegalArgumentException.class) - public void testNoCaseConversion() { - CaseStrategyConverterBinding converter = new CaseStrategyConverterBinding(); - converter.convert("pascalCase"); + @Test + void testNoCaseConversion() { + assertThrows(IllegalArgumentException.class, () -> { + CaseStrategyConverterBinding converter = new CaseStrategyConverterBinding(); + converter.convert("pascalCase"); + }); } - @Test(expected = IllegalArgumentException.class) - public void testNoEnumNameUse() { - CaseStrategyConverterBinding converter = new CaseStrategyConverterBinding(); - converter.convert("PASCAL_CASE"); + @Test + void testNoEnumNameUse() { + assertThrows(IllegalArgumentException.class, () -> { + CaseStrategyConverterBinding converter = new CaseStrategyConverterBinding(); + converter.convert("PASCAL_CASE"); + }); } - @Test(expected = IllegalArgumentException.class) - public void testInvalidValue() { - CaseStrategyConverterBinding converter = new CaseStrategyConverterBinding(); - converter.convert("someValue"); + @Test + void testInvalidValue() { + assertThrows(IllegalArgumentException.class, () -> { + CaseStrategyConverterBinding converter = new CaseStrategyConverterBinding(); + converter.convert("someValue"); + }); } } diff --git a/src/test/java/com/hermesworld/ais/galapagos/naming/impl/NamingServiceImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/naming/impl/NamingServiceImplTest.java index a7d7b1ab..484832f6 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/naming/impl/NamingServiceImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/naming/impl/NamingServiceImplTest.java @@ -9,17 +9,17 @@ import com.hermesworld.ais.galapagos.naming.config.NamingConfig; import com.hermesworld.ais.galapagos.naming.config.TopicNamingConfig; import com.hermesworld.ais.galapagos.topics.TopicType; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.util.List; import java.util.Set; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class NamingServiceImplTest { +class NamingServiceImplTest { private AdditionNamingRules rules; @@ -31,8 +31,8 @@ public class NamingServiceImplTest { private BusinessCapability cap2; - @Before - public void feedMocks() { + @BeforeEach + void feedMocks() { rules = new AdditionNamingRules(); rules.setAllowedSeparators("."); rules.setAllowPascalCase(true); @@ -63,7 +63,7 @@ public void feedMocks() { } @Test - public void testAllowInternalTopicNamesForConsumerGroups() { + void testAllowInternalTopicNamesForConsumerGroups() { config.setAllowInternalTopicNamesAsConsumerGroups(true); NamingServiceImpl service = new NamingServiceImpl(config); ApplicationPrefixes prefixes = service.getAllowedPrefixes(app); @@ -85,7 +85,7 @@ public void testAllowInternalTopicNamesForConsumerGroups() { } @Test - public void testGetTopicNameSuggestion() { + void testGetTopicNameSuggestion() { NamingServiceImpl service = new NamingServiceImpl(config); assertEquals("de.hlg.events.orders.my-topic", service.getTopicNameSuggestion(TopicType.EVENTS, app, cap1)); @@ -100,7 +100,7 @@ public void testGetTopicNameSuggestion() { } @Test - public void testGetAllowedPrefixes() { + void testGetAllowedPrefixes() { when(app.getAliases()).thenReturn(Set.of("tt")); NamingServiceImpl service = new NamingServiceImpl(config); @@ -131,7 +131,7 @@ public void testGetAllowedPrefixes() { } @Test - public void testValidateTopicName() throws InvalidTopicNameException { + void testValidateTopicName() throws InvalidTopicNameException { NamingServiceImpl service = new NamingServiceImpl(config); service.validateTopicName("de.hlg.events.orders.my-custom-order", TopicType.EVENTS, app); @@ -173,7 +173,7 @@ public void testValidateTopicName() throws InvalidTopicNameException { } @Test - public void testNormalize_simpleCases() { + void testNormalize_simpleCases() { NamingConfig config = mock(NamingConfig.class); when(config.getNormalizationStrategy()).thenReturn(CaseStrategy.PASCAL_CASE); @@ -194,7 +194,7 @@ public void testNormalize_simpleCases() { } @Test - public void testNormalize_lead_trail() { + void testNormalize_lead_trail() { NamingConfig config = mock(NamingConfig.class); when(config.getNormalizationStrategy()).thenReturn(CaseStrategy.KEBAB_CASE); NamingServiceImpl service = new NamingServiceImpl(config); @@ -206,7 +206,7 @@ public void testNormalize_lead_trail() { } @Test - public void testNormalize_localization() { + void testNormalize_localization() { NamingConfig config = mock(NamingConfig.class); when(config.getNormalizationStrategy()).thenReturn(CaseStrategy.KEBAB_CASE); NamingServiceImpl service = new NamingServiceImpl(config); diff --git a/src/test/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationEventListenerTest.java b/src/test/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationEventListenerTest.java index 3e238d7c..136d54f4 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationEventListenerTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationEventListenerTest.java @@ -14,20 +14,20 @@ import com.hermesworld.ais.galapagos.topics.TopicType; import com.hermesworld.ais.galapagos.topics.service.TopicService; import com.hermesworld.ais.galapagos.util.FutureUtil; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -public class NotificationEventListenerTest { +class NotificationEventListenerTest { private NotificationEventListener listener; @@ -35,8 +35,8 @@ public class NotificationEventListenerTest { private GalapagosEventContext context; - @Before - public void feedMocks() { + @BeforeEach + void feedMocks() { notificationService = spy(mock(NotificationService.class)); KafkaClusters kafkaClusters = mock(KafkaClusters.class); @@ -58,7 +58,7 @@ public void feedMocks() { } @Test - public void testHandleTopicDeprecated() { + void testHandleTopicDeprecated() { AtomicInteger sendCalled = new AtomicInteger(); when(notificationService.notifySubscribers(any(), any(), any(), any())).then(inv -> { sendCalled.incrementAndGet(); @@ -69,11 +69,11 @@ public void testHandleTopicDeprecated() { listener.handleTopicDeprecated(buildTestEvent("test2")); listener.handleTopicDeprecated(buildTestEvent("prod")); - assertEquals("Deprecation mail should only be sent for production environment", 1, sendCalled.get()); + assertEquals(1, sendCalled.get(), "Deprecation mail should only be sent for production environment"); } @Test - public void testHandleTopicUndeprecated() { + void testHandleTopicUndeprecated() { AtomicInteger sendCalled = new AtomicInteger(); when(notificationService.notifySubscribers(any(), any(), any(), any())).then(inv -> { sendCalled.incrementAndGet(); @@ -84,11 +84,11 @@ public void testHandleTopicUndeprecated() { listener.handleTopicUndeprecated(buildTestEvent("test2")); listener.handleTopicUndeprecated(buildTestEvent("prod")); - assertEquals("Undeprecation mail should only be sent for production environment", 1, sendCalled.get()); + assertEquals(1, sendCalled.get(), "Undeprecation mail should only be sent for production environment"); } @Test - public void testHandleSchemaChangeDesc() throws ExecutionException, InterruptedException { + void testHandleSchemaChangeDesc() throws ExecutionException, InterruptedException { TopicMetadata metadata = new TopicMetadata(); metadata.setName("testtopic"); diff --git a/src/test/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationServiceImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationServiceImplTest.java index 262f20ff..4f95ec51 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationServiceImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/notifications/impl/NotificationServiceImplTest.java @@ -9,10 +9,10 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; -import javax.mail.Address; -import javax.mail.MessagingException; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMessage.RecipientType; +import jakarta.mail.Address; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMessage.RecipientType; import com.hermesworld.ais.galapagos.applications.ApplicationOwnerRequest; import com.hermesworld.ais.galapagos.applications.ApplicationsService; @@ -21,11 +21,12 @@ import com.hermesworld.ais.galapagos.subscriptions.SubscriptionMetadata; import com.hermesworld.ais.galapagos.subscriptions.service.SubscriptionService; import com.hermesworld.ais.galapagos.topics.service.TopicService; -import org.junit.After; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import org.junit.Before; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import static org.mockito.Mockito.*; import org.springframework.mail.MailSendException; @@ -33,7 +34,7 @@ import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; import org.thymeleaf.ITemplateEngine; -public class NotificationServiceImplTest { +class NotificationServiceImplTest { private static final String TEST_ENV = "test-Environment"; @@ -55,8 +56,8 @@ public class NotificationServiceImplTest { private MimeMessageHolder messageHolder; - @Before - public void feedMocks() throws MessagingException { + @BeforeEach + void feedMocks() throws MessagingException { subscriptionService = mock(SubscriptionService.class); applicationService = mock(ApplicationsService.class); topicService = mock(TopicService.class); @@ -70,13 +71,13 @@ public void feedMocks() throws MessagingException { executor = Executors.newSingleThreadExecutor(); } - @After - public void destroyMocks() { + @AfterEach + void destroyMocks() { executor.shutdown(); } @Test - public void testDoSendAsync_NoFailOnMailException() throws Exception { + void testDoSendAsync_NoFailOnMailException() throws Exception { String testFromAddress = "test@abc.de"; String testAdminMailsRecipients = "Test@abc.de"; NotificationParams testNotificationParams = generateNotificationParams(TEST_USER, TEST_TOPIC); @@ -94,7 +95,7 @@ public void testDoSendAsync_NoFailOnMailException() throws Exception { } @Test - public void testNotifySubscribersWithExclusionUser() throws Exception { + void testNotifySubscribersWithExclusionUser() throws Exception { String testFromAddress = "test@abc.de"; String testAdminMailsRecipients = "Test@abc.de"; String testApplicationId = "42"; @@ -123,7 +124,7 @@ public void testNotifySubscribersWithExclusionUser() throws Exception { } @Test - public void testNotifyApplicationTopicOwners_noSubmittedIncluded() throws Exception { + void testNotifyApplicationTopicOwners_noSubmittedIncluded() throws Exception { String testFromAddress = "test@abc.de"; String testAdminMailsRecipients = "Test@abc.de"; String applicationId = "1"; @@ -171,7 +172,7 @@ public void testNotifyApplicationTopicOwners_noSubmittedIncluded() throws Except } @Test - public void testNotifySubscribers_noSubmittedIncluded() throws Exception { + void testNotifySubscribers_noSubmittedIncluded() throws Exception { String testFromAddress = "test@abc.de"; String testAdminMailsRecipients = "Test@abc.de"; NotificationParams testNotificationParams = generateNotificationParams(TEST_USER, TEST_TOPIC); diff --git a/src/test/java/com/hermesworld/ais/galapagos/request/ApplicationOwnerRequestTest.java b/src/test/java/com/hermesworld/ais/galapagos/request/ApplicationOwnerRequestTest.java index 5546e983..8bed6ef0 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/request/ApplicationOwnerRequestTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/request/ApplicationOwnerRequestTest.java @@ -1,19 +1,18 @@ package com.hermesworld.ais.galapagos.request; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.ObjectMapper; import com.hermesworld.ais.galapagos.applications.ApplicationOwnerRequest; import com.hermesworld.ais.galapagos.applications.RequestState; import com.hermesworld.ais.galapagos.util.JsonUtil; +import org.junit.jupiter.api.Test; -public class ApplicationOwnerRequestTest { +class ApplicationOwnerRequestTest { @Test - public void testApplicationOwnerRequestSerializable() throws Exception { + void testApplicationOwnerRequestSerializable() throws Exception { ObjectMapper mapper = JsonUtil.newObjectMapper(); ApplicationOwnerRequest request = new ApplicationOwnerRequest(); diff --git a/src/test/java/com/hermesworld/ais/galapagos/schema/SchemaCompatibilityValidatorTest.java b/src/test/java/com/hermesworld/ais/galapagos/schema/SchemaCompatibilityValidatorTest.java new file mode 100644 index 00000000..850fd7a0 --- /dev/null +++ b/src/test/java/com/hermesworld/ais/galapagos/schema/SchemaCompatibilityValidatorTest.java @@ -0,0 +1,165 @@ +package com.hermesworld.ais.galapagos.schema; + +import com.hermesworld.ais.galapagos.schemas.ConsumerCompatibilityErrorHandler; +import com.hermesworld.ais.galapagos.schemas.IncompatibleSchemaException; +import com.hermesworld.ais.galapagos.schemas.ProducerCompatibilityErrorHandler; +import com.hermesworld.ais.galapagos.schemas.SchemaCompatibilityValidator; +import org.everit.json.schema.Schema; +import org.everit.json.schema.loader.SchemaLoader; +import org.json.JSONObject; +import org.junit.Test; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class SchemaCompatibilityValidatorTest { + + @Test(expected = IncompatibleSchemaException.class) + public void testAddAdditionalPropertiesOnObject_fail() throws Exception { + verifyConsumerCompatibleTo(readSchema("test01a"), readSchema("test01b")); + } + + @Test + public void testAddRequiredPropertyOnObject_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test02a"), readSchema("test02b")); + } + + @Test + public void testRemoveOneOfSchema_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test03a"), readSchema("test03b")); + } + + @Test(expected = IncompatibleSchemaException.class) + public void testAddOneOfSchema_fail() throws Exception { + verifyConsumerCompatibleTo(readSchema("test03a"), readSchema("test03c")); + } + + @Test + public void testAddArrayRestriction_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test03a"), readSchema("test03d")); + } + + @Test(expected = IncompatibleSchemaException.class) + public void testRelaxArrayRestriction_fail() throws Exception { + verifyConsumerCompatibleTo(readSchema("test03d"), readSchema("test03e")); + } + + @Test + public void testAddPropertyWithAdditionalProperties_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test04a"), readSchema("test04b")); + } + + @Test + public void testAddStringLimits_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test04a"), readSchema("test04c")); + } + + @Test(expected = IncompatibleSchemaException.class) + public void testRelaxStringLimits_fail() throws Exception { + verifyConsumerCompatibleTo(readSchema("test04c"), readSchema("test04d")); + } + + @Test + public void testRemoveEnumValue_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test05a"), readSchema("test05b")); + } + + @Test(expected = IncompatibleSchemaException.class) + public void testAddEnumValue_fail() throws Exception { + verifyConsumerCompatibleTo(readSchema("test05a"), readSchema("test05c")); + } + + @Test + public void testNotMoreLiberal_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test06a"), readSchema("test06b")); + } + + @Test(expected = IncompatibleSchemaException.class) + public void testNotMoreStrict_fail() throws Exception { + verifyConsumerCompatibleTo(readSchema("test06a"), readSchema("test06c")); + } + + @Test(expected = IncompatibleSchemaException.class) + public void testTotallyDifferent_fail() throws Exception { + verifyConsumerCompatibleTo(readSchema("test01a"), readSchema("test03a")); + } + + @Test + public void testAnyOfReplacedBySubschema_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test07a"), readSchema("test07b")); + } + + @Test(expected = IncompatibleSchemaException.class) + public void testAnyOfReplacedByIncompatibleSchema_fail() throws Exception { + verifyConsumerCompatibleTo(readSchema("test07a"), readSchema("test07c")); + } + + @Test(expected = IncompatibleSchemaException.class) + public void testIntegerToNumber_fail() throws Exception { + verifyConsumerCompatibleTo(readSchema("test08a"), readSchema("test08b")); + } + + @Test + public void testIntegerStaysInteger_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test08a"), readSchema("test08c")); + } + + @Test + public void testNumberToInteger_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test08b"), readSchema("test08a")); + } + + @Test + public void testRemoveOptionalWithNoAdditionalProperties_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test09a"), readSchema("test09b")); + } + + @Test(expected = IncompatibleSchemaException.class) + public void testRemoveOptionalWithAdditionalProperties_fail() throws Exception { + verifyConsumerCompatibleTo(readSchema("test09a"), readSchema("test09c")); + } + + @Test + public void testPatternField_success() throws Exception { + verifyConsumerCompatibleTo(readSchema("test-pattern-field"), + readSchema("test-pattern-field-with-another-prop")); + } + + @Test(expected = IncompatibleSchemaException.class) + public void testProducerCompatible_failWithAdditionalProperty() throws Exception { + ProducerCompatibilityErrorHandler errorHandler = new ProducerCompatibilityErrorHandler(false); + new SchemaCompatibilityValidator(readSchema("test10b"), readSchema("test10a"), errorHandler).validate(); + } + + @Test + public void testProducerCompatible_liberalAdditionalProperty() throws Exception { + ProducerCompatibilityErrorHandler errorHandler = new ProducerCompatibilityErrorHandler(true); + new SchemaCompatibilityValidator(readSchema("test10b"), readSchema("test10a"), errorHandler).validate(); + } + + @Test + public void testConsumerCompatible_liberalRemoveProperty() throws Exception { + ConsumerCompatibilityErrorHandler errorHandler = new ConsumerCompatibilityErrorHandler(true); + new SchemaCompatibilityValidator(readSchema("test10b"), readSchema("test10a"), errorHandler).validate(); + } + + private static Schema readSchema(String id) { + try (InputStream in = SchemaCompatibilityValidatorTest.class.getClassLoader() + .getResourceAsStream("schema-compatibility/" + id + ".schema.json")) { + String data = StreamUtils.copyToString(in, StandardCharsets.UTF_8); + JSONObject obj = new JSONObject(data); + return SchemaLoader.load(obj); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void verifyConsumerCompatibleTo(Schema oldSchema, Schema newSchema) + throws IncompatibleSchemaException { + new SchemaCompatibilityValidator(oldSchema, newSchema, new ConsumerCompatibilityErrorHandler(false)).validate(); + } + +} diff --git a/src/test/java/com/hermesworld/ais/galapagos/schema/SchemaUtilTest.java b/src/test/java/com/hermesworld/ais/galapagos/schema/SchemaUtilTest.java deleted file mode 100644 index 3f46f203..00000000 --- a/src/test/java/com/hermesworld/ais/galapagos/schema/SchemaUtilTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.hermesworld.ais.galapagos.schema; - -import com.hermesworld.ais.galapagos.schemas.IncompatibleSchemaException; -import com.hermesworld.ais.galapagos.schemas.SchemaUtil; -import org.everit.json.schema.Schema; -import org.everit.json.schema.loader.SchemaLoader; -import org.json.JSONObject; -import org.junit.Test; -import org.springframework.util.StreamUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -public class SchemaUtilTest { - - @Test(expected = IncompatibleSchemaException.class) - public void testAddAdditionalPropertiesOnObject_fail() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test01a"), readSchema("test01b")); - } - - @Test - public void testAddRequiredPropertyOnObject_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test02a"), readSchema("test02b")); - } - - @Test - public void testRemoveOneOfSchema_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test03a"), readSchema("test03b")); - } - - @Test(expected = IncompatibleSchemaException.class) - public void testAddOneOfSchema_fail() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test03a"), readSchema("test03c")); - } - - @Test - public void testAddArrayRestriction_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test03a"), readSchema("test03d")); - } - - @Test(expected = IncompatibleSchemaException.class) - public void testRelaxArrayRestriction_fail() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test03d"), readSchema("test03e")); - } - - @Test - public void testAddPropertyWithAdditionalProperties_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test04a"), readSchema("test04b")); - } - - @Test - public void testAddStringLimits_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test04a"), readSchema("test04c")); - } - - @Test(expected = IncompatibleSchemaException.class) - public void testRelaxStringLimits_fail() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test04c"), readSchema("test04d")); - } - - @Test - public void testRemoveEnumValue_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test05a"), readSchema("test05b")); - } - - @Test(expected = IncompatibleSchemaException.class) - public void testAddEnumValue_fail() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test05a"), readSchema("test05c")); - } - - @Test - public void testNotMoreGreedy_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test06a"), readSchema("test06b")); - } - - @Test(expected = IncompatibleSchemaException.class) - public void testNotMoreStrict_fail() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test06a"), readSchema("test06c")); - } - - @Test(expected = IncompatibleSchemaException.class) - public void testTotallyDifferent_fail() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test01a"), readSchema("test03a")); - } - - @Test - public void testAnyOfReplacedBySubschema_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test07a"), readSchema("test07b")); - } - - @Test(expected = IncompatibleSchemaException.class) - public void testAnyOfReplacedByIncompatibleSchema_fail() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test07a"), readSchema("test07c")); - } - - @Test(expected = IncompatibleSchemaException.class) - public void testIntegerToNumber_fail() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test08a"), readSchema("test08b")); - } - - @Test - public void testIntegerStaysInteger_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test08a"), readSchema("test08c")); - } - - @Test - public void testNumberToInteger_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test08b"), readSchema("test08a")); - } - - @Test - public void testRemoveOptionalWithNoAdditionalProperties_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test09a"), readSchema("test09b")); - } - - @Test(expected = IncompatibleSchemaException.class) - public void testRemoveOptionalWithAdditionalProperties_fail() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test09a"), readSchema("test09c")); - } - - @Test - public void testPatternField_success() throws Exception { - SchemaUtil.verifyCompatibleTo(readSchema("test-pattern-field"), - readSchema("test-pattern-field-with-another-prop")); - } - - private static Schema readSchema(String id) { - try (InputStream in = SchemaUtilTest.class.getClassLoader() - .getResourceAsStream("schema-compatibility/" + id + ".schema.json")) { - String data = StreamUtils.copyToString(in, StandardCharsets.UTF_8); - JSONObject obj = new JSONObject(data); - return SchemaLoader.load(obj); - } - catch (IOException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/src/test/java/com/hermesworld/ais/galapagos/staging/impl/StagingImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/staging/impl/StagingImplTest.java index 62284b01..a03b8dd6 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/staging/impl/StagingImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/staging/impl/StagingImplTest.java @@ -11,24 +11,23 @@ import com.hermesworld.ais.galapagos.topics.TopicType; import com.hermesworld.ais.galapagos.topics.service.TopicService; import com.hermesworld.ais.galapagos.util.JsonUtil; -import org.junit.Test; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class StagingImplTest { +class StagingImplTest { @Test - public void testSubscriptionIdentity() throws Exception { + void testSubscriptionIdentity() throws Exception { TopicService topicService = mock(TopicService.class); TopicMetadata topic1 = new TopicMetadata(); @@ -36,8 +35,8 @@ public void testSubscriptionIdentity() throws Exception { topic1.setOwnerApplicationId("app-2"); topic1.setType(TopicType.EVENTS); - when(topicService.listTopics("dev")).thenReturn(Collections.singletonList(topic1)); - when(topicService.listTopics("int")).thenReturn(Collections.singletonList(topic1)); + when(topicService.listTopics("dev")).thenReturn(List.of(topic1)); + when(topicService.listTopics("int")).thenReturn(List.of(topic1)); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -45,8 +44,7 @@ public void testSubscriptionIdentity() throws Exception { sub1.setClientApplicationId("app-1"); sub1.setId("123"); sub1.setTopicName("topic-1"); - when(subscriptionService.getSubscriptionsOfApplication("dev", "app-1", false)) - .thenReturn(Collections.singletonList(sub1)); + when(subscriptionService.getSubscriptionsOfApplication("dev", "app-1", false)).thenReturn(List.of(sub1)); StagingImpl staging = StagingImpl.build("app-1", "dev", "int", null, topicService, subscriptionService).get(); @@ -62,8 +60,7 @@ public void testSubscriptionIdentity() throws Exception { sub2.setClientApplicationId("app-1"); sub2.setId("456"); sub2.setTopicName("topic-1"); - when(subscriptionService.getSubscriptionsOfApplication("int", "app-1", false)) - .thenReturn(Collections.singletonList(sub2)); + when(subscriptionService.getSubscriptionsOfApplication("int", "app-1", false)).thenReturn(List.of(sub2)); staging = StagingImpl.build("app-1", "dev", "int", null, topicService, subscriptionService).get(); changes = staging.getChanges(); @@ -72,7 +69,7 @@ public void testSubscriptionIdentity() throws Exception { @Test @DisplayName("should stage new added producer to next stage") - public void testProducerAddStaging() throws Exception { + void testProducerAddStaging() throws Exception { TopicService topicService = mock(TopicService.class); TopicMetadata topic1 = new TopicMetadata(); @@ -86,8 +83,8 @@ public void testProducerAddStaging() throws Exception { topic2.setOwnerApplicationId("app-1"); topic2.setType(TopicType.EVENTS); - when(topicService.listTopics("dev")).thenReturn(Collections.singletonList(topic1)); - when(topicService.listTopics("int")).thenReturn(Collections.singletonList(topic2)); + when(topicService.listTopics("dev")).thenReturn(List.of(topic1)); + when(topicService.listTopics("int")).thenReturn(List.of(topic2)); StagingImpl staging = StagingImpl .build("app-1", "dev", "int", null, topicService, mock(SubscriptionService.class)).get(); @@ -106,7 +103,7 @@ public void testProducerAddStaging() throws Exception { @Test @DisplayName("should stage removed producer to next stage") - public void testProducerRemoveStaging() throws Exception { + void testProducerRemoveStaging() throws Exception { TopicService topicService = mock(TopicService.class); TopicMetadata topic1 = new TopicMetadata(); @@ -121,8 +118,8 @@ public void testProducerRemoveStaging() throws Exception { topic2.setType(TopicType.EVENTS); topic2.setProducers(List.of("producer1")); - when(topicService.listTopics("dev")).thenReturn(Collections.singletonList(topic1)); - when(topicService.listTopics("int")).thenReturn(Collections.singletonList(topic2)); + when(topicService.listTopics("dev")).thenReturn(List.of(topic1)); + when(topicService.listTopics("int")).thenReturn(List.of(topic2)); List producers = new ArrayList<>(topic1.getProducers()); producers.remove("producer1"); @@ -138,7 +135,7 @@ public void testProducerRemoveStaging() throws Exception { } @Test - public void testSchemaIdentity() throws Exception { + void testSchemaIdentity() throws Exception { TopicService topicService = mock(TopicService.class); TopicMetadata topic1 = new TopicMetadata(); @@ -146,8 +143,8 @@ public void testSchemaIdentity() throws Exception { topic1.setOwnerApplicationId("app-1"); topic1.setType(TopicType.EVENTS); - when(topicService.listTopics("dev")).thenReturn(Collections.singletonList(topic1)); - when(topicService.listTopics("int")).thenReturn(Collections.singletonList(topic1)); + when(topicService.listTopics("dev")).thenReturn(List.of(topic1)); + when(topicService.listTopics("int")).thenReturn(List.of(topic1)); SchemaMetadata schema1 = new SchemaMetadata(); schema1.setId("999"); @@ -155,7 +152,7 @@ public void testSchemaIdentity() throws Exception { schema1.setCreatedBy("test"); schema1.setTopicName("topic-1"); - when(topicService.getTopicSchemaVersions("dev", "topic-1")).thenReturn(Collections.singletonList(schema1)); + when(topicService.getTopicSchemaVersions("dev", "topic-1")).thenReturn(List.of(schema1)); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -174,7 +171,7 @@ public void testSchemaIdentity() throws Exception { schema2.setCreatedBy("test"); schema2.setTopicName("topic-1"); schema2.setSchemaVersion(1); - when(topicService.getTopicSchemaVersions("int", "topic-1")).thenReturn(Collections.singletonList(schema2)); + when(topicService.getTopicSchemaVersions("int", "topic-1")).thenReturn(List.of(schema2)); staging = StagingImpl.build("app-1", "dev", "int", null, topicService, subscriptionService).get(); changes = staging.getChanges(); @@ -182,7 +179,7 @@ public void testSchemaIdentity() throws Exception { } @Test - public void testCompoundChangeForApiTopicCreation() throws Exception { + void testCompoundChangeForApiTopicCreation() throws Exception { TopicService topicService = mock(TopicService.class); TopicMetadata topic1 = new TopicMetadata(); @@ -190,7 +187,7 @@ public void testCompoundChangeForApiTopicCreation() throws Exception { topic1.setOwnerApplicationId("app-1"); topic1.setType(TopicType.EVENTS); - when(topicService.listTopics("dev")).thenReturn(Collections.singletonList(topic1)); + when(topicService.listTopics("dev")).thenReturn(List.of(topic1)); when(topicService.buildTopicCreateParams("dev", "topic-1")) .thenReturn(CompletableFuture.completedFuture(new TopicCreateParams(2, 2))); @@ -227,7 +224,7 @@ public void testCompoundChangeForApiTopicCreation() throws Exception { } @Test - public void testApiTopicWithoutSchema_fail() throws Exception { + void testApiTopicWithoutSchema_fail() throws Exception { TopicService topicService = mock(TopicService.class); TopicMetadata topic1 = new TopicMetadata(); @@ -235,7 +232,7 @@ public void testApiTopicWithoutSchema_fail() throws Exception { topic1.setOwnerApplicationId("app-1"); topic1.setType(TopicType.EVENTS); - when(topicService.listTopics("dev")).thenReturn(Collections.singletonList(topic1)); + when(topicService.listTopics("dev")).thenReturn(List.of(topic1)); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -254,7 +251,7 @@ public void testApiTopicWithoutSchema_fail() throws Exception { } @Test - public void testStageDeprecatedTopic_fail() throws Exception { + void testStageDeprecatedTopic_fail() throws Exception { TopicService topicService = mock(TopicService.class); TopicMetadata topic1 = new TopicMetadata(); @@ -263,7 +260,7 @@ public void testStageDeprecatedTopic_fail() throws Exception { topic1.setType(TopicType.EVENTS); topic1.setDeprecated(true); - when(topicService.listTopics("dev")).thenReturn(Collections.singletonList(topic1)); + when(topicService.listTopics("dev")).thenReturn(List.of(topic1)); SubscriptionService subscriptionService = mock(SubscriptionService.class); diff --git a/src/test/java/com/hermesworld/ais/galapagos/subscriptions/SubscriptionsControllerTest.java b/src/test/java/com/hermesworld/ais/galapagos/subscriptions/SubscriptionsControllerTest.java index 5691b4d7..94310807 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/subscriptions/SubscriptionsControllerTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/subscriptions/SubscriptionsControllerTest.java @@ -1,15 +1,17 @@ package com.hermesworld.ais.galapagos.subscriptions; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.invocation.InvocationOnMock; import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; @@ -23,7 +25,7 @@ import com.hermesworld.ais.galapagos.topics.service.TopicService; import com.hermesworld.ais.galapagos.util.FutureUtil; -public class SubscriptionsControllerTest { +class SubscriptionsControllerTest { private KafkaClusters kafkaClusters; @@ -35,8 +37,8 @@ public class SubscriptionsControllerTest { private SubscriptionMetadata subscription = new SubscriptionMetadata(); - @Before - public void initMocks() { + @BeforeEach + void initMocks() { subscription.setId("sub-1"); subscription.setTopicName("topic-1"); subscription.setClientApplicationId("app-1"); @@ -49,7 +51,7 @@ public void initMocks() { } @Test - public void testUpdateSubscription_positive() throws Exception { + void testUpdateSubscription_positive() throws Exception { TopicMetadata topic1 = new TopicMetadata(); topic1.setName("topic-1"); topic1.setOwnerApplicationId("app-2"); @@ -79,7 +81,7 @@ public void testUpdateSubscription_positive() throws Exception { } @Test - public void testUpdateSubscription_invalidUser_clientAppOwner() throws Exception { + void testUpdateSubscription_invalidUser_clientAppOwner() throws Exception { TopicMetadata topic = new TopicMetadata(); topic.setName("topic-1"); topic.setOwnerApplicationId("app-2"); @@ -104,12 +106,12 @@ public void testUpdateSubscription_invalidUser_clientAppOwner() throws Exception fail("Expected FORBIDDEN for owner of CLIENT application"); } catch (ResponseStatusException e) { - assertEquals(HttpStatus.FORBIDDEN, e.getStatus()); + assertEquals(HttpStatus.FORBIDDEN, e.getStatusCode()); } } @Test - public void testUpdateSubscription_invalidTopic_forSubscription() throws Exception { + void testUpdateSubscription_invalidTopic_forSubscription() throws Exception { TopicMetadata topic1 = new TopicMetadata(); topic1.setName("topic-1"); topic1.setOwnerApplicationId("app-2"); @@ -137,12 +139,12 @@ public void testUpdateSubscription_invalidTopic_forSubscription() throws Excepti fail("Expected NOT_FOUND as subscription does not match topic"); } catch (ResponseStatusException e) { - assertEquals(HttpStatus.NOT_FOUND, e.getStatus()); + assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode()); } } @Test - public void testUpdateSubscription_topicHasFlagNotSet() throws Exception { + void testUpdateSubscription_topicHasFlagNotSet() throws Exception { TopicMetadata topic1 = new TopicMetadata(); topic1.setName("topic-1"); topic1.setOwnerApplicationId("app-2"); @@ -164,7 +166,7 @@ public void testUpdateSubscription_topicHasFlagNotSet() throws Exception { fail("Expected BAD_REQUEST for updating subscription state for topic which does not require subscription approval"); } catch (ResponseStatusException e) { - assertEquals(HttpStatus.BAD_REQUEST, e.getStatus()); + assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); } } diff --git a/src/test/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionServiceImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionServiceImplTest.java index 18d43810..c022f16c 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionServiceImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/subscriptions/service/impl/SubscriptionServiceImplTest.java @@ -15,13 +15,15 @@ import com.hermesworld.ais.galapagos.topics.TopicMetadata; import com.hermesworld.ais.galapagos.topics.TopicType; import com.hermesworld.ais.galapagos.topics.service.TopicService; -import static org.junit.Assert.*; -import org.junit.Before; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class SubscriptionServiceImplTest { +class SubscriptionServiceImplTest { private KafkaClusters kafkaClusters; @@ -33,8 +35,8 @@ public class SubscriptionServiceImplTest { private TopicBasedRepositoryMock repository; - @Before - public void setupMocks() { + @BeforeEach + void setupMocks() { kafkaClusters = mock(KafkaClusters.class); KafkaCluster testCluster = mock(KafkaCluster.class); @@ -75,7 +77,7 @@ public void setupMocks() { } @Test - public void testAddSubscription_positive() throws Exception { + void testAddSubscription_positive() throws Exception { SubscriptionServiceImpl service = new SubscriptionServiceImpl(kafkaClusters, applicationsService, topicService, eventManager); @@ -93,7 +95,7 @@ public void testAddSubscription_positive() throws Exception { } @Test - public void testAddSubscription_pending() throws Exception { + void testAddSubscription_pending() throws Exception { SubscriptionServiceImpl service = new SubscriptionServiceImpl(kafkaClusters, applicationsService, topicService, eventManager); @@ -111,7 +113,7 @@ public void testAddSubscription_pending() throws Exception { } @Test - public void testAddSubscription_fail_internalTopic() throws Exception { + void testAddSubscription_fail_internalTopic() throws Exception { SubscriptionServiceImpl service = new SubscriptionServiceImpl(kafkaClusters, applicationsService, topicService, eventManager); @@ -127,7 +129,7 @@ public void testAddSubscription_fail_internalTopic() throws Exception { } @Test - public void testAddSubscription_directMetadata() throws Exception { + void testAddSubscription_directMetadata() throws Exception { SubscriptionMetadata sub = new SubscriptionMetadata(); sub.setId("123"); sub.setState(SubscriptionState.APPROVED); @@ -145,7 +147,7 @@ public void testAddSubscription_directMetadata() throws Exception { } @Test - public void testUpdateSubscriptionState_positive() throws Exception { + void testUpdateSubscriptionState_positive() throws Exception { SubscriptionMetadata sub = new SubscriptionMetadata(); sub.setId("123"); sub.setState(SubscriptionState.PENDING); @@ -168,7 +170,7 @@ public void testUpdateSubscriptionState_positive() throws Exception { } @Test - public void testUpdateSubscriptionState_rejected_deletes() throws Exception { + void testUpdateSubscriptionState_rejected_deletes() throws Exception { SubscriptionMetadata sub = new SubscriptionMetadata(); sub.setId("123"); sub.setState(SubscriptionState.PENDING); @@ -190,7 +192,7 @@ public void testUpdateSubscriptionState_rejected_deletes() throws Exception { } @Test - public void testUpdateSubscriptionState_canceled_deletes() throws Exception { + void testUpdateSubscriptionState_canceled_deletes() throws Exception { SubscriptionMetadata sub = new SubscriptionMetadata(); sub.setId("123"); sub.setState(SubscriptionState.APPROVED); @@ -212,7 +214,7 @@ public void testUpdateSubscriptionState_canceled_deletes() throws Exception { } @Test - public void testUpdateSubscriptionState_rejected_mapsToCanceled() throws Exception { + void testUpdateSubscriptionState_rejected_mapsToCanceled() throws Exception { SubscriptionMetadata sub = new SubscriptionMetadata(); sub.setId("123"); sub.setState(SubscriptionState.APPROVED); @@ -236,7 +238,7 @@ public void testUpdateSubscriptionState_rejected_mapsToCanceled() throws Excepti } @Test - public void testUpdateSubscriptionState_noop() throws Exception { + void testUpdateSubscriptionState_noop() throws Exception { SubscriptionMetadata sub = new SubscriptionMetadata(); sub.setId("123"); sub.setState(SubscriptionState.APPROVED); @@ -255,7 +257,7 @@ public void testUpdateSubscriptionState_noop() throws Exception { } @Test - public void testDeleteSubscription() throws Exception { + void testDeleteSubscription() throws Exception { SubscriptionMetadata sub = new SubscriptionMetadata(); sub.setId("123"); sub.setState(SubscriptionState.APPROVED); @@ -275,7 +277,7 @@ public void testDeleteSubscription() throws Exception { } @Test - public void testGetSubscriptionsForTopic() throws Exception { + void testGetSubscriptionsForTopic() throws Exception { SubscriptionMetadata sub1 = new SubscriptionMetadata(); sub1.setId("123"); sub1.setState(SubscriptionState.APPROVED); @@ -303,7 +305,7 @@ public void testGetSubscriptionsForTopic() throws Exception { } @Test - public void testGetSubscriptionsOfApplication() throws Exception { + void testGetSubscriptionsOfApplication() throws Exception { SubscriptionMetadata sub1 = new SubscriptionMetadata(); sub1.setId("123"); sub1.setState(SubscriptionState.APPROVED); diff --git a/src/test/java/com/hermesworld/ais/galapagos/topics/controller/TopicControllerTest.java b/src/test/java/com/hermesworld/ais/galapagos/topics/controller/TopicControllerTest.java index 5bda2328..c73dd0b1 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/topics/controller/TopicControllerTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/topics/controller/TopicControllerTest.java @@ -14,9 +14,9 @@ import com.hermesworld.ais.galapagos.topics.service.ValidatingTopicService; import com.hermesworld.ais.galapagos.util.FutureUtil; import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; @@ -26,11 +26,12 @@ import java.util.Map; import java.util.Optional; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.*; -public class TopicControllerTest { +class TopicControllerTest { @MockBean private KafkaClusters kafkaClusters; @@ -51,8 +52,8 @@ public class TopicControllerTest { private TopicBasedRepositoryMock topicRepository; - @Before - public void feedMocks() { + @BeforeEach + void feedMocks() { kafkaClusters = mock(KafkaClusters.class); applicationsService = mock(ApplicationsService.class); namingService = mock(NamingService.class); @@ -71,7 +72,7 @@ public void feedMocks() { @Test @DisplayName("it should not change the deprecation if topic description is changed") - public void testDontResetDeprecationWhenTopicDescChanges() { + void testDontResetDeprecationWhenTopicDescChanges() { TopicMetadata topic = new TopicMetadata(); topic.setOwnerApplicationId("app-1"); topic.setName("topic-1"); @@ -91,12 +92,12 @@ public void testDontResetDeprecationWhenTopicDescChanges() { controller.updateTopic("test", "topic-1", dto); verify(topicService, times(1)).updateTopicDescription("test", "topic-1", "updated description goes here"); - verify(topicService, times(0)).unmarkTopicDeprecated(anyString()); + verify(topicService, times(0)).unmarkTopicDeprecated(nullable(String.class)); } @Test @DisplayName("it should change the owner if current user is authorized") - public void testChangeTopicOwner_positive() throws Exception { + void testChangeTopicOwner_positive() throws Exception { TopicMetadata topic = new TopicMetadata(); topic.setName("topic-1"); topic.setOwnerApplicationId("app-1"); @@ -119,7 +120,7 @@ public void testChangeTopicOwner_positive() throws Exception { @Test @DisplayName("it should not change the owner if current user is not authorized") - public void testChangeTopicOwner_negative() { + void testChangeTopicOwner_negative() { TopicMetadata topic = new TopicMetadata(); topic.setName("topic-1"); topic.setOwnerApplicationId("app-1"); @@ -139,13 +140,13 @@ public void testChangeTopicOwner_negative() { fail("should fail because current user is not authorized"); } catch (ResponseStatusException e) { - assertEquals(HttpStatus.FORBIDDEN, e.getStatus()); + assertEquals(HttpStatus.FORBIDDEN, e.getStatusCode()); } } @Test @DisplayName("Can add producers for which I am not authorized") - public void testAddTopicProducer_notAuthorizedForProducer_positive() { + void testAddTopicProducer_notAuthorizedForProducer_positive() { ValidatingTopicService topicService = mock(ValidatingTopicService.class); TopicController controller = new TopicController(topicService, kafkaClusters, applicationsService, @@ -172,7 +173,7 @@ public void testAddTopicProducer_notAuthorizedForProducer_positive() { @Test @DisplayName("Cannot add producer if not authorized for topic (but for producer)") - public void testAddTopicProducer_notAuthorizedForTopic_negative() { + void testAddTopicProducer_notAuthorizedForTopic_negative() { ValidatingTopicService topicService = mock(ValidatingTopicService.class); TopicController controller = new TopicController(topicService, kafkaClusters, applicationsService, @@ -198,13 +199,13 @@ public void testAddTopicProducer_notAuthorizedForTopic_negative() { fail("ResponseStatusException expected, but adding producer succeeded"); } catch (ResponseStatusException e) { - assertEquals(HttpStatus.FORBIDDEN, e.getStatus()); + assertEquals(HttpStatus.FORBIDDEN, e.getStatusCode()); } } @Test @DisplayName("Can remove producers for which I am not authorized") - public void testRemoveTopicProducer_notAuthorizedForProducer_positive() { + void testRemoveTopicProducer_notAuthorizedForProducer_positive() { ValidatingTopicService topicService = mock(ValidatingTopicService.class); TopicController controller = new TopicController(topicService, kafkaClusters, applicationsService, @@ -229,7 +230,7 @@ public void testRemoveTopicProducer_notAuthorizedForProducer_positive() { @Test @DisplayName("user cant skip compability check if not admin") - public void testSkipCombatCheckForSchemas_userNotAuthorized() { + void testSkipCombatCheckForSchemas_userNotAuthorized() { ValidatingTopicService topicService = mock(ValidatingTopicService.class); TopicController controller = new TopicController(topicService, kafkaClusters, applicationsService, @@ -250,13 +251,13 @@ public void testSkipCombatCheckForSchemas_userNotAuthorized() { fail("HttpStatus.FORBIDDEN expected, but skipping check succeeded"); } catch (ResponseStatusException e) { - assertEquals(HttpStatus.FORBIDDEN, e.getStatus()); + assertEquals(HttpStatus.FORBIDDEN, e.getStatusCode()); } } @Test @DisplayName("Cannot remove producer if not authorized for topic (but for producer)") - public void testRemoveTopicProducer_notAuthorizedForTopic_negative() { + void testRemoveTopicProducer_notAuthorizedForTopic_negative() { ValidatingTopicService topicService = mock(ValidatingTopicService.class); TopicController controller = new TopicController(topicService, kafkaClusters, applicationsService, @@ -279,7 +280,7 @@ public void testRemoveTopicProducer_notAuthorizedForTopic_negative() { fail("ResponseStatusException expected, but removing producer succeeded"); } catch (ResponseStatusException e) { - assertEquals(HttpStatus.FORBIDDEN, e.getStatus()); + assertEquals(HttpStatus.FORBIDDEN, e.getStatusCode()); } } diff --git a/src/test/java/com/hermesworld/ais/galapagos/topics/impl/ValidatingTopicServiceImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/topics/impl/ValidatingTopicServiceImplTest.java index cbc4208d..762abb8e 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/topics/impl/ValidatingTopicServiceImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/topics/impl/ValidatingTopicServiceImplTest.java @@ -10,33 +10,32 @@ import com.hermesworld.ais.galapagos.topics.config.GalapagosTopicConfig; import com.hermesworld.ais.galapagos.topics.service.TopicService; import com.hermesworld.ais.galapagos.topics.service.impl.ValidatingTopicServiceImpl; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import java.time.LocalDate; import java.time.Period; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class ValidatingTopicServiceImplTest { +class ValidatingTopicServiceImplTest { private GalapagosTopicConfig topicConfig; - @Before - public void init() { + @BeforeEach + void init() { topicConfig = mock(GalapagosTopicConfig.class); when(topicConfig.getMinDeprecationTime()).thenReturn(Period.ofDays(10)); } @Test - public void testCannotDeleteSubscribedTopic() { + void testCannotDeleteSubscribedTopic() { TopicService topicService = mock(TopicService.class); SubscriptionService subscriptionService = mock(SubscriptionService.class); KafkaClusters clusters = mock(KafkaClusters.class); @@ -53,7 +52,7 @@ public void testCannotDeleteSubscribedTopic() { when(topicService.getTopic("_env1", "testtopic")).thenReturn(Optional.of(meta1)); when(subscriptionService.getSubscriptionsForTopic("_env1", "testtopic", false)) - .thenReturn(Collections.singletonList(subscription)); + .thenReturn(List.of(subscription)); ValidatingTopicServiceImpl service = new ValidatingTopicServiceImpl(topicService, subscriptionService, mock(ApplicationsService.class), clusters, topicConfig, false); @@ -62,7 +61,7 @@ public void testCannotDeleteSubscribedTopic() { } @Test - public void testCannotDeleteStagedPublicTopic() { + void testCannotDeleteStagedPublicTopic() { TopicService topicService = mock(TopicService.class); SubscriptionService subscriptionService = mock(SubscriptionService.class); KafkaClusters clusters = mock(KafkaClusters.class); @@ -85,7 +84,7 @@ public void testCannotDeleteStagedPublicTopic() { } @Test - public void canDeleteTopic_internal_positiv() { + void canDeleteTopic_internal_positiv() { TopicService topicService = mock(TopicService.class); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -110,7 +109,7 @@ public void canDeleteTopic_internal_positiv() { } @Test - public void canDeleteTopic_internal_negative() { + void canDeleteTopic_internal_negative() { TopicService topicService = mock(TopicService.class); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -136,7 +135,7 @@ public void canDeleteTopic_internal_negative() { @Test @DisplayName("Should throw Exception when trying to add Producer to Topic on staging-only Stage") - public void addTopicProducerOnOnlyStagingEnv_negative() { + void addTopicProducerOnOnlyStagingEnv_negative() { TopicService topicService = mock(TopicService.class); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -167,7 +166,7 @@ public void addTopicProducerOnOnlyStagingEnv_negative() { @Test @DisplayName("Should throw Exception when trying to delete Producer from Topic on staging-only Stage") - public void deleteProducerFromTopicOnOnlyStagingEnv_negative() { + void deleteProducerFromTopicOnOnlyStagingEnv_negative() { TopicService topicService = mock(TopicService.class); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -200,7 +199,7 @@ public void deleteProducerFromTopicOnOnlyStagingEnv_negative() { } @Test - public void canDeleteTopic_withSubscribersAndEolDatePast() { + void canDeleteTopic_withSubscribersAndEolDatePast() { TopicService topicService = mock(TopicService.class); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -220,7 +219,7 @@ public void canDeleteTopic_withSubscribersAndEolDatePast() { subscription.setClientApplicationId("2"); when(subscriptionService.getSubscriptionsForTopic("_env1", "testtopic", false)) - .thenReturn(Collections.singletonList(subscription)); + .thenReturn(List.of(subscription)); when(topicService.getTopic("_env1", "testtopic")).thenReturn(Optional.of(meta1)); ValidatingTopicServiceImpl service = new ValidatingTopicServiceImpl(topicService, subscriptionService, @@ -230,7 +229,7 @@ public void canDeleteTopic_withSubscribersAndEolDatePast() { } @Test - public void canDeleteTopic_withSubscribersAndEolDateInFuture() { + void canDeleteTopic_withSubscribersAndEolDateInFuture() { TopicService topicService = mock(TopicService.class); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -250,7 +249,7 @@ public void canDeleteTopic_withSubscribersAndEolDateInFuture() { subscription.setClientApplicationId("2"); when(subscriptionService.getSubscriptionsForTopic("_env1", "testtopic", false)) - .thenReturn(Collections.singletonList(subscription)); + .thenReturn(List.of(subscription)); when(topicService.getTopic("_env1", "testtopic")).thenReturn(Optional.of(meta1)); ValidatingTopicServiceImpl service = new ValidatingTopicServiceImpl(topicService, subscriptionService, diff --git a/src/test/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImplIntegrationTest.java b/src/test/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImplIntegrationTest.java index 8fb632bd..9272a374 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImplIntegrationTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImplIntegrationTest.java @@ -1,5 +1,6 @@ package com.hermesworld.ais.galapagos.topics.service.impl; +import com.hermesworld.ais.galapagos.GalapagosTestConfig; import com.hermesworld.ais.galapagos.applications.ApplicationsService; import com.hermesworld.ais.galapagos.changes.ChangeData; import com.hermesworld.ais.galapagos.events.*; @@ -14,10 +15,9 @@ import com.hermesworld.ais.galapagos.topics.config.GalapagosTopicConfig; import com.hermesworld.ais.galapagos.util.FutureUtil; import com.hermesworld.ais.galapagos.util.HasKey; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -26,7 +26,6 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; -import org.springframework.test.context.junit4.SpringRunner; import java.time.LocalDate; import java.util.ArrayList; @@ -38,7 +37,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -47,10 +47,9 @@ * Tests that Security Context data is passed correctly to event context, even when performing the quite complex, * chained markTopicDeprecated and unmarkTopicDeprecated operations. */ -@RunWith(SpringRunner.class) @SpringBootTest -@Import(TopicServiceImplIntegrationTest.TestEventListener.class) -public class TopicServiceImplIntegrationTest { +@Import({ GalapagosTestConfig.class, TopicServiceImplIntegrationTest.TestEventListener.class }) +class TopicServiceImplIntegrationTest { @Autowired private CurrentUserService currentUserService; @@ -78,8 +77,8 @@ public class TopicServiceImplIntegrationTest { private final TopicBasedRepositoryMock topicRepository2 = new DecoupledTopicBasedRepositoryMock<>( executorService); - @Before - public void feedMocks() { + @BeforeEach + void feedMocks() { KafkaCluster cluster1 = mock(KafkaCluster.class); KafkaCluster cluster2 = mock(KafkaCluster.class); @@ -103,14 +102,14 @@ public void feedMocks() { when(notificationService.notifySubscribers(any(), any(), any(), any())).thenReturn(FutureUtil.noop()); } - @After - public void shutdown() throws Exception { + @AfterEach + void shutdown() throws Exception { executorService.shutdown(); - executorService.awaitTermination(1, TimeUnit.MINUTES); + assertTrue(executorService.awaitTermination(1, TimeUnit.MINUTES)); } @Test - public void testTopicDeprecated_passesCurrentUser() throws Exception { + void testTopicDeprecated_passesCurrentUser() throws Exception { ApplicationsService applicationsService = mock(ApplicationsService.class); NamingService namingService = mock(NamingService.class); GalapagosTopicConfig topicSettings = mock(GalapagosTopicConfig.class); @@ -145,7 +144,7 @@ public void testTopicDeprecated_passesCurrentUser() throws Exception { } @Test - public void testTopicUndeprecated_passesCurrentUser() throws Exception { + void testTopicUndeprecated_passesCurrentUser() throws Exception { ApplicationsService applicationsService = mock(ApplicationsService.class); NamingService namingService = mock(NamingService.class); GalapagosTopicConfig topicSettings = mock(GalapagosTopicConfig.class); diff --git a/src/test/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImplTest.java b/src/test/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImplTest.java index e7c18508..26e30f50 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImplTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/topics/service/impl/TopicServiceImplTest.java @@ -18,11 +18,12 @@ import com.hermesworld.ais.galapagos.subscriptions.service.SubscriptionService; import com.hermesworld.ais.galapagos.topics.*; import com.hermesworld.ais.galapagos.topics.config.GalapagosTopicConfig; +import com.hermesworld.ais.galapagos.topics.config.TopicSchemaConfig; import com.hermesworld.ais.galapagos.util.FutureUtil; import org.json.JSONObject; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.mockito.invocation.InvocationOnMock; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.core.io.ClassPathResource; @@ -34,11 +35,11 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; -public class TopicServiceImplTest { +class TopicServiceImplTest { @MockBean private KafkaClusters kafkaClusters; @@ -59,8 +60,8 @@ public class TopicServiceImplTest { private TopicBasedRepositoryMock schemaRepository; - @Before - public void feedMocks() { + @BeforeEach + void feedMocks() { kafkaClusters = mock(KafkaClusters.class); applicationsService = mock(ApplicationsService.class); namingService = mock(NamingService.class); @@ -92,10 +93,11 @@ public void feedMocks() { when(topicConfig.getDefaultPartitionCount()).thenReturn(6); when(topicConfig.getStandardReplicationFactor()).thenReturn(2); when(topicConfig.getCriticalReplicationFactor()).thenReturn(4); + when(topicConfig.getSchemas()).thenReturn(new TopicSchemaConfig()); } @Test - public void testCreateTopic_positive() throws Exception { + void testCreateTopic_positive() throws Exception { List createInvs = new ArrayList<>(); when(kafkaTestCluster.createTopic(any(), any())).then(inv -> { @@ -135,7 +137,7 @@ public void testCreateTopic_positive() throws Exception { } @Test - public void testCreateTopic_downToMaxPartitions() throws Exception { + void testCreateTopic_downToMaxPartitions() throws Exception { List createInvs = new ArrayList<>(); when(kafkaTestCluster.createTopic(any(), any())).then(inv -> { @@ -163,7 +165,7 @@ public void testCreateTopic_downToMaxPartitions() throws Exception { } @Test - public void testCreateTopic_criticalReplicationFactor() throws Exception { + void testCreateTopic_criticalReplicationFactor() throws Exception { List createInvs = new ArrayList<>(); when(kafkaTestCluster.createTopic(any(), any())).then(inv -> { @@ -192,7 +194,7 @@ public void testCreateTopic_criticalReplicationFactor() throws Exception { } @Test - public void testCreateTopic_replicationFactor_downToNumBrokers() throws Exception { + void testCreateTopic_replicationFactor_downToNumBrokers() throws Exception { List createInvs = new ArrayList<>(); // 6 is more than the 5 brokers we have, so should be downed to 5 @@ -224,7 +226,7 @@ public void testCreateTopic_replicationFactor_downToNumBrokers() throws Exceptio } @Test - public void testCreateTopic_useDefaultPartitions() throws Exception { + void testCreateTopic_useDefaultPartitions() throws Exception { List createInvs = new ArrayList<>(); when(kafkaTestCluster.createTopic(any(), any())).then(inv -> { @@ -252,7 +254,7 @@ public void testCreateTopic_useDefaultPartitions() throws Exception { } @Test - public void testCreateTopic_nameValidationFails() throws Exception { + void testCreateTopic_nameValidationFails() throws Exception { List createInvs = new ArrayList<>(); when(kafkaTestCluster.createTopic(any(), any())).then(inv -> { @@ -284,7 +286,7 @@ public void testCreateTopic_nameValidationFails() throws Exception { @Test @DisplayName("should add producer to topic") - public void addTopicProducerTest_positive() throws Exception { + void addTopicProducerTest_positive() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -305,7 +307,7 @@ public void addTopicProducerTest_positive() throws Exception { @Test @DisplayName("should fail adding a producer to commands topic") - public void addTopicProducerTest_negative() throws Exception { + void addTopicProducerTest_negative() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -327,7 +329,7 @@ public void addTopicProducerTest_negative() throws Exception { @Test @DisplayName("should delete producer from topic") - public void deleteTopicProducersTest_positive() throws Exception { + void deleteTopicProducersTest_positive() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -349,7 +351,7 @@ public void deleteTopicProducersTest_positive() throws Exception { @Test @DisplayName("should not be able to delete producer from commands topic") - public void deleteTopicProducersTest_negative() throws Exception { + void deleteTopicProducersTest_negative() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -375,7 +377,7 @@ public void deleteTopicProducersTest_negative() throws Exception { @Test @DisplayName("should promote a producer to new Topic owner") - public void changeOwnerOfTopicTest_positive() throws Exception { + void changeOwnerOfTopicTest_positive() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -398,7 +400,7 @@ public void changeOwnerOfTopicTest_positive() throws Exception { @Test @DisplayName("should not promote a producer to new Topic owner for internal topics") - public void changeOwnerOfTopicTest_negative() throws Exception { + void changeOwnerOfTopicTest_negative() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -421,7 +423,7 @@ public void changeOwnerOfTopicTest_negative() throws Exception { } @Test - public void testDeleteLatestSchemaVersion() throws Exception { + void testDeleteLatestSchemaVersion() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -454,7 +456,7 @@ public void testDeleteLatestSchemaVersion() throws Exception { } @Test - public void testDeleteLatestSchemaVersionStaged_negative() throws Exception { + void testDeleteLatestSchemaVersionStaged_negative() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); KafkaCluster prodCluster = mock(KafkaCluster.class); @@ -496,7 +498,7 @@ public void testDeleteLatestSchemaVersionStaged_negative() throws Exception { } @Test - public void testDeleteLatestSchemaVersionWithSubscriber_negative() throws Exception { + void testDeleteLatestSchemaVersionWithSubscriber_negative() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -520,8 +522,7 @@ public void testDeleteLatestSchemaVersionWithSubscriber_negative() throws Except subscription.setClientApplicationId("2"); SubscriptionService subscriptionService = mock(SubscriptionService.class); - when(subscriptionService.getSubscriptionsForTopic("test", "topic-1", false)) - .thenReturn(Collections.singletonList(subscription)); + when(subscriptionService.getSubscriptionsForTopic("test", "topic-1", false)).thenReturn(List.of(subscription)); ValidatingTopicServiceImpl validatingService = new ValidatingTopicServiceImpl(service, subscriptionService, applicationsService, kafkaClusters, topicConfig, false); @@ -540,7 +541,7 @@ public void testDeleteLatestSchemaVersionWithSubscriber_negative() throws Except } @Test - public void testAddSchemaVersion_sameSchema() throws Exception { + void testAddSchemaVersion_sameSchema() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -572,7 +573,7 @@ public void testAddSchemaVersion_sameSchema() throws Exception { } @Test - public void testAddSchemaVersion_incompatibleSchema() throws Exception { + void testAddSchemaVersion_incompatibleSchema() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -605,7 +606,7 @@ public void testAddSchemaVersion_incompatibleSchema() throws Exception { @Test @DisplayName("should not to check for compatibility if skipCompatCheck is set to true") - public void testAddSchemaVersion_skipCompatibleSchemaCheckForAdmins() throws Exception { + void testAddSchemaVersion_skipCompatibleSchemaCheckForAdmins() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -627,7 +628,7 @@ public void testAddSchemaVersion_skipCompatibleSchemaCheckForAdmins() throws Exc } @Test - public void testAddSchemaVersion_withMetadata() throws Exception { + void testAddSchemaVersion_withMetadata() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -664,7 +665,7 @@ public void testAddSchemaVersion_withMetadata() throws Exception { } @Test - public void testAddSchemaVersion_withMetadata_illegalVersionNo_empty() throws Exception { + void testAddSchemaVersion_withMetadata_illegalVersionNo_empty() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -692,7 +693,7 @@ public void testAddSchemaVersion_withMetadata_illegalVersionNo_empty() throws Ex } @Test - public void testAddSchemaVersion_withMetadata_illegalVersionNo_notMatching() throws Exception { + void testAddSchemaVersion_withMetadata_illegalVersionNo_notMatching() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -729,7 +730,7 @@ public void testAddSchemaVersion_withMetadata_illegalVersionNo_notMatching() thr } @Test - public void testAddSchemaVersion_invalidSchema() throws Exception { + void testAddSchemaVersion_invalidSchema() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -751,7 +752,7 @@ public void testAddSchemaVersion_invalidSchema() throws Exception { } @Test - public void testAddSchemaVersion_invalidJson() throws Exception { + void testAddSchemaVersion_invalidJson() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -772,7 +773,7 @@ public void testAddSchemaVersion_invalidJson() throws Exception { } @Test - public void testAddSchemaVersion_DataObjectSimpleAtJSONSchema() throws Exception { + void testAddSchemaVersion_DataObjectSimpleAtJSONSchema() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -798,7 +799,7 @@ public void testAddSchemaVersion_DataObjectSimpleAtJSONSchema() throws Exception } @Test - public void testAddSchemaVersion_DataObjectNestedAtJSONSchema() throws Exception { + void testAddSchemaVersion_DataObjectNestedAtJSONSchema() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -818,7 +819,7 @@ public void testAddSchemaVersion_DataObjectNestedAtJSONSchema() throws Exception } @Test - public void testAddSchemaVersion_NoSchemaProp() throws Exception { + void testAddSchemaVersion_NoSchemaProp() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -844,7 +845,7 @@ public void testAddSchemaVersion_NoSchemaProp() throws Exception { } @Test - public void testSetSubscriptionApprovalRequired_positive() throws Exception { + void testSetSubscriptionApprovalRequired_positive() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -875,7 +876,7 @@ public void testSetSubscriptionApprovalRequired_positive() throws Exception { } @Test - public void testSetSubscriptionApprovalRequired_internalTopic() throws Exception { + void testSetSubscriptionApprovalRequired_internalTopic() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -898,7 +899,7 @@ public void testSetSubscriptionApprovalRequired_internalTopic() throws Exception } @Test - public void testSetSubscriptionApprovalRequired_noop() throws Exception { + void testSetSubscriptionApprovalRequired_noop() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -916,7 +917,7 @@ public void testSetSubscriptionApprovalRequired_noop() throws Exception { @Test @DisplayName("should stage new owner on all stages immediately") - public void testChangeOwnerStaging() throws Exception { + void testChangeOwnerStaging() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); KafkaCluster testCluster2 = mock(KafkaCluster.class); @@ -949,7 +950,7 @@ public void testChangeOwnerStaging() throws Exception { } @Test - public void testDeprecateTopic_positive() throws Exception { + void testDeprecateTopic_positive() throws Exception { KafkaCluster testCluster2 = mock(KafkaCluster.class); when(testCluster2.getId()).thenReturn("test2"); KafkaCluster testCluster3 = mock(KafkaCluster.class); @@ -985,7 +986,7 @@ public void testDeprecateTopic_positive() throws Exception { } @Test - public void testDeprecateTopic_noSuchTopic() throws Exception { + void testDeprecateTopic_noSuchTopic() throws Exception { KafkaCluster testCluster2 = mock(KafkaCluster.class); when(testCluster2.getId()).thenReturn("test2"); KafkaCluster testCluster3 = mock(KafkaCluster.class); @@ -1024,7 +1025,7 @@ public void testDeprecateTopic_noSuchTopic() throws Exception { } @Test - public void testunmarkTopicDeprecated() throws Exception { + void testunmarkTopicDeprecated() throws Exception { KafkaCluster testCluster2 = mock(KafkaCluster.class); when(testCluster2.getId()).thenReturn("test2"); KafkaCluster testCluster3 = mock(KafkaCluster.class); @@ -1055,7 +1056,7 @@ public void testunmarkTopicDeprecated() throws Exception { } @Test - public void testChangeDescOfTopic() throws Exception { + void testChangeDescOfTopic() throws Exception { TopicMetadata topic = new TopicMetadata(); topic.setName("topic-1"); @@ -1075,7 +1076,7 @@ public void testChangeDescOfTopic() throws Exception { } @Test - public void testAddSchemaVersion_DataObjectNestedAtJSONSchemaAndDataTopic() throws Exception { + void testAddSchemaVersion_DataObjectNestedAtJSONSchemaAndDataTopic() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -1095,7 +1096,7 @@ public void testAddSchemaVersion_DataObjectNestedAtJSONSchemaAndDataTopic() thro } @Test - public void testAddSchemaVersion_WithChangeDesc() throws Exception { + void testAddSchemaVersion_WithChangeDesc() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -1131,7 +1132,7 @@ public void testAddSchemaVersion_WithChangeDesc() throws Exception { } @Test - public void testAddSchemaVersion_WithChangeDesc_negative() throws Exception { + void testAddSchemaVersion_WithChangeDesc_negative() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -1160,7 +1161,7 @@ public void testAddSchemaVersion_WithChangeDesc_negative() throws Exception { } @Test - public void testDeleteSchemaWithSub_positive() throws Exception { + void testDeleteSchemaWithSub_positive() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -1185,8 +1186,7 @@ public void testDeleteSchemaWithSub_positive() throws Exception { subscription.setClientApplicationId("2"); SubscriptionService subscriptionService = mock(SubscriptionService.class); - when(subscriptionService.getSubscriptionsForTopic("test", "topic-1", false)) - .thenReturn(Collections.singletonList(subscription)); + when(subscriptionService.getSubscriptionsForTopic("test", "topic-1", false)).thenReturn(List.of(subscription)); ValidatingTopicServiceImpl validatingService = new ValidatingTopicServiceImpl(service, subscriptionService, applicationsService, kafkaClusters, topicConfig, true); @@ -1197,7 +1197,7 @@ public void testDeleteSchemaWithSub_positive() throws Exception { } @Test - public void testDeleteSchemaWithSub_negative() throws Exception { + void testDeleteSchemaWithSub_negative() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); @@ -1222,8 +1222,7 @@ public void testDeleteSchemaWithSub_negative() throws Exception { subscription.setClientApplicationId("2"); SubscriptionService subscriptionService = mock(SubscriptionService.class); - when(subscriptionService.getSubscriptionsForTopic("test", "topic-1", false)) - .thenReturn(Collections.singletonList(subscription)); + when(subscriptionService.getSubscriptionsForTopic("test", "topic-1", false)).thenReturn(List.of(subscription)); ValidatingTopicServiceImpl validatingService = new ValidatingTopicServiceImpl(service, subscriptionService, applicationsService, kafkaClusters, topicConfig, false); @@ -1240,7 +1239,7 @@ public void testDeleteSchemaWithSub_negative() throws Exception { } @Test - public void testDeleteLatestSchemaVersionStagedSchemaDeleteSub_negative() throws Exception { + void testDeleteLatestSchemaVersionStagedSchemaDeleteSub_negative() throws Exception { TopicServiceImpl service = new TopicServiceImpl(kafkaClusters, applicationsService, namingService, userService, topicConfig, eventManager); SubscriptionService subscriptionService = mock(SubscriptionService.class); @@ -1271,8 +1270,7 @@ public void testDeleteLatestSchemaVersionStagedSchemaDeleteSub_negative() throws subscription.setTopicName("topic-1"); subscription.setClientApplicationId("2"); - when(subscriptionService.getSubscriptionsForTopic("prod", "topic-1", false)) - .thenReturn(Collections.singletonList(subscription)); + when(subscriptionService.getSubscriptionsForTopic("prod", "topic-1", false)).thenReturn(List.of(subscription)); prodTopicRepository.save(topic1).get(); diff --git a/src/test/java/com/hermesworld/ais/galapagos/uisupport/controller/CustomLinksConfigTest.java b/src/test/java/com/hermesworld/ais/galapagos/uisupport/controller/CustomLinksConfigTest.java index e0a85a64..7a7b83c1 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/uisupport/controller/CustomLinksConfigTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/uisupport/controller/CustomLinksConfigTest.java @@ -1,45 +1,54 @@ package com.hermesworld.ais.galapagos.uisupport.controller; -import java.util.Collections; - -import org.junit.Test; - import com.hermesworld.ais.galapagos.uisupport.controller.CustomLinkConfig; + +import static org.junit.jupiter.api.Assertions.assertThrows; import com.hermesworld.ais.galapagos.uisupport.controller.CustomLinksConfig; import com.hermesworld.ais.galapagos.uisupport.controller.LinkType; +import org.junit.jupiter.api.Test; -public class CustomLinksConfigTest { +import java.util.List; - @Test(expected = RuntimeException.class) - public void testCustomLinksConfigID() { - CustomLinkConfig customLinkConfig = generatedCustomLinkConfig(null, "www.test.de", "Test-Label", - LinkType.OTHER); - new CustomLinksConfig().setLinks(Collections.singletonList(customLinkConfig)); +class CustomLinksConfigTest { + + @Test + void testCustomLinksConfigID() { + assertThrows(RuntimeException.class, () -> { + CustomLinkConfig customLinkConfig = generatedCustomLinkConfig(null, "www.test.de", "Test-Label", + LinkType.OTHER); + new CustomLinksConfig().setLinks(List.of(customLinkConfig)); + }); } - @Test(expected = RuntimeException.class) - public void testCustomLinksConfigH_Ref() { - CustomLinkConfig customLinkConfig = generatedCustomLinkConfig("42", null, "Test-Label", LinkType.OTHER); - new CustomLinksConfig().setLinks(Collections.singletonList(customLinkConfig)); + @Test + void testCustomLinksConfigH_Ref() { + assertThrows(RuntimeException.class, () -> { + CustomLinkConfig customLinkConfig = generatedCustomLinkConfig("42", null, "Test-Label", LinkType.OTHER); + new CustomLinksConfig().setLinks(List.of(customLinkConfig)); + }); } - @Test(expected = RuntimeException.class) - public void testCustomLinksConfigLabel() { - CustomLinkConfig customLinkConfig = generatedCustomLinkConfig("42", "www.test.de", null, LinkType.OTHER); - new CustomLinksConfig().setLinks(Collections.singletonList(customLinkConfig)); + @Test + void testCustomLinksConfigLabel() { + assertThrows(RuntimeException.class, () -> { + CustomLinkConfig customLinkConfig = generatedCustomLinkConfig("42", "www.test.de", null, LinkType.OTHER); + new CustomLinksConfig().setLinks(List.of(customLinkConfig)); + }); } - @Test(expected = RuntimeException.class) - public void testCustomLinksConfigLinkType() { - CustomLinkConfig customLinkConfig = generatedCustomLinkConfig("42", "www.test.de", "Test-Label", null); - new CustomLinksConfig().setLinks(Collections.singletonList(customLinkConfig)); + @Test + void testCustomLinksConfigLinkType() { + assertThrows(RuntimeException.class, () -> { + CustomLinkConfig customLinkConfig = generatedCustomLinkConfig("42", "www.test.de", "Test-Label", null); + new CustomLinksConfig().setLinks(List.of(customLinkConfig)); + }); } @Test - public void testCustomLinkConfigPositive() { + void testCustomLinkConfigPositive() { CustomLinkConfig customLinkConfig = generatedCustomLinkConfig("42", "www.test.de", "Test-Label", LinkType.OTHER); - new CustomLinksConfig().setLinks(Collections.singletonList(customLinkConfig)); + new CustomLinksConfig().setLinks(List.of(customLinkConfig)); } private CustomLinkConfig generatedCustomLinkConfig(String id, String href, String label, LinkType linkType) { diff --git a/src/test/java/com/hermesworld/ais/galapagos/uisupport/controller/UISupportControllerTest.java b/src/test/java/com/hermesworld/ais/galapagos/uisupport/controller/UISupportControllerTest.java index 3ca05d53..2f79fde7 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/uisupport/controller/UISupportControllerTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/uisupport/controller/UISupportControllerTest.java @@ -1,21 +1,22 @@ package com.hermesworld.ais.galapagos.uisupport.controller; +import com.hermesworld.ais.galapagos.GalapagosTestConfig; import com.hermesworld.ais.galapagos.kafka.KafkaClusters; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.context.annotation.Import; import java.util.List; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.*; -@RunWith(SpringRunner.class) @SpringBootTest -public class UISupportControllerTest { +@Import(GalapagosTestConfig.class) +class UISupportControllerTest { + @SuppressWarnings("unused") @MockBean private KafkaClusters kafkaClusters; @@ -23,26 +24,25 @@ public class UISupportControllerTest { private UISupportController testController; @Test - public void testCustomLinks() { + void testCustomLinks() { List links = testController.getCustomLinks(); assertNotNull(links); - for (int i = 0; i < links.size(); i++) { - assertNotNull(links.get(i).getId()); + for (CustomLinkConfig link : links) { + assertNotNull(link.getId()); - assertNotNull(links.get(i).getHref()); - assertFalse(links.get(i).getHref().isBlank()); + assertNotNull(link.getHref()); + assertFalse(link.getHref().isBlank()); - assertNotNull(links.get(i).getLabel()); - assertFalse(links.get(i).getLabel().isBlank()); - - assertNotNull(links.get(i).getLinkType()); + assertNotNull(link.getLabel()); + assertFalse(link.getLabel().isBlank()); + assertNotNull(link.getLinkType()); } } @Test - public void testKafkaDoc() { + void testKafkaDoc() { List result = new UISupportController(null, null, null, null, null, null, null) .getSupportedKafkaConfigs(); assertNotNull(result); diff --git a/src/test/java/com/hermesworld/ais/galapagos/util/CnUtilTest.java b/src/test/java/com/hermesworld/ais/galapagos/util/CnUtilTest.java index ab98abda..385faab5 100644 --- a/src/test/java/com/hermesworld/ais/galapagos/util/CnUtilTest.java +++ b/src/test/java/com/hermesworld/ais/galapagos/util/CnUtilTest.java @@ -1,13 +1,13 @@ package com.hermesworld.ais.galapagos.util; -import static org.junit.Assert.assertEquals; +import org.junit.jupiter.api.Test; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; -public class CnUtilTest { +class CnUtilTest { @Test - public void testToAppCn() { + void testToAppCn() { assertEquals("alpha", CertificateUtil.toAppCn("ALPHA")); assertEquals("track_trace", CertificateUtil.toAppCn("Track & Trace")); assertEquals("elisa", CertificateUtil.toAppCn(" Elisa ")); diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 36e914fe..81b8a632 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,7 +1,17 @@ spring.datasource.url=jdbc:h2:file:./target/test-db;DB_CLOSE_ON_EXIT=TRUE;AUTO_RECONNECT=TRUE spring.jpa.hibernate.ddl-auto=update -keycloak.configurationFile=classpath:keycloak.json +galapagos.security.jwt-email-claim=email +galapagos.security.jwt-display-name-claim=name +galapagos.security.jwt-role-claim=roles +galapagos.security.jwt-user-name-claim=username +spring.security.oauth2.client.registration.keycloak.client-id=test-webapp +spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email,offline_access +spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code + +spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8089/realms/galapagos +spring.security.oauth2.client.provider.keycloak.user-name-attribute=username + spring.mail.host= spring.mail.port=25 diff --git a/src/test/resources/keycloak.json b/src/test/resources/keycloak.json deleted file mode 100644 index 470f42b7..00000000 --- a/src/test/resources/keycloak.json +++ /dev/null @@ -1,8 +0,0 @@ -{ -"auth-server-url":"http://localhost:7080/auth", -"realm":"galapagos", -"resource":"galapagos-webapp", -"public-client": true, -"use-resource-role-mappings": true, -"principal-attribute": "preferred_username" -} diff --git a/src/test/resources/schema-compatibility/test10a.schema.json b/src/test/resources/schema-compatibility/test10a.schema.json new file mode 100644 index 00000000..902c27ab --- /dev/null +++ b/src/test/resources/schema-compatibility/test10a.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] +} diff --git a/src/test/resources/schema-compatibility/test10b.schema.json b/src/test/resources/schema-compatibility/test10b.schema.json new file mode 100644 index 00000000..64001757 --- /dev/null +++ b/src/test/resources/schema-compatibility/test10b.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "country": { + "type": "string" + } + }, + "required": [ + "name" + ] +} diff --git a/start-demo.sh b/start-demo.sh index 823165b5..943ec165 100755 --- a/start-demo.sh +++ b/start-demo.sh @@ -34,7 +34,7 @@ echo "Starting Galapagos (via Maven). Stop the application any time with Ctrl+C. echo "" echo "Use user1/user1 or admin1/admin1 for login at http://localhost:8080, once the application runs." echo "" -./mvnw package spring-boot:run -DskipTests -Dspring-boot.run.profiles=democonf,demo,actuator +KEYCLOAK_URL=http://localhost:8089 KEYCLOAK_CLIENT_ID=galapagos-webapp-dev ./mvnw package spring-boot:run -DskipTests -Dspring-boot.run.profiles=democonf,demo,oauth2,actuator echo "Shutting down Keycloak..." kill "$KEYCLOAK_PID" 2>/dev/null diff --git a/ui/package-lock.json b/ui/package-lock.json index 0b953d07..4edb2e3a 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -24,12 +24,12 @@ "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", "@popperjs/core": "^2.11.6", + "angular-oauth2-oidc": "^14.0.0", "core-js": "3.21.1", "file-saver": "^2.0.5", "highlight.js": "^11.4.0", - "keycloak-angular": "^12.1.0", "keycloak-js": "^19.0.2", - "luxon": "^2.3.1", + "luxon": "^2.5.2", "ngx-highlightjs": "^6.1.1", "rxjs": "7.5.4", "ts-md5": "^1.2.11", @@ -131,15 +131,15 @@ "dev": true }, "node_modules/@angular-devkit/build-angular": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.2.3.tgz", - "integrity": "sha512-Gun2WBM9oXqgOmpwan0OC5OEW2RY6Sd6nrOGzdC5HkvvwxLBV5uycrpYVJiQSPLuQjDLp9S2QTjA2yLtVABYCA==", + "version": "14.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.2.11.tgz", + "integrity": "sha512-O3X7GXcCBCGceVSHT+GIJ2JrRCg2YcO7HtNavpmPrraNr1o+aCdTkmT5WTS2cqWkZBm/z0wqKR8PsX/ZoD2r1A==", "dev": true, "dependencies": { "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1402.3", - "@angular-devkit/build-webpack": "0.1402.3", - "@angular-devkit/core": "14.2.3", + "@angular-devkit/architect": "0.1402.11", + "@angular-devkit/build-webpack": "0.1402.11", + "@angular-devkit/core": "14.2.11", "@babel/core": "7.18.10", "@babel/generator": "7.18.12", "@babel/helper-annotate-as-pure": "7.18.6", @@ -150,7 +150,7 @@ "@babel/runtime": "7.18.9", "@babel/template": "7.18.10", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "14.2.3", + "@ngtools/webpack": "14.2.11", "ansi-colors": "4.1.3", "babel-loader": "8.2.5", "babel-plugin-istanbul": "6.1.1", @@ -168,7 +168,7 @@ "less": "4.1.3", "less-loader": "11.0.0", "license-webpack-plugin": "4.0.2", - "loader-utils": "3.2.0", + "loader-utils": "3.2.1", "mini-css-extract-plugin": "2.6.1", "minimatch": "5.1.0", "open": "8.4.0", @@ -193,7 +193,7 @@ "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.4.0", - "webpack": "5.74.0", + "webpack": "5.76.1", "webpack-dev-middleware": "5.3.3", "webpack-dev-server": "4.11.0", "webpack-merge": "5.8.0", @@ -238,6 +238,47 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { + "version": "0.1402.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.11.tgz", + "integrity": "sha512-RuSZrBQ+QbipAESZ4aXCyAMQHaEaDyyV/FDS9J2HJWfEFbRD5oxlEt/tBC8XjmJQsktaUOh07GT8MNJjPKVAQw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "14.2.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { + "version": "14.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.11.tgz", + "integrity": "sha512-cBIGs6y9rykOQqnuAQOB1DgIRyBFYtvKRJb7QNUfIJ0qUfARKkuV/yikv3lrb95ePGkmoRzmjkFqcFZiYU+r7A==", + "dev": true, + "dependencies": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -274,12 +315,12 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1402.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1402.3.tgz", - "integrity": "sha512-d7ZG7dZElJgtPbp2x2dzMv6usqqzz9CH+RtaGueuivIa/Cd061c3D0pi3XuUBvfaS0qENrlnysYhLkuTnUQGcQ==", + "version": "0.1402.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1402.11.tgz", + "integrity": "sha512-Ajyg1O6B6JSHsDlPdh165uy3glW4IiUlRXu8VVAOSA88WIT1Dl17f4Oun0/t27ip0/CNceiVY9MzOqIwGL1E6g==", "dev": true, "dependencies": { - "@angular-devkit/architect": "0.1402.3", + "@angular-devkit/architect": "0.1402.11", "rxjs": "6.6.7" }, "engines": { @@ -292,6 +333,47 @@ "webpack-dev-server": "^4.0.0" } }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { + "version": "0.1402.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.11.tgz", + "integrity": "sha512-RuSZrBQ+QbipAESZ4aXCyAMQHaEaDyyV/FDS9J2HJWfEFbRD5oxlEt/tBC8XjmJQsktaUOh07GT8MNJjPKVAQw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "14.2.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { + "version": "14.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.11.tgz", + "integrity": "sha512-cBIGs6y9rykOQqnuAQOB1DgIRyBFYtvKRJb7QNUfIJ0qUfARKkuV/yikv3lrb95ePGkmoRzmjkFqcFZiYU+r7A==", + "dev": true, + "dependencies": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -3107,9 +3189,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.3.tgz", - "integrity": "sha512-/9bOlmpx7a5P8QhjmggxEJ6LX5qvfkBZhxM8Orjr6ZjJcmAfm+3wiUDzU3EM+5M0YV3y3+dvQpn6Jrwy9y4rfQ==", + "version": "14.2.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.11.tgz", + "integrity": "sha512-4enbLFAp98uTgWYF6OFceQqLcfv2/0brIrNN4iWT9xe/Mh3zdCt+eH42zvNRsqo9WXNWRSLvnx8I924p83LNlw==", "dev": true, "engines": { "node": "^14.15.0 || >=16.10.0", @@ -3555,6 +3637,17 @@ "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", "dev": true }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -3656,21 +3749,21 @@ "dev": true }, "node_modules/@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", "dev": true, "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", "dev": true, "dependencies": { "@types/node": "*", @@ -3685,9 +3778,9 @@ "dev": true }, "node_modules/@types/http-proxy": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "version": "1.17.10", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", + "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", "dev": true, "dependencies": { "@types/node": "*" @@ -3726,12 +3819,28 @@ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, + "node_modules/@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/@types/node": { "version": "17.0.45", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", "dev": true }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -3787,9 +3896,9 @@ } }, "node_modules/@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", "dev": true, "dependencies": { "@types/mime": "*", @@ -3806,9 +3915,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", "dev": true, "dependencies": { "@types/node": "*" @@ -4422,9 +4531,9 @@ } }, "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -4436,12 +4545,12 @@ } }, "node_modules/adm-zip": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", - "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", "dev": true, "engines": { - "node": ">=0.3.0" + "node": ">=6.0" } }, "node_modules/agent-base": { @@ -4537,6 +4646,18 @@ "ajv": "^8.8.2" } }, + "node_modules/angular-oauth2-oidc": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-14.0.1.tgz", + "integrity": "sha512-2DgIqGapAQYSYwgMmMv5Ef7BfpGO7DJvU3kZyjL7Z2aMbpacuzia17eUb2Y/lwqrzJZyMl/sgacAE1SJI4lQ4w==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=14.0.0", + "@angular/core": ">=14.0.0" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -4610,6 +4731,21 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -4722,6 +4858,17 @@ "node": ">=0.8" } }, + "node_modules/async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4817,9 +4964,9 @@ } }, "node_modules/babel-loader/node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -4995,9 +5142,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -5008,7 +5155,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", + "qs": "6.11.0", "raw-body": "2.5.1", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -5034,9 +5181,9 @@ "dev": true }, "node_modules/bonjour-service": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz", - "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", "dev": true, "dependencies": { "array-flatten": "^2.1.2", @@ -5283,6 +5430,25 @@ "node": ">=6" } }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001412", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz", @@ -5486,9 +5652,9 @@ } }, "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, "node_modules/colors": { @@ -6157,10 +6323,39 @@ "node": ">=0.10.0" } }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true, "engines": { "node": ">=0.10" @@ -6423,9 +6618,9 @@ "dev": true }, "node_modules/dns-packet": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", - "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", + "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", "dev": true, "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" @@ -6646,9 +6841,9 @@ } }, "node_modules/engine.io": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", - "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", + "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", "dev": true, "dependencies": { "@types/cookie": "^0.4.1", @@ -7910,14 +8105,14 @@ } }, "node_modules/express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.5.0", @@ -7936,7 +8131,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -8406,6 +8601,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "globule": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8445,6 +8676,17 @@ "node": ">=8.0.0" } }, + "node_modules/get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -8545,6 +8787,70 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globule": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", + "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "glob": "~7.1.1", + "lodash": "^4.17.21", + "minimatch": "~3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/globule/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/globule/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globule/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -8608,6 +8914,17 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -8742,10 +9059,38 @@ "resolved": "https://registry.npmjs.org/highlightjs-line-numbers.js/-/highlightjs-line-numbers.js-2.8.0.tgz", "integrity": "sha512-TEf1gw0c8mb8nan0QwliqS7obT4cpUd9hzsGzsZLweteNnWea/VIqy5/aQqsa5wnz9lnvmtAkS1ZtDTjB/goYQ==" }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, "dependencies": { "inherits": "^2.0.1", @@ -8755,9 +9100,9 @@ } }, "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "dependencies": { "core-util-is": "~1.0.0", @@ -8854,9 +9199,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "node_modules/http-deceiver": { @@ -8910,6 +9255,22 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/http-proxy-middleware": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", @@ -9527,6 +9888,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -10021,6 +10393,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/js-sdsl": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz", @@ -10107,9 +10487,9 @@ "dev": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": { "json5": "lib/cli.js" }, @@ -10438,20 +10818,6 @@ "node": ">=10" } }, - "node_modules/keycloak-angular": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/keycloak-angular/-/keycloak-angular-12.1.0.tgz", - "integrity": "sha512-ykEoEC4hRMlKLNLJd3kSGr0DHYX1NYeHcNj9NEaPLKwhbz95Bf5cwzYwoqn0oo07OqB3e9r5ZzgXsiQRKwMebg==", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/common": "^14", - "@angular/core": "^14", - "@angular/router": "^14", - "keycloak-js": "^18 || ^19" - } - }, "node_modules/keycloak-js": { "version": "19.0.2", "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-19.0.2.tgz", @@ -10637,9 +11003,9 @@ } }, "node_modules/loader-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz", - "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", "dev": true, "engines": { "node": ">= 12.13.0" @@ -10796,9 +11162,9 @@ } }, "node_modules/luxon": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.0.tgz", - "integrity": "sha512-IDkEPB80Rb6gCAU+FEib0t4FeJ4uVOuX1CQ9GsvU3O+JAGIgu0J7sf1OarXKaKDygTZIoJyU6YdZzTFRu+YR0A==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz", + "integrity": "sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA==", "engines": { "node": ">=12" } @@ -10844,6 +11210,184 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -10854,9 +11398,9 @@ } }, "node_modules/memfs": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.7.tgz", - "integrity": "sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.0.tgz", + "integrity": "sha512-yK6o8xVJlQerz57kvPROwTMgx5WtGwC2ZxDtOUsnGl49rHjYkfQoPNZPCKH73VdLE1BwBu/+Fx/NL8NYMUw2aA==", "dev": true, "dependencies": { "fs-monkey": "^1.0.3" @@ -10865,6 +11409,48 @@ "node": ">= 4.0.0" } }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -10950,6 +11536,17 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz", @@ -11011,6 +11608,22 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/minipass": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", @@ -11035,6 +11648,25 @@ "node": ">= 8" } }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, "node_modules/minipass-flush": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", @@ -11130,7 +11762,15 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/nanoid": { + "node_modules/nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", @@ -11339,6 +11979,32 @@ "node": ">= 6.13.0" } }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, "node_modules/node-gyp-build": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", @@ -11350,29 +12016,272 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" }, "node_modules/node-sass": { - "name": "sass", - "version": "1.55.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz", - "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-7.0.3.tgz", + "integrity": "sha512-8MIlsY/4dXUkJDYht9pIWBhMil3uHmE8b/AdJPjmFn1nBx9X9BASzfzmsCy0uCCb8eqI3SYYzVPDswWqSx7gjw==", "dev": true, + "hasInstallScript": true, "optional": true, "peer": true, "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" + "async-foreach": "^0.1.3", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "lodash": "^4.17.15", + "meow": "^9.0.0", + "nan": "^2.13.2", + "node-gyp": "^8.4.1", + "npmlog": "^5.0.0", + "request": "^2.88.0", + "sass-graph": "^4.0.1", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" }, "bin": { - "sass": "sass.js" + "node-sass": "bin/node-sass" }, "engines": { - "node": ">=12.0.0" + "node": ">=12" + } + }, + "node_modules/node-sass/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/node-sass/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-sass/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/node-sass/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/node-sass/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/node-sass/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-sass/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-sass/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-sass/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/nopt": { @@ -11390,6 +12299,23 @@ "node": ">=6" } }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -11626,6 +12552,20 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -13387,9 +14327,9 @@ } }, "node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, "dependencies": { "side-channel": "^1.0.4" @@ -13421,6 +14361,17 @@ } ] }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -13488,43 +14439,134 @@ "npm-normalize-package-bin": "^1.0.1" }, "engines": { - "node": ">=10" + "node": ">=10" + } + }, + "node_modules/read-package-json/node_modules/hosted-git-info": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.1.0.tgz", + "integrity": "sha512-Ek+QmMEqZF8XrbFdwoDjSbm7rT23pCgEMOJmz6GPk/s4yH//RQfNPArhIxbguNxROq/+5lNBwCDHMhA903Kx1Q==", + "dev": true, + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/read-package-json/node_modules/normalize-package-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", + "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", + "dev": true, + "dependencies": { + "hosted-git-info": "^5.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/read-package-json/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", + "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "dev": true, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-package-json/node_modules/hosted-git-info": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.1.0.tgz", - "integrity": "sha512-Ek+QmMEqZF8XrbFdwoDjSbm7rT23pCgEMOJmz6GPk/s4yH//RQfNPArhIxbguNxROq/+5lNBwCDHMhA903Kx1Q==", + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true, - "dependencies": { - "lru-cache": "^7.5.1" - }, + "optional": true, + "peer": true, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=8" } }, - "node_modules/read-package-json/node_modules/normalize-package-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-4.0.1.tgz", - "integrity": "sha512-EBk5QKKuocMJhB3BILuKhmaPjI8vNRSpIfO9woLC6NyHVkKKdVEdAO1mrT0ZfxNR1lKwCcTkuZfmGIFdizZ8Pg==", + "node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, + "optional": true, + "peer": true, "dependencies": { - "hosted-git-info": "^5.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, - "node_modules/read-package-json/node_modules/npm-normalize-package-bin": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-2.0.0.tgz", - "integrity": "sha512-awzfKUO7v0FscrSpRoogyNm0sajikhBWpU0QMrW09AMi9n1PoKU6WaIqUzuJSQnpciZZmJ/jMZ2Egfmb/9LiWQ==", + "node_modules/read-pkg/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "dev": true, + "optional": true, + "peer": true, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=8" } }, "node_modules/readable-stream": { @@ -13552,6 +14594,21 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -13823,9 +14880,9 @@ } }, "node_modules/resolve-url-loader/node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -14059,6 +15116,74 @@ "node": ">=12.0.0" } }, + "node_modules/sass-graph": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.1.tgz", + "integrity": "sha512-5YCfmGBmxoIRYHnKK2AKzrAkCoQ8ozO+iumT8K4tXJXRVCPf+7s1/9KxTSW3Rbvf+7Y7b4FR3mWyLnQr3PHocA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "glob": "^7.0.0", + "lodash": "^4.17.11", + "scss-tokenizer": "^0.4.3", + "yargs": "^17.2.1" + }, + "bin": { + "sassgraph": "bin/sassgraph" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/sass-graph/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/sass-graph/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sass-graph/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/sass-loader": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.0.2.tgz", @@ -14198,6 +15323,18 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/scss-tokenizer": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz", + "integrity": "sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "js-base64": "^2.4.9", + "source-map": "^0.7.3" + } + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -14638,6 +15775,22 @@ "npm": ">= 3.0.0" } }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -14838,6 +15991,45 @@ "node": ">= 0.6" } }, + "node_modules/stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/stdout-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stdout-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/streamroller": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.2.tgz", @@ -14951,6 +16143,20 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -15350,6 +16556,76 @@ "tree-kill": "cli.js" } }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "glob": "^7.1.2" + } + }, + "node_modules/true-case-path/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/true-case-path/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/true-case-path/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/ts-md5": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.3.1.tgz", @@ -15413,9 +16689,9 @@ } }, "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -15543,9 +16819,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", - "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", + "version": "0.7.35", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz", + "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==", "dev": true, "funding": [ { @@ -15834,12 +17110,12 @@ } }, "node_modules/webdriver-manager": { - "version": "12.1.8", - "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.8.tgz", - "integrity": "sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==", + "version": "12.1.9", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz", + "integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==", "dev": true, "dependencies": { - "adm-zip": "^0.4.9", + "adm-zip": "^0.5.2", "chalk": "^1.1.1", "del": "^2.2.0", "glob": "^7.0.3", @@ -15983,9 +17259,9 @@ } }, "node_modules/webpack": { - "version": "5.74.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", - "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -16053,15 +17329,15 @@ } }, "node_modules/webpack-dev-middleware/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -16125,17 +17401,17 @@ "optional": true } } - }, - "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" }, "engines": { "node": ">= 12.13.0" @@ -16146,16 +17422,16 @@ } }, "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", - "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "dev": true, "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -16594,15 +17870,15 @@ } }, "@angular-devkit/build-angular": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.2.3.tgz", - "integrity": "sha512-Gun2WBM9oXqgOmpwan0OC5OEW2RY6Sd6nrOGzdC5HkvvwxLBV5uycrpYVJiQSPLuQjDLp9S2QTjA2yLtVABYCA==", + "version": "14.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-14.2.11.tgz", + "integrity": "sha512-O3X7GXcCBCGceVSHT+GIJ2JrRCg2YcO7HtNavpmPrraNr1o+aCdTkmT5WTS2cqWkZBm/z0wqKR8PsX/ZoD2r1A==", "dev": true, "requires": { "@ampproject/remapping": "2.2.0", - "@angular-devkit/architect": "0.1402.3", - "@angular-devkit/build-webpack": "0.1402.3", - "@angular-devkit/core": "14.2.3", + "@angular-devkit/architect": "0.1402.11", + "@angular-devkit/build-webpack": "0.1402.11", + "@angular-devkit/core": "14.2.11", "@babel/core": "7.18.10", "@babel/generator": "7.18.12", "@babel/helper-annotate-as-pure": "7.18.6", @@ -16613,7 +17889,7 @@ "@babel/runtime": "7.18.9", "@babel/template": "7.18.10", "@discoveryjs/json-ext": "0.5.7", - "@ngtools/webpack": "14.2.3", + "@ngtools/webpack": "14.2.11", "ansi-colors": "4.1.3", "babel-loader": "8.2.5", "babel-plugin-istanbul": "6.1.1", @@ -16632,7 +17908,7 @@ "less": "4.1.3", "less-loader": "11.0.0", "license-webpack-plugin": "4.0.2", - "loader-utils": "3.2.0", + "loader-utils": "3.2.1", "mini-css-extract-plugin": "2.6.1", "minimatch": "5.1.0", "open": "8.4.0", @@ -16657,13 +17933,36 @@ "text-table": "0.2.0", "tree-kill": "1.2.2", "tslib": "2.4.0", - "webpack": "5.74.0", + "webpack": "5.76.1", "webpack-dev-middleware": "5.3.3", "webpack-dev-server": "4.11.0", "webpack-merge": "5.8.0", "webpack-subresource-integrity": "5.1.0" }, "dependencies": { + "@angular-devkit/architect": { + "version": "0.1402.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.11.tgz", + "integrity": "sha512-RuSZrBQ+QbipAESZ4aXCyAMQHaEaDyyV/FDS9J2HJWfEFbRD5oxlEt/tBC8XjmJQsktaUOh07GT8MNJjPKVAQw==", + "dev": true, + "requires": { + "@angular-devkit/core": "14.2.11", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/core": { + "version": "14.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.11.tgz", + "integrity": "sha512-cBIGs6y9rykOQqnuAQOB1DgIRyBFYtvKRJb7QNUfIJ0qUfARKkuV/yikv3lrb95ePGkmoRzmjkFqcFZiYU+r7A==", + "dev": true, + "requires": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + } + }, "rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -16695,15 +17994,38 @@ } }, "@angular-devkit/build-webpack": { - "version": "0.1402.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1402.3.tgz", - "integrity": "sha512-d7ZG7dZElJgtPbp2x2dzMv6usqqzz9CH+RtaGueuivIa/Cd061c3D0pi3XuUBvfaS0qENrlnysYhLkuTnUQGcQ==", + "version": "0.1402.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1402.11.tgz", + "integrity": "sha512-Ajyg1O6B6JSHsDlPdh165uy3glW4IiUlRXu8VVAOSA88WIT1Dl17f4Oun0/t27ip0/CNceiVY9MzOqIwGL1E6g==", "dev": true, "requires": { - "@angular-devkit/architect": "0.1402.3", + "@angular-devkit/architect": "0.1402.11", "rxjs": "6.6.7" }, "dependencies": { + "@angular-devkit/architect": { + "version": "0.1402.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1402.11.tgz", + "integrity": "sha512-RuSZrBQ+QbipAESZ4aXCyAMQHaEaDyyV/FDS9J2HJWfEFbRD5oxlEt/tBC8XjmJQsktaUOh07GT8MNJjPKVAQw==", + "dev": true, + "requires": { + "@angular-devkit/core": "14.2.11", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/core": { + "version": "14.2.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-14.2.11.tgz", + "integrity": "sha512-cBIGs6y9rykOQqnuAQOB1DgIRyBFYtvKRJb7QNUfIJ0qUfARKkuV/yikv3lrb95ePGkmoRzmjkFqcFZiYU+r7A==", + "dev": true, + "requires": { + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.1.0", + "rxjs": "6.6.7", + "source-map": "0.7.4" + } + }, "rxjs": { "version": "6.6.7", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", @@ -18617,9 +19939,9 @@ } }, "@ngtools/webpack": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.3.tgz", - "integrity": "sha512-/9bOlmpx7a5P8QhjmggxEJ6LX5qvfkBZhxM8Orjr6ZjJcmAfm+3wiUDzU3EM+5M0YV3y3+dvQpn6Jrwy9y4rfQ==", + "version": "14.2.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.2.11.tgz", + "integrity": "sha512-4enbLFAp98uTgWYF6OFceQqLcfv2/0brIrNN4iWT9xe/Mh3zdCt+eH42zvNRsqo9WXNWRSLvnx8I924p83LNlw==", "dev": true, "requires": {} }, @@ -18951,6 +20273,14 @@ "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", "dev": true }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "optional": true, + "peer": true + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -19052,21 +20382,21 @@ "dev": true }, "@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", "dev": true, "requires": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", + "version": "4.17.33", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", + "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", "dev": true, "requires": { "@types/node": "*", @@ -19081,9 +20411,9 @@ "dev": true }, "@types/http-proxy": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", + "version": "1.17.10", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz", + "integrity": "sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==", "dev": true, "requires": { "@types/node": "*" @@ -19122,12 +20452,28 @@ "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, + "@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "dev": true, + "optional": true, + "peer": true + }, "@types/node": { "version": "17.0.45", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", "dev": true }, + "@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true, + "optional": true, + "peer": true + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -19183,9 +20529,9 @@ } }, "@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", "dev": true, "requires": { "@types/mime": "*", @@ -19202,9 +20548,9 @@ } }, "@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", "dev": true, "requires": { "@types/node": "*" @@ -19632,9 +20978,9 @@ }, "dependencies": { "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -19645,9 +20991,9 @@ } }, "adm-zip": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", - "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", "dev": true }, "agent-base": { @@ -19718,6 +21064,14 @@ "fast-deep-equal": "^3.1.3" } }, + "angular-oauth2-oidc": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/angular-oauth2-oidc/-/angular-oauth2-oidc-14.0.1.tgz", + "integrity": "sha512-2DgIqGapAQYSYwgMmMv5Ef7BfpGO7DJvU3kZyjL7Z2aMbpacuzia17eUb2Y/lwqrzJZyMl/sgacAE1SJI4lQ4w==", + "requires": { + "tslib": "^2.0.0" + } + }, "ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -19767,6 +21121,18 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true }, + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, "arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -19852,6 +21218,14 @@ "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha512-VUeSMD8nEGBWaZK4lizI1sf3yEC7pnAQ/mrI7pC2fBz2s/tq5jWWEngTwaf0Gruu/OoXRGLGg1XFqpYBiGTYJA==", + "dev": true, + "optional": true, + "peer": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -19909,9 +21283,9 @@ }, "dependencies": { "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -20044,9 +21418,9 @@ } }, "body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", "dev": true, "requires": { "bytes": "3.1.2", @@ -20057,7 +21431,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", + "qs": "6.11.0", "raw-body": "2.5.1", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -20081,9 +21455,9 @@ } }, "bonjour-service": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.0.14.tgz", - "integrity": "sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", "dev": true, "requires": { "array-flatten": "^2.1.2", @@ -20268,6 +21642,19 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + } + }, "caniuse-lite": { "version": "1.0.30001412", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001412.tgz", @@ -20413,9 +21800,9 @@ "dev": true }, "colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, "colors": { @@ -20897,10 +22284,32 @@ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true }, + "decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "optional": true, + "peer": true + } + } + }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true }, "deep-is": { @@ -21107,9 +22516,9 @@ "dev": true }, "dns-packet": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", - "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", + "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", "dev": true, "requires": { "@leichtgewicht/ip-codec": "^2.0.1" @@ -21295,9 +22704,9 @@ } }, "engine.io": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz", - "integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz", + "integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==", "dev": true, "requires": { "@types/cookie": "^0.4.1", @@ -22157,14 +23566,14 @@ "dev": true }, "express": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", - "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "dev": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.0", + "body-parser": "1.20.1", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.5.0", @@ -22183,7 +23592,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", "proxy-addr": "~2.0.7", - "qs": "6.10.3", + "qs": "6.11.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.18.0", @@ -22534,6 +23943,36 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "globule": "^1.0.0" + } + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -22561,6 +24000,14 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", + "dev": true, + "optional": true, + "peer": true + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -22631,6 +24078,60 @@ "slash": "^3.0.0" } }, + "globule": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", + "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "glob": "~7.1.1", + "lodash": "^4.17.21", + "minimatch": "~3.0.2" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -22685,6 +24186,14 @@ } } }, + "hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "optional": true, + "peer": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -22785,6 +24294,30 @@ "resolved": "https://registry.npmjs.org/highlightjs-line-numbers.js/-/highlightjs-line-numbers.js-2.8.0.tgz", "integrity": "sha512-TEf1gw0c8mb8nan0QwliqS7obT4cpUd9hzsGzsZLweteNnWea/VIqy5/aQqsa5wnz9lnvmtAkS1ZtDTjB/goYQ==" }, + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -22798,9 +24331,9 @@ }, "dependencies": { "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -22876,9 +24409,9 @@ } }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "http-deceiver": { @@ -22925,6 +24458,19 @@ "requires-port": "^1.0.0" } }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, "http-proxy-middleware": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", @@ -23361,6 +24907,14 @@ "path-is-inside": "^1.0.1" } }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "optional": true, + "peer": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -23736,6 +25290,14 @@ } } }, + "js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "dev": true, + "optional": true, + "peer": true + }, "js-sdsl": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz", @@ -23810,9 +25372,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, "jsonc-parser": { "version": "3.1.0", @@ -24087,14 +25649,6 @@ "source-map-support": "^0.5.5" } }, - "keycloak-angular": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/keycloak-angular/-/keycloak-angular-12.1.0.tgz", - "integrity": "sha512-ykEoEC4hRMlKLNLJd3kSGr0DHYX1NYeHcNj9NEaPLKwhbz95Bf5cwzYwoqn0oo07OqB3e9r5ZzgXsiQRKwMebg==", - "requires": { - "tslib": "^2.3.0" - } - }, "keycloak-js": { "version": "19.0.2", "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-19.0.2.tgz", @@ -24225,9 +25779,9 @@ "dev": true }, "loader-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz", - "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", "dev": true }, "locate-path": { @@ -24347,9 +25901,9 @@ "dev": true }, "luxon": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.0.tgz", - "integrity": "sha512-IDkEPB80Rb6gCAU+FEib0t4FeJ4uVOuX1CQ9GsvU3O+JAGIgu0J7sf1OarXKaKDygTZIoJyU6YdZzTFRu+YR0A==" + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz", + "integrity": "sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA==" }, "magic-string": { "version": "0.26.2", @@ -24376,11 +25930,160 @@ } } }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "dependencies": { + "@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "minipass": "^3.1.1" + } + } + } + }, + "map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "optional": true, + "peer": true }, "media-typer": { "version": "0.3.0", @@ -24389,14 +26092,46 @@ "dev": true }, "memfs": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.7.tgz", - "integrity": "sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.0.tgz", + "integrity": "sha512-yK6o8xVJlQerz57kvPROwTMgx5WtGwC2ZxDtOUsnGl49rHjYkfQoPNZPCKH73VdLE1BwBu/+Fx/NL8NYMUw2aA==", "dev": true, "requires": { "fs-monkey": "^1.0.3" } }, + "meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "dependencies": { + "type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "optional": true, + "peer": true + } + } + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -24458,6 +26193,14 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "optional": true, + "peer": true + }, "mini-css-extract-plugin": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.1.tgz", @@ -24501,6 +26244,19 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + } + }, "minipass": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", @@ -24519,6 +26275,20 @@ "minipass": "^3.0.0" } }, + "minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, "minipass-flush": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", @@ -24593,6 +26363,14 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, + "nan": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", + "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "dev": true, + "optional": true, + "peer": true + }, "nanoid": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", @@ -24754,6 +26532,111 @@ "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "dev": true }, + "node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + } + } + }, "node-gyp-build": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", @@ -24766,15 +26649,130 @@ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==" }, "node-sass": { - "version": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz", - "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-7.0.3.tgz", + "integrity": "sha512-8MIlsY/4dXUkJDYht9pIWBhMil3uHmE8b/AdJPjmFn1nBx9X9BASzfzmsCy0uCCb8eqI3SYYzVPDswWqSx7gjw==", "dev": true, "optional": true, "peer": true, "requires": { - "chokidar": ">=3.0.0 <4.0.0", - "immutable": "^4.0.0", - "source-map-js": ">=0.6.2 <2.0.0" + "async-foreach": "^0.1.3", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "lodash": "^4.17.15", + "meow": "^9.0.0", + "nan": "^2.13.2", + "node-gyp": "^8.4.1", + "npmlog": "^5.0.0", + "request": "^2.88.0", + "sass-graph": "^4.0.1", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true, + "peer": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true, + "peer": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "nopt": { @@ -24786,6 +26784,20 @@ "abbrev": "1" } }, + "normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -24975,6 +26987,20 @@ "path-key": "^3.0.0" } }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -26194,9 +28220,9 @@ "dev": true }, "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dev": true, "requires": { "side-channel": "^1.0.4" @@ -26208,6 +28234,14 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "optional": true, + "peer": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -26295,6 +28329,83 @@ "npm-normalize-package-bin": "^1.0.1" } }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "optional": true, + "peer": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true, + "peer": true + }, + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "optional": true, + "peer": true + } + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -26314,6 +28425,18 @@ "picomatch": "^2.2.1" } }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + } + }, "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -26530,9 +28653,9 @@ }, "dependencies": { "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -26695,6 +28818,61 @@ "source-map-js": ">=0.6.2 <2.0.0" } }, + "sass-graph": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-4.0.1.tgz", + "integrity": "sha512-5YCfmGBmxoIRYHnKK2AKzrAkCoQ8ozO+iumT8K4tXJXRVCPf+7s1/9KxTSW3Rbvf+7Y7b4FR3mWyLnQr3PHocA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.17.11", + "scss-tokenizer": "^0.4.3", + "yargs": "^17.2.1" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, "sass-loader": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.0.2.tgz", @@ -26788,6 +28966,18 @@ } } }, + "scss-tokenizer": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.4.3.tgz", + "integrity": "sha512-raKLgf1LI5QMQnG+RxHz6oK0sL3x3I4FN2UDLqgLOGO8hodECNnNh5BXn7fAyBxrA8zVzdQizQ6XjNJQ+uBwMw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "js-base64": "^2.4.9", + "source-map": "^0.7.3" + } + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -27155,6 +29345,19 @@ "smart-buffer": "^4.2.0" } }, + "socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + } + }, "source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -27319,6 +29522,47 @@ "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "readable-stream": "^2.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "streamroller": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.2.tgz", @@ -27399,6 +29643,17 @@ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "min-indent": "^1.0.0" + } + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -27683,6 +29938,66 @@ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true }, + "trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "optional": true, + "peer": true + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "glob": "^7.1.2" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, "ts-md5": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.3.1.tgz", @@ -27722,9 +30037,9 @@ }, "dependencies": { "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" @@ -27816,9 +30131,9 @@ } }, "ua-parser-js": { - "version": "0.7.31", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", - "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", + "version": "0.7.35", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz", + "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==", "dev": true }, "unbox-primitive": { @@ -28029,12 +30344,12 @@ } }, "webdriver-manager": { - "version": "12.1.8", - "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.8.tgz", - "integrity": "sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==", + "version": "12.1.9", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz", + "integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==", "dev": true, "requires": { - "adm-zip": "^0.4.9", + "adm-zip": "^0.5.2", "chalk": "^1.1.1", "del": "^2.2.0", "glob": "^7.0.3", @@ -28144,9 +30459,9 @@ } }, "webpack": { - "version": "5.74.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", - "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", @@ -28233,15 +30548,15 @@ }, "dependencies": { "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" } } } @@ -28284,21 +30599,21 @@ }, "dependencies": { "schema-utils": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", - "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.1.tgz", + "integrity": "sha512-lELhBAAly9NowEsX0yZBlw9ahZG+sK/1RJ21EpzdYHKEs13Vku3LJ+MIPhh4sMs0oCCeufZQEQbMekiA4vuVIQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "ajv": "^8.8.0", + "ajv": "^8.9.0", "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.0.0" + "ajv-keywords": "^5.1.0" } }, "ws": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", - "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "dev": true, "requires": {} } diff --git a/ui/package.json b/ui/package.json index 2eeef79f..21443714 100644 --- a/ui/package.json +++ b/ui/package.json @@ -27,12 +27,12 @@ "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", "@popperjs/core": "^2.11.6", + "angular-oauth2-oidc": "^14.0.0", "core-js": "3.21.1", "file-saver": "^2.0.5", "highlight.js": "^11.4.0", - "keycloak-angular": "^12.1.0", "keycloak-js": "^19.0.2", - "luxon": "^2.3.1", + "luxon": "^2.5.2", "ngx-highlightjs": "^6.1.1", "rxjs": "7.5.4", "ts-md5": "^1.2.11", diff --git a/ui/proxy.conf.json b/ui/proxy.conf.json index 25cc18c6..b37caeab 100644 --- a/ui/proxy.conf.json +++ b/ui/proxy.conf.json @@ -15,8 +15,8 @@ "target": "http://localhost:8080", "secure": false }, - "/keycloak": { - "target": "http://localhost:8080", - "secure": false + "/oauth2": { + "target": "http://localhost:8080", + "secure": false } } diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts index 3d0fd334..eee58250 100644 --- a/ui/src/app/app-routing.module.ts +++ b/ui/src/app/app-routing.module.ts @@ -1,10 +1,9 @@ import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; +import { RouterModule, Routes } from '@angular/router'; import { AuthGuard } from './shared'; const routes: Routes = [ { path: '', loadChildren: () => import('./layout/layout.module').then(m => m.LayoutModule), canActivate: [AuthGuard] }, - { path: 'login', loadChildren: () => import('./login/login.module').then(m => m.LoginModule) }, { path: 'error', loadChildren: () => import('./server-error/server-error.module').then(m => m.ServerErrorModule) }, { path: 'access-denied', loadChildren: () => import('./access-denied/access-denied.module').then(m => m.AccessDeniedModule) }, { path: 'not-found', loadChildren: () => import('./not-found/not-found.module').then(m => m.NotFoundModule) }, diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 0680b43f..9f11dcbb 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -1 +1,6 @@ +
+
+
+
+
diff --git a/ui/src/app/app.component.spec.ts b/ui/src/app/app.component.spec.ts index 8c07140c..25388cce 100644 --- a/ui/src/app/app.component.spec.ts +++ b/ui/src/app/app.component.spec.ts @@ -3,6 +3,8 @@ import { APP_BASE_HREF } from '@angular/common'; import { AppComponent } from './app.component'; import { AppModule } from './app.module'; +import { AuthService } from './shared/services/auth.service'; +import { MockAuthService } from './shared/util/test-util'; describe('AppComponent', () => { let component: AppComponent; @@ -12,7 +14,8 @@ describe('AppComponent', () => { TestBed.configureTestingModule({ imports: [AppModule], providers: [ - { provide: APP_BASE_HREF, useValue: '/' } + { provide: APP_BASE_HREF, useValue: '/' }, + { provide: AuthService, useClass: MockAuthService } ] }).compileComponents(); } diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index 20d03bac..daa2f25f 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from '@angular/core'; +import { AuthService } from './shared/services/auth.service'; @Component({ selector: 'app-root', @@ -6,7 +7,7 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { - constructor() { + constructor(public authService: AuthService) { } ngOnInit() { diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 5d5dc60d..175441de 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -9,7 +9,6 @@ import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { AuthGuard } from './shared'; -import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular'; import { ApplicationsService } from './shared/services/applications.service'; import { EnvironmentsService } from './shared/services/environments.service'; import { ToastService } from './shared/modules/toast/toast.service'; @@ -21,7 +20,9 @@ import { getHighlightLanguages } from './layout/topics/topics.module'; import { ApiKeyService } from './shared/services/apikey.service'; import { CertificateService } from './shared/services/certificates.service'; -const keycloakService = new KeycloakService(); +import { OAuthModule, OAuthService } from 'angular-oauth2-oidc'; + +const isApiUrl = (url: string) => !url.startsWith('http') && url.indexOf('/api/') > -1; @NgModule({ imports: [ @@ -31,14 +32,16 @@ const keycloakService = new KeycloakService(); HttpClientModule, LanguageTranslationModule, AppRoutingModule, - KeycloakAngularModule + OAuthModule.forRoot({ + resourceServer: { + sendAccessToken: true, + customUrlValidation: isApiUrl + } + }) ], declarations: [AppComponent], providers: [AuthGuard, ApplicationsService, EnvironmentsService, TopicsService, ApiKeyService, ToastService, CertificateService, - ServerInfoService, { - provide: KeycloakService, - useValue: keycloakService - }, + ServerInfoService, OAuthService, { provide: HIGHLIGHT_OPTIONS, useValue: { @@ -51,23 +54,6 @@ const keycloakService = new KeycloakService(); export class AppModule implements DoBootstrap { ngDoBootstrap(app: ApplicationRef) { - // use fetch as it does not require any other services or modules to be loaded - fetch('/keycloak/config.json', { method: 'GET' }) - .then(resp => resp.json()) - .then(config => this.initKeycloak(config)) - .then(() => app.bootstrap(AppComponent)) - .catch(err => console.error('Could not initialize Keycloak. Application cannot be initialized')); - } - - private initKeycloak(config: any): Promise { - return keycloakService.init({ - config: config, - initOptions: { - onLoad: 'login-required', - checkLoginIframe: false - }, - enableBearerInterceptor: true, - bearerExcludedUrls: ['/assets'] - }); + app.bootstrap(AppComponent); } } diff --git a/ui/src/app/layout/admin/admin.component.html b/ui/src/app/layout/admin/admin.component.html index d26f47a9..5634bda6 100644 --- a/ui/src/app/layout/admin/admin.component.html +++ b/ui/src/app/layout/admin/admin.component.html @@ -1,6 +1,6 @@ -
+

{{ 'Administration' | translate }}

-
+
@@ -8,6 +8,20 @@

{{ 'Administration' | translate }}

{{ 'Application Owner Requests' | translate }}
+
+ +
+ +
+
@@ -20,12 +34,12 @@

{{ 'Administration' | translate }}

- + - + + +
{{ niceTimestamp(request.createdAt) | async }} - - {{ 'Administration' | translate }}
+ +
diff --git a/ui/src/app/layout/admin/admin.component.ts b/ui/src/app/layout/admin/admin.component.ts index 2af4b80c..595e8c8e 100644 --- a/ui/src/app/layout/admin/admin.component.ts +++ b/ui/src/app/layout/admin/admin.component.ts @@ -1,19 +1,37 @@ import { Component, OnInit } from '@angular/core'; import { routerTransition } from '../../router.animations'; -import { ApplicationInfo, ApplicationOwnerRequest, ApplicationsService } from '../../shared/services/applications.service'; -import { combineLatest, firstValueFrom, Observable, of } from 'rxjs'; +import { + ApplicationInfo, + ApplicationOwnerRequest, + ApplicationsService +} from '../../shared/services/applications.service'; +import { combineLatest, Observable, tap } from 'rxjs'; import { map } from 'rxjs/operators'; -import { KeycloakService } from 'keycloak-angular'; import { SortEvent } from './sortable.directive'; import { toNiceTimestamp } from 'src/app/shared/util/time-util'; import { TranslateService } from '@ngx-translate/core'; +import { NgbPaginationConfig } from '@ng-bootstrap/ng-bootstrap'; +import { SortDirection } from '../topics/sort'; +import { AuthService } from '../../shared/services/auth.service'; + interface TranslatedApplicationOwnerRequest extends ApplicationOwnerRequest { applicationName?: string; applicationInfoUrl?: string; } + +interface State { + currentPage: number; + totalItems: number; + pageSize: number; + maxSize: number; + searchTerm: string; + sortColumn: string; + sortDirection: SortDirection; +} + // TODO I think this could be moved to applicationService const translateApps: (requests: ApplicationOwnerRequest[], apps: ApplicationInfo[]) => TranslatedApplicationOwnerRequest[] = (requests, apps) => { @@ -40,23 +58,42 @@ const entityMap = { }) export class AdminComponent implements OnInit { - isAdmin = false; + isAdmin: Observable; - allRequests: Observable; + currentRequests: TranslatedApplicationOwnerRequest[]; - searchTerm: string; + allFetchedRequests: TranslatedApplicationOwnerRequest[]; + + state: State = { + currentPage: 1, // Current page number + pageSize: 15, // Number of items per page + maxSize: 5, // Maximum number of page links to display + totalItems: 0, // Total number of items + searchTerm: '', + sortColumn: '', + sortDirection: '' + }; - constructor(private applicationsService: ApplicationsService, private keycloakService: KeycloakService, - private translate: TranslateService) { + constructor(private applicationsService: ApplicationsService, authService: AuthService, + private translate: TranslateService, private config: NgbPaginationConfig) { + config.size = 'sm'; + config.boundaryLinks = true; + this.isAdmin = authService.admin; } - ngOnInit() { - this.isAdmin = this.keycloakService.getUserRoles().indexOf('admin') > -1; + async ngOnInit() { // TODO move this to applicationService, for all and for user requests - this.allRequests = combineLatest([this.applicationsService.getAllApplicationOwnerRequests(), + const allRequests = combineLatest([this.applicationsService.getAllApplicationOwnerRequests(), this.applicationsService.getAvailableApplications(false)]).pipe(map(values => translateApps(values[0], values[1]))) .pipe(map(values => values.map(req => this.escapeComments(req)))); + allRequests.pipe(tap(requests => { + this.allFetchedRequests = requests; + this.state.totalItems = requests.length; + this.sliceData(); + this.search(); + })).subscribe(); + this.applicationsService.refresh().then(); } @@ -70,13 +107,19 @@ export class AdminComponent implements OnInit { } async onSort({ column, direction }: SortEvent) { - const requests = await firstValueFrom(this.allRequests); + const requests = this.allFetchedRequests; if (direction === 'asc') { - this.allRequests = of(requests.sort((a, b) => a[column] < b[column] ? 1 : a[column] > b[column] ? -1 : 0)); + this.allFetchedRequests = requests.sort((a, b) => a[column] < b[column] ? 1 : a[column] > b[column] ? -1 : 0); } if (direction === 'desc') { - this.allRequests = of(requests.sort((a, b) => a[column] > b[column] ? 1 : a[column] < b[column] ? -1 : 0)); + this.allFetchedRequests = requests.sort((a, b) => a[column] > b[column] ? 1 : a[column] < b[column] ? -1 : 0); } + this.sliceData(); + } + + sliceData() { + this.currentRequests = this.allFetchedRequests.slice( + (this.state.currentPage-1)*this.state.pageSize, this.state.currentPage*this.state.pageSize); } lastChangeTitle(request: TranslatedApplicationOwnerRequest): Observable { @@ -103,4 +146,20 @@ export class AdminComponent implements OnInit { return String(source).replace(/[&<>"'\/]/g, s => entityMap[s]); } + matches(request: TranslatedApplicationOwnerRequest, searchTerm: string) { + return ( + (request.applicationName && request.applicationName.toLowerCase().includes(searchTerm.toLowerCase())) || + (request.applicationId && request.applicationId.toLowerCase().includes(searchTerm.toLowerCase())) || + (request.userName && request.userName.toLowerCase().includes(searchTerm.toLowerCase())) || + (request.comments && request.comments.toLowerCase().includes(searchTerm.toLowerCase())) + ); + } + + search() { + const { pageSize, searchTerm } = this.state; + const filterData = this.allFetchedRequests.filter(request => this.matches(request, searchTerm)); + this.state.totalItems = filterData.length; + this.currentRequests = filterData.slice(0, pageSize); + } + } diff --git a/ui/src/app/layout/components/header/header.component.ts b/ui/src/app/layout/components/header/header.component.ts index d78902b3..85745de0 100644 --- a/ui/src/app/layout/components/header/header.component.ts +++ b/ui/src/app/layout/components/header/header.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit } from '@angular/core'; import { ServerInfoService } from '../../../shared/services/serverinfo.service'; import { NavigationEnd, Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; -import { KeycloakService } from 'keycloak-angular'; import { Observable } from 'rxjs'; import { EnvironmentsService, KafkaEnvironment } from 'src/app/shared/services/environments.service'; import { map } from 'rxjs/operators'; +import { TranslateService } from '@ngx-translate/core'; +import { AuthService } from '../../../shared/services/auth.service'; @Component({ selector: 'app-header', @@ -15,7 +15,7 @@ import { map } from 'rxjs/operators'; export class HeaderComponent implements OnInit { public pushRightClass: string; - public userName: Promise; + public userName: Observable; instanceNameInfo: Observable; @@ -29,8 +29,8 @@ export class HeaderComponent implements OnInit { darkmodeActive: boolean; - constructor(private translate: TranslateService, public router: Router, private keycloak: KeycloakService, - private environments: EnvironmentsService, private serverInfoService: ServerInfoService) { + constructor(private translate: TranslateService, public router: Router, private authService: AuthService, + private environments: EnvironmentsService, private serverInfoService: ServerInfoService) { } @@ -39,8 +39,7 @@ export class HeaderComponent implements OnInit { this.instanceNameInfo = this.serverInfoService.getServerInfo().pipe(map(info => info.galapagos.instanceName)); - this.userName = Promise.resolve(this.keycloak.getKeycloakInstance().idTokenParsed.given_name - + ' ' + this.keycloak.getKeycloakInstance().idTokenParsed.family_name); + this.userName = this.authService.userProfile.pipe(map(profile => profile.displayName)); this.router.events.subscribe(val => { if ( @@ -79,7 +78,7 @@ export class HeaderComponent implements OnInit { } async onLoggedout() { - return this.keycloak.logout(); + return this.authService.logout(); } changeLang(language: string) { diff --git a/ui/src/app/layout/components/sidebar/sidebar.component.html b/ui/src/app/layout/components/sidebar/sidebar.component.html index 2a339ef9..c43890ff 100644 --- a/ui/src/app/layout/components/sidebar/sidebar.component.html +++ b/ui/src/app/layout/components/sidebar/sidebar.component.html @@ -21,7 +21,8 @@ {{ 'Staging' | translate }} - {{ 'Administration' | translate }} diff --git a/ui/src/app/layout/components/sidebar/sidebar.component.spec.ts b/ui/src/app/layout/components/sidebar/sidebar.component.spec.ts index f2e4f4df..f6fd0bf8 100644 --- a/ui/src/app/layout/components/sidebar/sidebar.component.spec.ts +++ b/ui/src/app/layout/components/sidebar/sidebar.component.spec.ts @@ -6,8 +6,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { PageHeaderModule } from '../../../shared/modules'; -import { KeycloakService } from 'keycloak-angular'; +import { PageHeaderModule } from '../../../shared'; import { By } from '@angular/platform-browser'; import { Routes } from '@angular/router'; import { AdminComponent } from '../../admin/admin.component'; @@ -16,10 +15,14 @@ import { DashboardModule } from '../../dashboard/dashboard.module'; import { Location } from '@angular/common'; import { ApplicationsService } from '../../../shared/services/applications.service'; import { DashboardComponent } from '../../dashboard/dashboard.component'; +import { AuthService } from '../../../shared/services/auth.service'; +import { BehaviorSubject } from 'rxjs'; +import { MockAuthService } from '../../../shared/util/test-util'; describe('SidebarComponent', () => { let component: SidebarComponent; let fixture: ComponentFixture; + const admin = new BehaviorSubject(true); const routes: Routes = [ { path: 'admin', component: AdminComponent }, { path: 'dashboard', component: DashboardComponent } @@ -40,7 +43,7 @@ describe('SidebarComponent', () => { ], declarations: [SidebarComponent], providers: [TranslateService, - KeycloakService, + { provide: AuthService, useClass: MockAuthService }, Location, ApplicationsService ] @@ -48,6 +51,8 @@ describe('SidebarComponent', () => { fixture = TestBed.createComponent(SidebarComponent); component = fixture.componentInstance; + const auth = fixture.debugElement.injector.get(AuthService); + auth.admin = admin; }); it('should create Sidebar Component', () => { @@ -55,15 +60,13 @@ describe('SidebarComponent', () => { }); it('should not display Administration Section when user is no admin', (() => { - const keycloak = fixture.debugElement.injector.get(KeycloakService); - spyOn(keycloak, 'getUserRoles').and.returnValue(['user']); + admin.next(false); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('#adminSection'))).toBeNull(); })); it('should display Administration Section when user is admin', (() => { - const keycloak = fixture.debugElement.injector.get(KeycloakService); - spyOn(keycloak, 'getUserRoles').and.returnValue(['admin']); + admin.next(true); fixture.detectChanges(); expect(fixture.debugElement.query(By.css('#adminSection'))).not.toBeNull(); })); diff --git a/ui/src/app/layout/components/sidebar/sidebar.component.ts b/ui/src/app/layout/components/sidebar/sidebar.component.ts index 02ce9a7a..a932b9c0 100644 --- a/ui/src/app/layout/components/sidebar/sidebar.component.ts +++ b/ui/src/app/layout/components/sidebar/sidebar.component.ts @@ -1,7 +1,10 @@ -import { Component, Output, EventEmitter, OnInit } from '@angular/core'; -import { Router, NavigationEnd } from '@angular/router'; +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { KeycloakService } from 'keycloak-angular'; +import { AuthService } from '../../../shared/services/auth.service'; +import { Observable } from 'rxjs'; + +const pushRightClass = 'push-right'; @Component({ selector: 'app-sidebar', @@ -14,11 +17,10 @@ export class SidebarComponent implements OnInit { isActive: boolean; collapsed: boolean; showMenu: string; - pushRightClass: string; - isAdmin: boolean; + isAdmin: Observable; - constructor(private translate: TranslateService, public router: Router, private keycloakService: KeycloakService) { + constructor(private translate: TranslateService, public router: Router, private authService: AuthService) { this.router.events.subscribe(val => { if ( val instanceof NavigationEnd && @@ -30,24 +32,11 @@ export class SidebarComponent implements OnInit { }); } - ngOnInit() { + async ngOnInit() { this.isActive = false; this.collapsed = false; this.showMenu = ''; - this.pushRightClass = 'push-right'; - this.isAdmin = this.keycloakService.getUserRoles().indexOf('admin') > -1; - } - - eventCalled() { - this.isActive = !this.isActive; - } - - addExpandClass(element: any) { - if (element === this.showMenu) { - this.showMenu = '0'; - } else { - this.showMenu = element; - } + this.isAdmin = this.authService.admin; } toggleCollapsed() { @@ -57,24 +46,12 @@ export class SidebarComponent implements OnInit { isToggled(): boolean { const dom: Element = document.querySelector('body'); - return dom.classList.contains(this.pushRightClass); + return dom.classList.contains(pushRightClass); } toggleSidebar() { const dom: any = document.querySelector('body'); - dom.classList.toggle(this.pushRightClass); + dom.classList.toggle(pushRightClass); } - rltAndLtr() { - const dom: any = document.querySelector('body'); - dom.classList.toggle('rtl'); - } - - changeLang(language: string) { - this.translate.use(language); - } - - onLoggedout() { - localStorage.removeItem('isLoggedin'); - } } diff --git a/ui/src/app/layout/createtopic/createtopic.component.spec.ts b/ui/src/app/layout/createtopic/createtopic.component.spec.ts index 3591265a..2fae609c 100644 --- a/ui/src/app/layout/createtopic/createtopic.component.spec.ts +++ b/ui/src/app/layout/createtopic/createtopic.component.spec.ts @@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { LanguageTranslationModule } from '../../shared/modules/language-translation/language-translation.module'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { KeycloakService } from 'keycloak-angular'; import { ToastService } from '../../shared/modules/toast/toast.service'; import { EnvironmentsService } from '../../shared/services/environments.service'; import { ApplicationsService } from '../../shared/services/applications.service'; @@ -17,6 +16,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; import { ApiKeyService } from '../../shared/services/apikey.service'; import { CertificateService } from '../../shared/services/certificates.service'; +import { AuthService } from '../../shared/services/auth.service'; +import { MockAuthService } from '../../shared/util/test-util'; describe('CreateTopicComponent', () => { let component: CreateTopicComponent; @@ -36,7 +37,7 @@ describe('CreateTopicComponent', () => { ], declarations: [CreateTopicComponent], providers: [TranslateService, - KeycloakService, + { provide: AuthService, useClass: MockAuthService }, ToastService, TopicsService, ApiKeyService, diff --git a/ui/src/app/layout/dashboard/dashboard.component.spec.ts b/ui/src/app/layout/dashboard/dashboard.component.spec.ts index 2d3288b6..ab48ef87 100644 --- a/ui/src/app/layout/dashboard/dashboard.component.spec.ts +++ b/ui/src/app/layout/dashboard/dashboard.component.spec.ts @@ -11,8 +11,7 @@ import { Location } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { firstValueFrom, of } from 'rxjs'; -import { LoginComponent } from '../../login/login.component'; -import { PageHeaderModule } from '../../shared/modules'; +import { PageHeaderModule } from '../../shared'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { By } from '@angular/platform-browser'; import { LanguageTranslationModule } from '../../shared/modules/language-translation/language-translation.module'; @@ -39,8 +38,7 @@ describe('DashboardComponent', () => { ApplicationsService, ServerInfoService, Location, - TranslateService, - LoginComponent + TranslateService ] }).compileComponents(); diff --git a/ui/src/app/layout/layout.component.html b/ui/src/app/layout/layout.component.html index b718cbf9..84a8ca08 100644 --- a/ui/src/app/layout/layout.component.html +++ b/ui/src/app/layout/layout.component.html @@ -1,9 +1,13 @@ - - -
-
- {{ t.toast.message }} -
- -
+ + + +
+
+ {{ t.toast.message }} +
+ +
+
diff --git a/ui/src/app/layout/layout.component.spec.ts b/ui/src/app/layout/layout.component.spec.ts index 06e3f7d7..1b5a7c9a 100644 --- a/ui/src/app/layout/layout.component.spec.ts +++ b/ui/src/app/layout/layout.component.spec.ts @@ -2,12 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { RouterTestingModule } from '@angular/router/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { By } from '@angular/platform-browser'; -import { Router, Routes } from '@angular/router'; import { Location } from '@angular/common'; -import { NgZone } from '@angular/core'; import { LayoutComponent } from './layout.component'; import { LanguageTranslationModule } from '../shared/modules/language-translation/language-translation.module'; import { PageHeaderModule } from '../shared'; @@ -17,25 +13,19 @@ import { SidebarComponent } from './components/sidebar/sidebar.component'; import { GalapagosToastComponent } from '../shared/modules/toast/toast.component'; import { ToastService } from '../shared/modules/toast/toast.service'; import { EnvironmentsService } from '../shared/services/environments.service'; -import { KeycloakService } from 'keycloak-angular'; import { ApplicationsService } from '../shared/services/applications.service'; import { ServerInfoService } from '../shared/services/serverinfo.service'; import { StagingModule } from './staging/staging.module'; -import { AdminComponent } from './admin/admin.component'; -import { KeycloakInstance } from 'keycloak-js'; +import { AuthService } from '../shared/services/auth.service'; +import { MockAuthService } from '../shared/util/test-util'; describe('LayoutComponent', () => { let component: LayoutComponent; let fixture: ComponentFixture; - let keycloak: KeycloakService; - const routes: Routes = [ - { path: 'admin', component: AdminComponent } - ]; beforeEach(() => { TestBed.configureTestingModule({ imports: [ - RouterTestingModule.withRoutes(routes), TranslateModule.forRoot(), LanguageTranslationModule, NgbModule, @@ -47,7 +37,7 @@ describe('LayoutComponent', () => { ], declarations: [LayoutComponent, HeaderComponent, SidebarComponent, GalapagosToastComponent], providers: [TranslateService, - KeycloakService, + { provide: AuthService, useClass: MockAuthService }, Location, ToastService, EnvironmentsService, @@ -58,28 +48,9 @@ describe('LayoutComponent', () => { fixture = TestBed.createComponent(LayoutComponent); component = fixture.componentInstance; - keycloak = fixture.debugElement.injector.get(KeycloakService); - const token = { - // eslint-disable-next-line @typescript-eslint/naming-convention - given_name: 'John', - // eslint-disable-next-line @typescript-eslint/naming-convention - family_name: 'Doe' - }; - const ki: jasmine.SpyObj = jasmine.createSpyObj([], { idTokenParsed: token }); - spyOn(keycloak, 'getKeycloakInstance').and.returnValue(ki); }); it('should create Layout Component', () => { expect(component).toBeTruthy(); }); - - it('should not be null when navigation to administration section as admin', (() => { - const ngZone = TestBed.get(NgZone); - const router = TestBed.get(Router); - spyOn(keycloak, 'getUserRoles').and.returnValue(['admin']); - - ngZone.run(() => router.navigate(['admin'])).then(); - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('#adminSection'))).not.toBeNull(); - })); }); diff --git a/ui/src/app/layout/layout.component.ts b/ui/src/app/layout/layout.component.ts index 2ab49b03..7196e7b9 100644 --- a/ui/src/app/layout/layout.component.ts +++ b/ui/src/app/layout/layout.component.ts @@ -1,8 +1,9 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { trigger, style, state, transition, animate } from '@angular/animations'; +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { animate, state, style, transition, trigger } from '@angular/animations'; import { Toast, ToastService } from '../shared/modules/toast/toast.service'; import { Subscription } from 'rxjs'; import { ToastHideEvent } from '../shared/modules/toast/toast.component'; +import { AuthService } from '../shared/services/auth.service'; interface DisplayToast { toast: Toast; @@ -34,7 +35,8 @@ export class LayoutComponent implements OnInit, OnDestroy { private toastSubscription: Subscription; - constructor(private toastService: ToastService) {} + constructor(private toastService: ToastService, public authService: AuthService) { + } ngOnInit() { this.toastSubscription = this.toastService.getToasts().subscribe(toast => this.addToast(toast)); diff --git a/ui/src/app/layout/topics/schemasection/schema-section.component.html b/ui/src/app/layout/topics/schemasection/schema-section.component.html index 9e6beea2..9248cc23 100644 --- a/ui/src/app/layout/topics/schemasection/schema-section.component.html +++ b/ui/src/app/layout/topics/schemasection/schema-section.component.html @@ -57,7 +57,7 @@ [dismissible]="false">

-
+
diff --git a/ui/src/app/login/login.component.scss b/ui/src/app/login/login.component.scss deleted file mode 100644 index bf6d7e2f..00000000 --- a/ui/src/app/login/login.component.scss +++ /dev/null @@ -1,95 +0,0 @@ -$topnav-background-color: #222; -:host { - display: block; -} -.login-page { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - overflow: auto; - background: $topnav-background-color; - text-align: center; - color: #fff; - padding: 3em; - .col-lg-4 { - padding: 0; - } - .input-lg { - height: 46px; - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; - border-radius: 0; - } - .input-underline { - background: 0 0; - border: none; - box-shadow: none; - border-bottom: 2px solid rgba(255, 255, 255, 0.5); - color: #fff; - border-radius: 0; - } - .input-underline:focus { - border-bottom: 2px solid #fff; - box-shadow: none; - } - .rounded-btn { - -webkit-border-radius: 50px; - border-radius: 50px; - color: rgba(255, 255, 255, 0.8); - background: $topnav-background-color; - border: 2px solid rgba(255, 255, 255, 0.8); - font-size: 18px; - line-height: 40px; - padding: 0 25px; - } - .rounded-btn:hover, - .rounded-btn:focus, - .rounded-btn:active, - .rounded-btn:visited { - color: rgba(255, 255, 255, 1); - border: 2px solid rgba(255, 255, 255, 1); - outline: none; - } - - h1 { - font-weight: 300; - margin-top: 20px; - margin-bottom: 10px; - font-size: 36px; - small { - color: rgba(255, 255, 255, 0.7); - } - } - - .form-group { - padding: 8px 0; - input::-webkit-input-placeholder { - color: rgba(255, 255, 255, 0.6) !important; - } - - input:-moz-placeholder { - /* Firefox 18- */ - color: rgba(255, 255, 255, 0.6) !important; - } - - input::-moz-placeholder { - /* Firefox 19+ */ - color: rgba(255, 255, 255, 0.6) !important; - } - - input:-ms-input-placeholder { - color: rgba(255, 255, 255, 0.6) !important; - } - } - .form-content { - padding: 40px 0; - } - .user-avatar { - -webkit-border-radius: 50%; - border-radius: 50%; - border: 2px solid #fff; - } -} diff --git a/ui/src/app/login/login.component.spec.ts b/ui/src/app/login/login.component.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/ui/src/app/login/login.component.ts b/ui/src/app/login/login.component.ts deleted file mode 100644 index 144b4728..00000000 --- a/ui/src/app/login/login.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { routerTransition } from '../router.animations'; - -@Component({ - selector: 'app-login', - templateUrl: './login.component.html', - styleUrls: ['./login.component.scss'], - animations: [routerTransition()] -}) -export class LoginComponent implements OnInit { - constructor( - public router: Router - ) {} - - ngOnInit() {} - - onLoggedin() { - localStorage.setItem('isLoggedin', 'true'); - } -} diff --git a/ui/src/app/login/login.module.spec.ts b/ui/src/app/login/login.module.spec.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/ui/src/app/login/login.module.ts b/ui/src/app/login/login.module.ts deleted file mode 100644 index c9338562..00000000 --- a/ui/src/app/login/login.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { TranslateModule } from '@ngx-translate/core'; - -import { LoginRoutingModule } from './login-routing.module'; -import { LoginComponent } from './login.component'; - -@NgModule({ - imports: [ - CommonModule, - TranslateModule, - LoginRoutingModule], - declarations: [LoginComponent] -}) -export class LoginModule {} diff --git a/ui/src/app/shared/guard/auth.guard.ts b/ui/src/app/shared/guard/auth.guard.ts index 957013d6..e82f02d5 100644 --- a/ui/src/app/shared/guard/auth.guard.ts +++ b/ui/src/app/shared/guard/auth.guard.ts @@ -1,32 +1,32 @@ import { Injectable } from '@angular/core'; -import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { Router } from '@angular/router'; -import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular'; +import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { AuthService } from '../services/auth.service'; +import { firstValueFrom, map } from 'rxjs'; @Injectable() -export class AuthGuard extends KeycloakAuthGuard implements CanActivate { - constructor(protected router: Router, protected keycloakAngular: KeycloakService) { - super(router, keycloakAngular); +export class AuthGuard implements CanActivate { + + constructor(private router: Router, private authService: AuthService) { } - isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { - return new Promise((resolve, reject) => { - if (!this.authenticated) { - this.keycloakAngular.login() - .catch(e => console.error(e)); - return reject(false); - } - - const requiredRoles: string[] = route.data.roles; - if (!requiredRoles || requiredRoles.length === 0) { - return resolve(true); - } else { - if (!this.roles || this.roles.length === 0) { - resolve(false); - } - resolve(requiredRoles.every(role => this.roles.indexOf(role) > -1)); - } - }); + async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { + const authenticated = await this.authService.tryLoginFromData(); + + if (!authenticated) { + return this.authService.login(state.url); + } + + const requiredRoles: string[] = route.data.roles; + + if (!requiredRoles || requiredRoles.length === 0) { + return Promise.resolve(true); + } + return this.checkRoles(requiredRoles); } + + private checkRoles(requiredRoles: string[]): Promise { + return firstValueFrom(this.authService.roles.pipe(map(roles => requiredRoles.every(role => roles.indexOf(role) > -1)))); + } + } diff --git a/ui/src/app/shared/services/auth.service.ts b/ui/src/app/shared/services/auth.service.ts new file mode 100644 index 00000000..cabd42aa --- /dev/null +++ b/ui/src/app/shared/services/auth.service.ts @@ -0,0 +1,191 @@ +import { Injectable } from '@angular/core'; +import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; +import { BehaviorSubject, firstValueFrom, Observable, skipWhile, tap } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { HttpClient } from '@angular/common/http'; +import { LocationStrategy } from '@angular/common'; +import { Router } from '@angular/router'; + +const emptyUserProfile = { userName: '', displayName: '' }; + +const loginHandlerUri = '/dashboard'; + +export interface UserProfile { + + userName: string; + + displayName: string; + +} + +@Injectable({ providedIn: 'root' }) +export class AuthService { + + authenticated: Observable; + + admin: Observable; + + roles: Observable; + + userProfile: Observable; + + private authenticatedSubject = new BehaviorSubject(false); + + private rolesSubject = new BehaviorSubject([]); + + private profileSubject = new BehaviorSubject(emptyUserProfile); + + private userNameClaim: string = null; + + private displayNameClaim: string = null; + + private rolesClaim: string = null; + + private configLoaded = new BehaviorSubject(false); + + constructor(private oauthService: OAuthService, private http: HttpClient, private locationStrategy: LocationStrategy, + private router: Router) { + this.authenticated = this.authenticatedSubject.asObservable(); + this.roles = this.rolesSubject.asObservable(); + this.admin = this.roles.pipe(map(roles => !!roles.find(r => r.toUpperCase() === 'ADMIN'))); + this.userProfile = this.profileSubject.asObservable(); + + this.loadAuthConfig().then(config => { + this.configure(config); + this.configLoaded.next(true); + }); + + window.addEventListener('storage', event => { + // The `key` is `null` if the event was caused by `.clear()` + if (event.key !== 'access_token' && event.key !== null) { + return; + } + + this.checkAuthenticated(); + }); + } + + async tryLoginFromData(): Promise { + await firstValueFrom(this.getConfigLoadedObservable()); + + if (this.checkAuthenticated()) { + return Promise.resolve(true); + } + + await this.oauthService.tryLoginCodeFlow().catch(e => console.error('Could not extract login information', e)); + if (this.checkAuthenticated()) { + return this.router.navigateByUrl(decodeURIComponent(this.oauthService.state)); + } + + return Promise.resolve(false); + } + + async login(targetUrl: string): Promise { + await firstValueFrom(this.getConfigLoadedObservable()); + return this.oauthService.loadDiscoveryDocumentAndLogin({ + state: targetUrl + }); + } + + public logout() { + this.oauthService.logOut(); + } + + private getConfigLoadedObservable(): Observable { + return this.configLoaded.asObservable().pipe(skipWhile(v => !v)); + } + + private loadAuthConfig(): Promise { + return firstValueFrom(this.http.get('/oauth2/config.json') + .pipe(tap((data: any) => this.extractClaimNames(data))) + .pipe(map((data: any) => ({ + issuer: data.issuerUri, + + tokenEndpoint: data.tokenEndpoint, + + redirectUri: window.location.origin, + + clientId: data.clientId, + + responseType: 'code', + + scope: (data.scope as string[]).join(' '), + + preserveRequestedRoute: false, + + postLogoutRedirectUri: '/logout' + })))); + } + + private configure(config: AuthConfig) { + this.oauthService.configure(config); + this.oauthService.redirectUri = window.location.origin + this.locationStrategy.prepareExternalUrl(loginHandlerUri); + this.oauthService.events + .pipe(filter(e => ['token_received'].includes(e.type))) + .subscribe(() => this.checkAuthenticated()); + this.oauthService.events.pipe(filter(e => ['logout'].includes(e.type))).subscribe(() => this.handleLogout()); + + this.oauthService.setupAutomaticSilentRefresh(); + } + + private extractClaimNames(data: any) { + this.userNameClaim = data.userNameClaim; + this.displayNameClaim = data.displayNameClaim; + this.rolesClaim = data.rolesClaim; + } + + private checkAuthenticated(): boolean { + const authenticated = this.oauthService.hasValidAccessToken(); + if (authenticated) { + this.authenticatedSubject.next(true); + const jwtClaims = this.jwtClaims(this.oauthService.getAccessToken()); + this.rolesSubject.next(this.extractRoles(jwtClaims)); + this.profileSubject.next(this.extractUserProfile(jwtClaims)); + } else { + this.authenticatedSubject.next(false); + this.rolesSubject.next(null); + this.profileSubject.next(null); + } + + return authenticated; + } + + private handleLogout() { + this.authenticatedSubject.next(false); + this.profileSubject.next(emptyUserProfile); + this.rolesSubject.next([]); + } + + private extractRoles(jwtClaims: object): string[] { + if (!this.rolesClaim) { + return []; + } + return jwtClaims[this.rolesClaim] || []; + } + + private extractUserProfile(jwtClaims: object): UserProfile { + if (!this.userNameClaim || !this.displayNameClaim) { + console.error('Missing userName or displayName claim names from server config'); + return emptyUserProfile; + } + return { + userName: jwtClaims[this.userNameClaim] || '', + displayName: jwtClaims[this.displayNameClaim] || '' + }; + } + + private jwtClaims(accessToken: string): object { + const tokenParts = accessToken.split('.'); + const claimsBase64 = this.padBase64(tokenParts[1]); + const claimsJson = atob(claimsBase64); + return JSON.parse(claimsJson); + } + + private padBase64(base64data) { + while (base64data.length % 4 !== 0) { + base64data += '='; + } + return base64data; + } + +} diff --git a/ui/src/app/shared/util/test-util.ts b/ui/src/app/shared/util/test-util.ts new file mode 100644 index 00000000..9b91554b --- /dev/null +++ b/ui/src/app/shared/util/test-util.ts @@ -0,0 +1,16 @@ +import { Observable, of } from 'rxjs'; +import { UserProfile } from '../services/auth.service'; + +export class MockAuthService { + + admin: Observable = of(false); + + userProfile: Observable = of({ userName: '', displayName: '' }); + + roles: Observable = of([]); + + async login(): Promise { + return Promise.resolve(true); + } + +}