From 9ea1a37bd5e76495d6f42b5af0da7355cad6fcc2 Mon Sep 17 00:00:00 2001 From: Patrick Hobusch Date: Sat, 4 Jul 2026 11:43:36 +0200 Subject: [PATCH] Add _all endpoint applying a complete configuration in one request PUT / accepts a product-specific _AllModel and applies each present sub-field through the existing services. The outcome of every sub-field is reported in the response's status map; the overall HTTP status aggregates them (200 all success, 207 partial success, uniform client error code, or 500 on any server error). License keys in responses are redacted. Commons provides the shared framework: _AllResource/_AllService abstractions, ServiceResult, _AllModelStatus, ServiceResultUtil (including group-aware sub-entity status keys such as settings/custom-html), and LicenseKeyRedactor. Each product contributes its _AllModel, _AllServiceImpl, _AllResourceImpl (with product-typed OpenAPI response schemas), composite settings services, and Spring wiring. Composite settings services are not exposed as beans where their type would make by-type injection of the narrow settings interfaces ambiguous in the REST layer. Covered by unit tests for the framework, the composite default methods and the product services, plus functional tests for the _all endpoint (single- and multi-entity payloads, empty model, authentication and authorization) and a Confluence settings-branding functional test guarding the REST DI wiring. --- .../commons/constants/BootstrAPI.java | 16 +- .../model/AbstractAuthenticationIdpModel.java | 2 +- .../commons/model/AuthenticationModel.java | 25 +++ .../commons/model/MailServerModel.java | 26 +++ .../SettingsBrandingColorSchemeModel.java | 2 +- ...gsModel.java => SettingsGeneralModel.java} | 12 +- .../commons/model/type/ServiceResult.java | 26 +++ .../commons/model/type/_AllModelAccessor.java | 9 + .../commons/model/type/_AllModelStatus.java | 40 ++++ .../AbstractSettingsGeneralResourceImpl.java | 31 +++ .../rest/AbstractSettingsResourceImpl.java | 31 --- .../rest/_AbstractAllResourceImpl.java | 85 ++++++++ ...urce.java => SettingsGeneralResource.java} | 8 +- .../commons/rest/api/_AllResource.java | 44 ++++ .../service/_AbstractAllServiceImpl.java | 89 ++++++++ .../service/api/AuthenticationService.java | 39 ++++ .../commons/service/api/LicensesService.java | 14 ++ .../service/api/MailServerService.java | 35 +++ ...rvice.java => SettingsGeneralService.java} | 4 +- .../commons/service/api/_AllService.java | 16 ++ .../commons/util/LicenseKeyRedactor.java | 64 ++++++ .../commons/util/ServiceResultUtil.java | 97 +++++++++ ...est.java => SettingsGeneralModelTest.java} | 0 ....java => SettingsGeneralResourceTest.java} | 22 +- .../rest/_AbstractAllResourceImplTest.java | 65 ++++++ .../impl/TestSettingsGeneralResourceImpl.java | 12 ++ .../rest/impl/TestSettingsResourceImpl.java | 12 -- .../service/_AbstractAllServiceImplTest.java | 200 ++++++++++++++++++ .../api/AuthenticationServiceTest.java | 88 ++++++++ .../service/api/MailServerServiceTest.java | 80 +++++++ .../commons/util/LicenseKeyRedactorTest.java | 44 ++++ .../commons/util/ServiceResultUtilTest.java | 111 ++++++++++ ...tractSettingsGeneralResourceFuncTest.java} | 12 +- .../rest/Abstract_AllResourceFuncTest.java | 121 +++++++++++ confluence/Apis/AllApi.md | 36 ++++ confluence/Apis/SettingsApi.md | 10 +- confluence/Models/AuthenticationModel.md | 10 + confluence/Models/MailServerModel.md | 10 + confluence/Models/SettingsGeneralModel.md | 13 ++ confluence/Models/SettingsModel.md | 9 +- confluence/Models/_AllModel.md | 16 ++ confluence/Models/_AllModelStatus.md | 11 + confluence/README.md | 6 + .../confluence/config/ServiceConfig.java | 35 ++- .../confluence/model/SettingsModel.java | 35 +++ .../confluence/model/_AllModel.java | 52 +++++ ....java => SettingsGeneralResourceImpl.java} | 8 +- .../confluence/rest/_AllResourceImpl.java | 64 ++++++ .../ConfluenceSettingsServiceImpl.java | 90 ++++++++ .../service/LicensesServiceImpl.java | 14 ++ .../service/SettingsServiceImpl.java | 23 +- .../confluence/service/_AllServiceImpl.java | 82 +++++++ .../api/ConfluenceSettingsService.java | 67 +++++- .../confluence/rest/_AllResourceTest.java | 88 ++++++++ .../ConfluenceSettingsServiceTest.java | 107 ++++++++++ .../service/SettingsServiceTest.java | 14 +- .../service/_AllServiceImplTest.java | 172 +++++++++++++++ .../SettingsBrandingResourceFuncTest.java | 46 ++++ .../rest/SettingsGeneralResourceFuncTest.java | 12 ++ .../rest/SettingsResourceFuncTest.java | 12 -- .../confluence/rest/_AllResourceFuncTest.java | 51 +++++ crowd/Apis/AllApi.md | 36 ++++ crowd/Apis/SettingsApi.md | 10 +- crowd/Models/MailServerModel.md | 10 + crowd/Models/MailServerPopModel.md | 16 ++ crowd/Models/SettingsGeneralModel.md | 13 ++ crowd/Models/SettingsModel.md | 7 +- crowd/Models/_AllModel.md | 18 ++ crowd/Models/_AllModelStatus.md | 11 + crowd/README.md | 6 + .../crowd/config/ServiceConfig.java | 20 ++ .../model/SettingsBrandingLoginPageModel.java | 6 +- .../{AllModel.java => SettingsModel.java} | 15 +- .../bootstrapi/crowd/model/_AllModel.java | 57 +++++ ....java => SettingsGeneralResourceImpl.java} | 8 +- .../crowd/rest/_AllResourceImpl.java | 64 ++++++ .../service/CrowdSettingsServiceImpl.java | 49 +++++ .../crowd/service/LicensesServiceImpl.java | 14 ++ .../crowd/service/SettingsServiceImpl.java | 8 +- .../crowd/service/_AllServiceImpl.java | 99 +++++++++ .../crowd/service/api/AllService.java | 10 - .../api/CrowdSettingsGeneralService.java | 6 +- .../service/api/CrowdSettingsService.java | 46 ++++ .../crowd/rest/_AllResourceTest.java | 93 ++++++++ .../service/CrowdSettingsServiceTest.java | 92 ++++++++ .../crowd/service/DirectoriesServiceTest.java | 1 - .../crowd/service/SettingsServiceTest.java | 6 +- .../crowd/service/_AllServiceImplTest.java | 194 +++++++++++++++++ .../crowd/rest/FuncTestExamples.java | 31 +++ .../rest/SettingsGeneralResourceFuncTest.java | 13 ++ .../crowd/rest/SettingsResourceFuncTest.java | 16 -- .../crowd/rest/_AllResourceFuncTest.java | 55 +++++ jira/Apis/AllApi.md | 36 ++++ jira/Apis/SettingsApi.md | 10 +- jira/Models/AuthenticationModel.md | 10 + jira/Models/MailServerModel.md | 10 + jira/Models/SettingsGeneralModel.md | 13 ++ jira/Models/SettingsModel.md | 8 +- jira/Models/_AllModel.md | 16 ++ jira/Models/_AllModelStatus.md | 11 + jira/README.md | 6 + .../bootstrapi/jira/config/ServiceConfig.java | 13 ++ .../bootstrapi/jira/model/SettingsModel.java | 31 +++ .../bootstrapi/jira/model/_AllModel.java | 52 +++++ ....java => SettingsGeneralResourceImpl.java} | 8 +- .../jira/rest/_AllResourceImpl.java | 64 ++++++ .../jira/service/LicensesServiceImpl.java | 14 ++ .../jira/service/SettingsServiceImpl.java | 10 +- .../jira/service/_AllServiceImpl.java | 82 +++++++ .../jira/service/api/JiraSettingsService.java | 51 ++++- .../jira/rest/_AllResourceTest.java | 88 ++++++++ .../jira/service/JiraSettingsServiceTest.java | 81 ++++++- .../jira/service/LicensesServiceTest.java | 35 +++ .../jira/service/SettingsServiceTest.java | 12 +- .../jira/service/_AllServiceImplTest.java | 172 +++++++++++++++ .../rest/SettingsGeneralResourceFuncTest.java | 5 + .../jira/rest/SettingsResourceFuncTest.java | 5 - .../jira/rest/_AllResourceFuncTest.java | 46 ++++ 118 files changed, 4265 insertions(+), 229 deletions(-) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java rename commons/src/main/java/com/deftdevs/bootstrapi/commons/model/{SettingsModel.java => SettingsGeneralModel.java} (75%) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java delete mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java rename commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/{SettingsResource.java => SettingsGeneralResource.java} (86%) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java rename commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/{SettingsService.java => SettingsGeneralService.java} (72%) create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/_AllService.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactor.java create mode 100644 commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java rename commons/src/test/java/com/deftdevs/bootstrapi/commons/model/{SettingsModelTest.java => SettingsGeneralModelTest.java} (100%) rename commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/{SettingsResourceTest.java => SettingsGeneralResourceTest.java} (56%) create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsGeneralResourceImpl.java delete mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsResourceImpl.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactorTest.java create mode 100644 commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java rename commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/{AbstractSettingsResourceFuncTest.java => AbstractSettingsGeneralResourceFuncTest.java} (84%) create mode 100644 commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java create mode 100644 confluence/Apis/AllApi.md create mode 100644 confluence/Models/AuthenticationModel.md create mode 100644 confluence/Models/MailServerModel.md create mode 100644 confluence/Models/SettingsGeneralModel.md create mode 100644 confluence/Models/_AllModel.md create mode 100644 confluence/Models/_AllModelStatus.md create mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java create mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java rename confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/{SettingsResourceImpl.java => SettingsGeneralResourceImpl.java} (69%) create mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java create mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java create mode 100644 confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java create mode 100644 confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java create mode 100644 confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java create mode 100644 confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java create mode 100644 confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsBrandingResourceFuncTest.java create mode 100644 confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceFuncTest.java delete mode 100644 confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceFuncTest.java create mode 100644 confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java create mode 100644 crowd/Apis/AllApi.md create mode 100644 crowd/Models/MailServerModel.md create mode 100644 crowd/Models/MailServerPopModel.md create mode 100644 crowd/Models/SettingsGeneralModel.md create mode 100644 crowd/Models/_AllModel.md create mode 100644 crowd/Models/_AllModelStatus.md rename crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/{AllModel.java => SettingsModel.java} (53%) create mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java rename crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/{SettingsResourceImpl.java => SettingsGeneralResourceImpl.java} (69%) create mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java create mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java create mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java delete mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/AllService.java create mode 100644 crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java create mode 100644 crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java create mode 100644 crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java create mode 100644 crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java create mode 100644 crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java create mode 100644 crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java delete mode 100644 crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceFuncTest.java create mode 100644 crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java create mode 100644 jira/Apis/AllApi.md create mode 100644 jira/Models/AuthenticationModel.md create mode 100644 jira/Models/MailServerModel.md create mode 100644 jira/Models/SettingsGeneralModel.md create mode 100644 jira/Models/_AllModel.md create mode 100644 jira/Models/_AllModelStatus.md create mode 100644 jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java create mode 100644 jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java rename jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/{SettingsResourceImpl.java => SettingsGeneralResourceImpl.java} (69%) create mode 100644 jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java create mode 100644 jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java create mode 100644 jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java create mode 100644 jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java create mode 100644 jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceFuncTest.java delete mode 100644 jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsResourceFuncTest.java create mode 100644 jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java index 644ae427..8792b473 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java @@ -2,7 +2,9 @@ public class BootstrAPI { - public static final String ALL = "all"; + public static final String _ALL = "_all"; + public static final String _ROOT = "/"; + public static final String APPLICATION = "application"; public static final String APPLICATIONS = "applications"; public static final String APPLICATION_LINK = "application-link"; @@ -38,29 +40,39 @@ public class BootstrAPI { public static final String MAIL_SERVERS = "mail-servers"; public static final String MAIL_SERVER_POP = "pop"; public static final String MAIL_SERVER_SMTP = "smtp"; + public static final String MAIL_TEMPLATES = "mail-templates"; public static final String PERMISSION = "permission"; public static final String PERMISSIONS = "permissions"; public static final String PERMISSION_ANONYMOUS_ACCESS = "anonymous-access"; public static final String PERMISSIONS_GLOBAL = "global"; public static final String PING = "ping"; + public static final String SESSION_CONFIG = "session-config"; public static final String SETTINGS = "settings"; public static final String SETTINGS_BANNER = "banner"; public static final String SETTINGS_BRANDING = "branding"; public static final String SETTINGS_BRANDING_LOGIN_PAGE = "login-page"; public static final String SETTINGS_BRANDING_LOGO = "logo"; public static final String SETTINGS_CUSTOM_HTML = "custom-html"; + public static final String SETTINGS_GENERAL = "general"; public static final String SETTINGS_SECURITY = "security"; public static final String USER = "user"; public static final String USERS = "users"; + public static final String TRUSTED_PROXIES = "trusted-proxies"; public static final String USER_PASSWORD = "password"; public static final String MEDIA_TYPE_IMAGE = "image/*"; public static final String ERROR_COLLECTION_RESPONSE_DESCRIPTION = "Returns a list of error messages."; + public static final String _ALL_PUT_SUMMARY = "Apply a complete configuration"; + public static final String _ALL_PUT_RESPONSE_DESCRIPTION = "Returns the updated configuration. The per-sub-field outcome is reported in the" + + " 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details')." + + " License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2')."; + public static final String _ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION = "Configuration applied partially. Inspect the per-sub-field 'status' map" + + " in the response body to see which fields succeeded and which failed."; public static final String SETTINGS_GENERAL_GET_SUMMARY = "Get the general settings"; public static final String SETTINGS_GENERAL_GET_RESPONSE_DESCRIPTION = "Returns the general settings"; public static final String SETTINGS_GENERAL_PUT_SUMMARY = "Set the general settings"; - public static final String SETTINGS_GENERAL_PUT_RESPONSE_DESCRIPTION = "Returns the general security settings"; + public static final String SETTINGS_GENERAL_PUT_RESPONSE_DESCRIPTION = "Returns the updated general settings"; public static final String SETTINGS_SECURITY_GET_SUMMARY = "Get the security settings"; public static final String SETTINGS_SECURITY_GET_RESPONSE_DESCRIPTION = "Returns the security settings"; public static final String SETTINGS_SECURITY_PUT_SUMMARY = "Set the security settings"; diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java index ba5828d8..86ee05b1 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java @@ -17,7 +17,7 @@ @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement +@XmlRootElement(name = "authentication-idps") @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java new file mode 100644 index 00000000..909488f4 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java @@ -0,0 +1,25 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.AUTHENTICATION; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = AUTHENTICATION) +public class AuthenticationModel { + + @XmlElement + private Map idps; + + @XmlElement + private AuthenticationSsoModel sso; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java new file mode 100644 index 00000000..5329276e --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java @@ -0,0 +1,26 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.MAIL_SERVER; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = MAIL_SERVER) +public class MailServerModel { + + @XmlElement + private MailServerSmtpModel smtp; + + @XmlElement + private MailServerPopModel pop; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java index 29fe6548..66740ab0 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java @@ -16,7 +16,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING + "-" + COLOR_SCHEME) +@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsBrandingColorSchemeModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java similarity index 75% rename from commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsModel.java rename to commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java index 1689a097..db8e9dbe 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java @@ -1,6 +1,5 @@ package com.deftdevs.bootstrapi.commons.model; -import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; @@ -13,13 +12,16 @@ import javax.xml.bind.annotation.XmlRootElement; import java.net.URI; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS_GENERAL; + @Data @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = BootstrAPI.SETTINGS) +@XmlRootElement(name = SETTINGS + "-" + SETTINGS_GENERAL) @XmlAccessorType(XmlAccessType.FIELD) -public class SettingsModel { +public class SettingsGeneralModel { @XmlElement private URI baseUrl; @@ -46,7 +48,7 @@ public String getMode() { // Example instances for documentation and tests - public static final SettingsModel EXAMPLE_1 = SettingsModel.builder() + public static final SettingsGeneralModel EXAMPLE_1 = SettingsGeneralModel.builder() .title("Example") .baseUrl(URI.create("https://example.com")) .mode("private") @@ -54,7 +56,7 @@ public String getMode() { .externalUserManagement(true) .build(); - public static final SettingsModel EXAMPLE_1_NO_MODE = SettingsModel.builder() + public static final SettingsGeneralModel EXAMPLE_1_NO_MODE = SettingsGeneralModel.builder() .title("Example") .baseUrl(URI.create("https://example.com")) .mode(null) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java new file mode 100644 index 00000000..53b0f1fb --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java @@ -0,0 +1,26 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import java.util.Map; + +public class ServiceResult { + + private final T model; + private final Map status; + + public ServiceResult( + final T model, + final Map status) { + + this.model = model; + this.status = status; + } + + public T getModel() { + return model; + } + + public Map getStatus() { + return status; + } + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java new file mode 100644 index 00000000..c586adff --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java @@ -0,0 +1,9 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import java.util.Map; + +public interface _AllModelAccessor { + + Map getStatus(); + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java new file mode 100644 index 00000000..42d73b65 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java @@ -0,0 +1,40 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.ws.rs.core.Response; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = "status") +public class _AllModelStatus { + + @XmlElement + private int status; + + @XmlElement + private String message; + + @XmlElement + private String details; + + public static _AllModelStatus success() { + return new _AllModelStatus(Response.Status.OK.getStatusCode(), "Success", null); + } + + public static _AllModelStatus error(Response.Status status, String message, String details) { + final int code = status != null + ? status.getStatusCode() + : Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + return new _AllModelStatus(code, message, details); + } + + public static _AllModelStatus error(int statusCode, String message, String details) { + return new _AllModelStatus(statusCode, message, details); + } +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java new file mode 100644 index 00000000..b93da638 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java @@ -0,0 +1,31 @@ +package com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.api.SettingsGeneralResource; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; + +import javax.ws.rs.core.Response; + +public abstract class AbstractSettingsGeneralResourceImpl> + implements SettingsGeneralResource { + + private final S settingsService; + + public AbstractSettingsGeneralResourceImpl( + final S settingsService) { + + this.settingsService = settingsService; + } + + @Override + public Response getSettings() { + final B settingsModel = settingsService.getSettingsGeneral(); + return Response.ok(settingsModel).build(); + } + + @Override + public Response setSettings(B settingsModel) { + final B updatedSettingsGeneralModel = settingsService.setSettingsGeneral(settingsModel); + return Response.ok(updatedSettingsGeneralModel).build(); + } +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java deleted file mode 100644 index c1c4c405..00000000 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.deftdevs.bootstrapi.commons.rest; - -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.api.SettingsResource; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; - -import javax.ws.rs.core.Response; - -public abstract class AbstractSettingsResourceImpl> - implements SettingsResource { - - private final S settingsService; - - public AbstractSettingsResourceImpl( - final S settingsService) { - - this.settingsService = settingsService; - } - - @Override - public Response getSettings() { - final B settingsModel = settingsService.getSettingsGeneral(); - return Response.ok(settingsModel).build(); - } - - @Override - public Response setSettings(B settingsModel) { - final B updatedSettingsModel = settingsService.setSettingsGeneral(settingsModel); - return Response.ok(updatedSettingsModel).build(); - } -} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java new file mode 100644 index 00000000..45a5febf --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java @@ -0,0 +1,85 @@ +package com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.rest.api._AllResource; +import com.deftdevs.bootstrapi.commons.service.api._AllService; + +import javax.ws.rs.core.Response; +import java.util.Map; + +public abstract class _AbstractAllResourceImpl<_AllModel extends _AllModelAccessor> + implements _AllResource<_AllModel> { + + static final int MULTI_STATUS = 207; + + private final _AllService<_AllModel> allService; + + public _AbstractAllResourceImpl( + final _AllService<_AllModel> allService) { + + this.allService = allService; + } + + @Override + public Response setAll( + final _AllModel allModel) { + + final _AllModel result = allService.setAll(allModel); + final int overallStatus = computeOverallStatus(result.getStatus()); + return Response.status(overallStatus).entity(result).build(); + } + + /** + * Aggregates per-sub-field statuses into a single HTTP response code: + *
    + *
  • All successful (or empty) → 200 OK.
  • + *
  • All entries share the same status code → that code.
  • + *
  • Any 5xx → 500 Internal Server Error.
  • + *
  • Mixed 2xx/4xx (or multiple distinct 4xx) → 207 Multi-Status.
  • + *
+ * 207 is used to signal "partial success" — callers must inspect the + * per-field {@code status} map in the response body to see which + * fields succeeded and which failed. + */ + static int computeOverallStatus( + final Map statusMap) { + + if (statusMap == null || statusMap.isEmpty()) { + return Response.Status.OK.getStatusCode(); + } + + boolean anyServerError = false; + boolean anyClientError = false; + boolean anySuccess = false; + Integer firstClientErrorCode = null; + boolean clientErrorCodesDiffer = false; + + for (_AllModelStatus entry : statusMap.values()) { + final int code = entry.getStatus(); + if (code >= 500) { + anyServerError = true; + } else if (code >= 400) { + anyClientError = true; + if (firstClientErrorCode == null) { + firstClientErrorCode = code; + } else if (firstClientErrorCode != code) { + clientErrorCodesDiffer = true; + } + } else if (code >= 200 && code < 300) { + anySuccess = true; + } + } + + if (anyServerError) { + return Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + } + if (anyClientError) { + if (anySuccess || clientErrorCodesDiffer) { + return MULTI_STATUS; + } + return firstClientErrorCode; + } + return Response.Status.OK.getStatusCode(); + } +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsResource.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsGeneralResource.java similarity index 86% rename from commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsResource.java rename to commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsGeneralResource.java index 8e415842..6bfcbe8a 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsResource.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsGeneralResource.java @@ -2,7 +2,7 @@ import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import com.deftdevs.bootstrapi.commons.model.ErrorCollection; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -12,14 +12,14 @@ import javax.ws.rs.PUT; import javax.ws.rs.core.Response; -public interface SettingsResource { +public interface SettingsGeneralResource { @GET @Operation( summary = BootstrAPI.SETTINGS_GENERAL_GET_SUMMARY, responses = { @ApiResponse( - responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsModel.class)), + responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsGeneralModel.class)), description = BootstrAPI.SETTINGS_GENERAL_GET_RESPONSE_DESCRIPTION ), @ApiResponse( @@ -35,7 +35,7 @@ public interface SettingsResource { summary = BootstrAPI.SETTINGS_GENERAL_PUT_SUMMARY, responses = { @ApiResponse( - responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsModel.class)), + responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsGeneralModel.class)), description = BootstrAPI.SETTINGS_GENERAL_PUT_RESPONSE_DESCRIPTION ), @ApiResponse( diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java new file mode 100644 index 00000000..62d097a0 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java @@ -0,0 +1,44 @@ +package com.deftdevs.bootstrapi.commons.rest.api; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.ErrorCollection; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import javax.validation.constraints.NotNull; +import javax.ws.rs.PUT; +import javax.ws.rs.core.Response; + +public interface _AllResource<_AllModel extends _AllModelAccessor> { + + /** + * Product implementations should override this method and re-declare the + * {@code @Operation} annotation with their concrete {@code _AllModel} as the + * response schema, so the generated OpenAPI documentation shows the product + * model instead of an empty response body. + */ + @PUT + @Operation( + summary = BootstrAPI._ALL_PUT_SUMMARY, + responses = { + @ApiResponse( + responseCode = "200", + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "207", + description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = BootstrAPI.ERROR_COLLECTION_RESPONSE_DESCRIPTION + ), + } + ) + Response setAll( + @NotNull final _AllModel bean); + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java new file mode 100644 index 00000000..1f6c938c --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java @@ -0,0 +1,89 @@ +package com.deftdevs.bootstrapi.commons.service; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; + +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +public abstract class _AbstractAllServiceImpl<_AllModel extends _AllModelAccessor> implements _AllService<_AllModel> { + + protected void setEntityWithStatus( + final I input, + final Function> updateFunction, + final Consumer resultConsumer, + final Map statusMap) { + + if (input == null) { + return; + } + + try { + final ServiceResult serviceResult = updateFunction.apply(input); + resultConsumer.accept(serviceResult.getModel()); + statusMap.putAll(serviceResult.getStatus()); + } catch (Exception e) { + // The composite updateFunction normally produces fine-grained + // sub-field status entries itself. If it threw before doing so + // (e.g. NPE on a sub-field accessor, or a cast failure on a + // generic parameter), attribute the failure to the top-level + // group derived from the input model's @XmlRootElement name. + final String fallbackKey = ServiceResultUtil.entityType(input.getClass()); + statusMap.put(fallbackKey, ServiceResultUtil.toErrorStatus(fallbackKey, e)); + } + } + + protected _AllModelStatus setEntity( + final String entityType, + final I input, + final Function updateFunction, + final Consumer resultConsumer, + final Map statusMap) { + + if (input == null) { + return null; + } + + try { + final O updatedEntity = updateFunction.apply(input); + resultConsumer.accept(updatedEntity); + final _AllModelStatus status = _AllModelStatus.success(); + statusMap.put(entityType, status); + return status; + } catch (Exception e) { + final _AllModelStatus status = ServiceResultUtil.toErrorStatus(entityType, e); + statusMap.put(entityType, status); + return status; + } + } + + @SuppressWarnings("unchecked") + protected _AllModelStatus setEntities( + final String entityType, + final Map entityMap, + final Function, Map> updateFunction, + final Consumer> resultConsumer, + final Map statusMap) { + + if (entityMap == null || entityMap.isEmpty()) { + return null; + } + + try { + final Map updatedEntities = updateFunction.apply(entityMap); + resultConsumer.accept((Map) updatedEntities); + final _AllModelStatus status = _AllModelStatus.success(); + statusMap.put(entityType, status); + return status; + } catch (Exception e) { + final _AllModelStatus status = ServiceResultUtil.toErrorStatus(entityType, e); + statusMap.put(entityType, status); + return status; + } + } + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java index 871806a1..a248a904 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java @@ -1,12 +1,51 @@ package com.deftdevs.bootstrapi.commons.service.api; import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import java.util.LinkedHashMap; import java.util.Map; public interface AuthenticationService { + @SuppressWarnings("unchecked") + default AuthenticationModel getAuthentication() { + return new AuthenticationModel( + (Map) getAuthenticationIdps(), + getAuthenticationSso()); + } + + @SuppressWarnings("unchecked") + default ServiceResult setAuthentication(final AuthenticationModel authenticationModel) { + final AuthenticationModel result = new AuthenticationModel(); + final Map status = new LinkedHashMap<>(); + + if (authenticationModel.getIdps() != null) { + final String key = ServiceResultUtil.subEntityKey(AbstractAuthenticationIdpModel.class); + try { + result.setIdps((Map) setAuthenticationIdps( + (Map) authenticationModel.getIdps())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (authenticationModel.getSso() != null) { + final String key = ServiceResultUtil.subEntityKey(AuthenticationSsoModel.class); + try { + result.setSso(setAuthenticationSso((SB) authenticationModel.getSso())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + Map getAuthenticationIdps(); Map setAuthenticationIdps( diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java index 8f07d738..6c39a758 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java @@ -3,6 +3,7 @@ import com.deftdevs.bootstrapi.commons.model.LicenseModel; import java.util.List; +import java.util.Map; public interface LicensesService { @@ -21,6 +22,19 @@ public interface LicensesService { List setLicenses( List licenseKeys); + /** + * Set licenses from a map keyed by license key. + *

+ * Input values may be {@code null} (key-only entries are valid); the map keys are the + * license keys to apply. The returned map is keyed by a redacted form of each input + * key so that full license keys are not echoed in the response. + * + * @param licenseInputs map of license key to (optional) input model; values are ignored + * @return map of redacted-key to applied {@link LicenseModel} + */ + Map setLicenses( + Map licenseInputs); + /** * Add a single license * diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java index e38f5c5c..160fcfbd 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/MailServerService.java @@ -1,11 +1,46 @@ package com.deftdevs.bootstrapi.commons.service.api; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import java.util.LinkedHashMap; +import java.util.Map; public interface MailServerService { + default MailServerModel getMailServer() { + return new MailServerModel(getMailServerSmtp(), getMailServerPop()); + } + + default ServiceResult setMailServer(final MailServerModel mailServerModel) { + final MailServerModel result = new MailServerModel(); + final Map status = new LinkedHashMap<>(); + + if (mailServerModel.getSmtp() != null) { + final String key = ServiceResultUtil.subEntityKey(MailServerSmtpModel.class); + try { + result.setSmtp(setMailServerSmtp(mailServerModel.getSmtp())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (mailServerModel.getPop() != null) { + final String key = ServiceResultUtil.subEntityKey(MailServerPopModel.class); + try { + result.setPop(setMailServerPop(mailServerModel.getPop())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + /** * Get the smtp mailserver settings. * diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsGeneralService.java similarity index 72% rename from commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsService.java rename to commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsGeneralService.java index 7bbf8fa2..bb41c8e0 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/SettingsGeneralService.java @@ -1,9 +1,9 @@ package com.deftdevs.bootstrapi.commons.service.api; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; -public interface SettingsService { +public interface SettingsGeneralService { /** * Get the settings. diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/_AllService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/_AllService.java new file mode 100644 index 00000000..9afbbbf9 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/_AllService.java @@ -0,0 +1,16 @@ +package com.deftdevs.bootstrapi.commons.service.api; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; + +public interface _AllService<_AllModel extends _AllModelAccessor> { + + /** + * Apply a complete configuration. + * + * @param allModel the configuration to apply + * @return the updated configuration with status + */ + _AllModel setAll( + _AllModel allModel); + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactor.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactor.java new file mode 100644 index 00000000..ade5c82c --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactor.java @@ -0,0 +1,64 @@ +package com.deftdevs.bootstrapi.commons.util; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Redacts license keys for inclusion in HTTP responses. + *

+ * License keys are sensitive and should not be echoed back in full to clients, + * since responses may be captured by intermediaries (proxies, logs, etc.) that + * do not capture the original request body. Redacted keys allow a caller to + * correlate a response entry with the key they sent without leaking the full + * value. + *

+ * Format: {@code ...#}, where {@code hash4} is the first + * four hex characters of the SHA-1 digest of the full key. The hash suffix + * disambiguates keys that happen to share the same prefix and suffix (e.g. + * Atlassian license keys, which all start with the same product header). + *

+ * Keys shorter than nine characters are hashed entirely and emitted as + * {@code ***#} to avoid leaking the full key when redaction would be + * effectively a no-op. + */ +public final class LicenseKeyRedactor { + + private static final int VISIBLE_PREFIX_LENGTH = 4; + private static final int VISIBLE_SUFFIX_LENGTH = 4; + private static final int HASH_LENGTH = 4; + private static final int MIN_REDACTABLE_LENGTH = VISIBLE_PREFIX_LENGTH + VISIBLE_SUFFIX_LENGTH + 1; + + private LicenseKeyRedactor() { + } + + public static String redact(final String key) { + if (key == null) { + return null; + } + final String hash = shortSha1(key); + if (key.length() < MIN_REDACTABLE_LENGTH) { + return "***#" + hash; + } + return key.substring(0, VISIBLE_PREFIX_LENGTH) + + "..." + + key.substring(key.length() - VISIBLE_SUFFIX_LENGTH) + + "#" + + hash; + } + + private static String shortSha1(final String input) { + try { + final MessageDigest md = MessageDigest.getInstance("SHA-1"); // NOSONAR: Hash is just used for license collisions + final byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); + final StringBuilder hex = new StringBuilder(HASH_LENGTH); + for (int i = 0; i < (HASH_LENGTH + 1) / 2; i++) { + hex.append(String.format("%02x", digest[i] & 0xff)); + } + return hex.substring(0, HASH_LENGTH); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-1 not available", e); + } + } + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java new file mode 100644 index 00000000..24d1f9e0 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java @@ -0,0 +1,97 @@ +package com.deftdevs.bootstrapi.commons.util; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.xml.bind.annotation.XmlRootElement; + +public class ServiceResultUtil { + + private static final Logger log = LoggerFactory.getLogger(ServiceResultUtil.class); + + /** + * Returns the raw {@code @XmlRootElement(name = ...)} of a model class. + * Used as the top-level status-map key for non-composite entries + * (e.g. {@code mail-templates}, {@code session-config}). Dashes in the + * name are preserved — composite "group/sub" keys are produced by the + * {@link #subEntityKey(Class)} overload instead. + */ + public static String entityType(final Class entityClass) { + final XmlRootElement rootElement = entityClass.getAnnotation(XmlRootElement.class); + if (rootElement == null || "##default".equals(rootElement.name())) { + throw new IllegalArgumentException( + entityClass.getName() + " must have @XmlRootElement with explicit name"); + } + return rootElement.name(); + } + + /** + * Returns the status-map key for a sub-field of a composite group, + * derived from the leaf model's {@code @XmlRootElement(name = ...)} + * by replacing the last dash with a slash. The convention assumes + * sub-field models are named {@code -}, where the group + * itself may contain dashes but the sub-name may not + * (e.g. {@code mail-server-smtp} → {@code mail-server/smtp}). + * Single-dash names map straightforwardly + * (e.g. {@code settings-general} → {@code settings/general}, + * {@code authentication-idps} → {@code authentication/idps}, + * {@code permissions-global} → {@code permissions/global}). + * If the sub-name itself contains dashes (e.g. {@code settings-custom-html}), + * use {@link #subEntityKey(Class, Class)} instead. + * + * @throws IllegalArgumentException if the model name contains no dash + * (i.e. the model is not a sub-field model — use {@link #entityType(Class)} instead) + */ + public static String subEntityKey(final Class entityClass) { + final String name = entityType(entityClass); + final int dash = name.lastIndexOf('-'); + if (dash < 0) { + throw new IllegalArgumentException( + entityClass.getName() + " is not a sub-field model: name '" + name + + "' has no dash separator"); + } + return name.substring(0, dash) + "/" + name.substring(dash + 1); + } + + /** + * Returns the status-map key {@code /} for a sub-field model + * whose {@code @XmlRootElement} name is prefixed by the group model's + * {@code @XmlRootElement} name. Unlike {@link #subEntityKey(Class)}, this + * works for sub-names that themselves contain dashes + * (e.g. group {@code settings} + leaf {@code settings-custom-html} + * → {@code settings/custom-html}). + * + * @throws IllegalArgumentException if the leaf model's name does not + * start with the group model's name followed by a dash + */ + public static String subEntityKey(final Class groupClass, final Class entityClass) { + final String groupName = entityType(groupClass); + final String name = entityType(entityClass); + if (!name.startsWith(groupName + "-")) { + throw new IllegalArgumentException( + entityClass.getName() + " is not a sub-field model of group '" + groupName + + "': name '" + name + "' does not start with '" + groupName + "-'"); + } + return groupName + "/" + name.substring(groupName.length() + 1); + } + + public static _AllModelStatus toErrorStatus(final String entityType, final Exception e) { + final String message = String.format("Failed to apply %s configuration", entityType); + if (e instanceof WebApplicationException) { + final Response response = ((WebApplicationException) e).getResponse(); + final int statusCode = response != null + ? response.getStatus() + : Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + return _AllModelStatus.error(statusCode, message, e.getMessage()); + } + log.warn("Unexpected error applying {} configuration", entityType, e); + return _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, message, null); + } + + private ServiceResultUtil() { + } + +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/model/SettingsModelTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModelTest.java similarity index 100% rename from commons/src/test/java/com/deftdevs/bootstrapi/commons/model/SettingsModelTest.java rename to commons/src/test/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModelTest.java diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsResourceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsGeneralResourceTest.java similarity index 56% rename from commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsResourceTest.java rename to commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsGeneralResourceTest.java index 588bd515..327076d4 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsResourceTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/SettingsGeneralResourceTest.java @@ -1,8 +1,8 @@ package com.deftdevs.bootstrapi.commons.rest; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.impl.TestSettingsResourceImpl; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.impl.TestSettingsGeneralResourceImpl; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,40 +15,40 @@ import static org.mockito.Mockito.doReturn; @ExtendWith(MockitoExtension.class) -class SettingsResourceTest { +class SettingsGeneralResourceTest { @Mock - private SettingsService settingsService; + private SettingsGeneralService settingsService; - private TestSettingsResourceImpl resource; + private TestSettingsGeneralResourceImpl resource; @BeforeEach public void setup() { - resource = new TestSettingsResourceImpl(settingsService); + resource = new TestSettingsGeneralResourceImpl(settingsService); } @Test void testGetSettings() { - final SettingsModel bean = SettingsModel.EXAMPLE_1; + final SettingsGeneralModel bean = SettingsGeneralModel.EXAMPLE_1; doReturn(bean).when(settingsService).getSettingsGeneral(); final Response response = resource.getSettings(); assertEquals(200, response.getStatus()); - final SettingsModel settingsModel = (SettingsModel) response.getEntity(); + final SettingsGeneralModel settingsModel = (SettingsGeneralModel) response.getEntity(); assertEquals(settingsModel, bean); } @Test void testSetSettings() { - final SettingsModel bean = SettingsModel.EXAMPLE_1; + final SettingsGeneralModel bean = SettingsGeneralModel.EXAMPLE_1; doReturn(bean).when(settingsService).setSettingsGeneral(bean); final Response response = resource.setSettings(bean); assertEquals(200, response.getStatus()); - final SettingsModel settingsModel = (SettingsModel) response.getEntity(); + final SettingsGeneralModel settingsModel = (SettingsGeneralModel) response.getEntity(); assertEquals(settingsModel, bean); } diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java new file mode 100644 index 00000000..f7742bb9 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java @@ -0,0 +1,65 @@ +package com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class _AbstractAllResourceImplTest { + + @Test + void emptyMapYieldsOk() { + assertEquals(200, _AbstractAllResourceImpl.computeOverallStatus(Collections.emptyMap())); + } + + @Test + void nullMapYieldsOk() { + assertEquals(200, _AbstractAllResourceImpl.computeOverallStatus(null)); + } + + @Test + void allSuccessYieldsOk() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.success()); + map.put("b", _AllModelStatus.success()); + assertEquals(200, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void anyServerErrorYields500() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.success()); + map.put("b", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); + map.put("c", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "x", null)); + assertEquals(500, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void mixedSuccessAndClientErrorYields207() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.success()); + map.put("b", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); + assertEquals(_AbstractAllResourceImpl.MULTI_STATUS, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void allSameClientErrorBubblesThat() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); + map.put("b", _AllModelStatus.error(Response.Status.BAD_REQUEST, "y", null)); + assertEquals(400, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void differentClientErrorsYield207() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); + map.put("b", _AllModelStatus.error(Response.Status.CONFLICT, "y", null)); + assertEquals(_AbstractAllResourceImpl.MULTI_STATUS, _AbstractAllResourceImpl.computeOverallStatus(map)); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsGeneralResourceImpl.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsGeneralResourceImpl.java new file mode 100644 index 00000000..135137c6 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsGeneralResourceImpl.java @@ -0,0 +1,12 @@ +package com.deftdevs.bootstrapi.commons.rest.impl; + +import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceImpl; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; + +public class TestSettingsGeneralResourceImpl extends AbstractSettingsGeneralResourceImpl { + + public TestSettingsGeneralResourceImpl(SettingsGeneralService settingsService) { + super(settingsService); + } + +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsResourceImpl.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsResourceImpl.java deleted file mode 100644 index db9c2e44..00000000 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestSettingsResourceImpl.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.deftdevs.bootstrapi.commons.rest.impl; - -import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceImpl; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; - -public class TestSettingsResourceImpl extends AbstractSettingsResourceImpl { - - public TestSettingsResourceImpl(SettingsService settingsService) { - super(settingsService); - } - -} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java new file mode 100644 index 00000000..0668496e --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java @@ -0,0 +1,200 @@ +package com.deftdevs.bootstrapi.commons.service; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class _AbstractAllServiceImplTest { + + private static class TestAllModel implements _AllModelAccessor { + + private final Map status = new HashMap<>(); + + @Override + public Map getStatus() { + return status; + } + } + + private static class TestAllService extends _AbstractAllServiceImpl { + + @Override + public TestAllModel setAll(final TestAllModel allModel) { + return allModel; + } + } + + private final TestAllService service = new TestAllService(); + + // setEntity + + @Test + void setEntitySkipsNullInput() { + final Map statusMap = new HashMap<>(); + + final _AllModelStatus status = service.setEntity("entity", null, + input -> input, result -> {}, statusMap); + + assertNull(status); + assertTrue(statusMap.isEmpty()); + } + + @Test + void setEntitySuccessRecordsStatusAndResult() { + final Map statusMap = new HashMap<>(); + final AtomicReference result = new AtomicReference<>(); + + final _AllModelStatus status = service.setEntity("entity", "input", + input -> input + "-updated", result::set, statusMap); + + assertEquals(Response.Status.OK.getStatusCode(), status.getStatus()); + assertEquals("input-updated", result.get()); + assertSame(status, statusMap.get("entity")); + } + + @Test + void setEntityUsesWebApplicationExceptionStatusCode() { + final Map statusMap = new HashMap<>(); + + final _AllModelStatus status = service.setEntity("entity", "input", + input -> { + throw new WebApplicationException("conflict", Response.Status.CONFLICT); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.CONFLICT.getStatusCode(), status.getStatus()); + assertEquals("Failed to apply entity configuration", status.getMessage()); + assertSame(status, statusMap.get("entity")); + } + + @Test + void setEntityMapsUnexpectedExceptionTo500WithoutDetails() { + final Map statusMap = new HashMap<>(); + + final _AllModelStatus status = service.setEntity("entity", "input", + input -> { + throw new IllegalStateException("internal details that must not leak"); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), status.getStatus()); + assertNull(status.getDetails()); + } + + // setEntities + + @Test + void setEntitiesSkipsNullAndEmptyMap() { + final Map statusMap = new HashMap<>(); + + assertNull(service.setEntities("entities", null, + entities -> entities, result -> {}, statusMap)); + assertNull(service.setEntities("entities", Collections.emptyMap(), + entities -> entities, result -> {}, statusMap)); + assertTrue(statusMap.isEmpty()); + } + + @Test + void setEntitiesSuccessRecordsStatusAndResult() { + final Map statusMap = new HashMap<>(); + final AtomicReference> result = new AtomicReference<>(); + final Map entities = Collections.singletonMap("key", "value"); + + final _AllModelStatus status = service.setEntities("entities", entities, + input -> input, result::set, statusMap); + + assertEquals(Response.Status.OK.getStatusCode(), status.getStatus()); + assertEquals(entities, result.get()); + assertSame(status, statusMap.get("entities")); + } + + @Test + void setEntitiesRecordsErrorStatusOnException() { + final Map statusMap = new HashMap<>(); + final Map entities = Collections.singletonMap("key", "value"); + + final _AllModelStatus status = service.setEntities("entities", entities, + input -> { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), status.getStatus()); + assertSame(status, statusMap.get("entities")); + } + + // setEntityWithStatus + + @Test + void setEntityWithStatusSkipsNullInput() { + final Map statusMap = new HashMap<>(); + + service.setEntityWithStatus(null, + input -> new ServiceResult<>(input, Collections.emptyMap()), + result -> {}, statusMap); + + assertTrue(statusMap.isEmpty()); + } + + @Test + void setEntityWithStatusMergesSubFieldStatuses() { + final Map statusMap = new HashMap<>(); + final AtomicReference result = new AtomicReference<>(); + + final Map subStatus = new LinkedHashMap<>(); + subStatus.put("settings/general", _AllModelStatus.success()); + subStatus.put("settings/security", _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + + service.setEntityWithStatus("input", + input -> new ServiceResult<>(input + "-updated", subStatus), + result::set, statusMap); + + assertEquals("input-updated", result.get()); + assertEquals(2, statusMap.size()); + assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("settings/general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), statusMap.get("settings/security").getStatus()); + } + + @Test + void setEntityWithStatusFallsBackToRootElementNameOnThrow() { + final Map statusMap = new HashMap<>(); + + service.setEntityWithStatus(SettingsGeneralModel.EXAMPLE_1, + input -> { + throw new IllegalStateException("boom"); + }, + result -> {}, statusMap); + + // fallback key is the input model's @XmlRootElement name + final _AllModelStatus status = statusMap.get("settings-general"); + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), status.getStatus()); + } + + @Test + void setEntityWithStatusUsesWebApplicationExceptionStatusCodeOnThrow() { + final Map statusMap = new HashMap<>(); + + service.setEntityWithStatus(SettingsGeneralModel.EXAMPLE_1, + input -> { + throw new WebApplicationException(Response.Status.CONFLICT); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.CONFLICT.getStatusCode(), statusMap.get("settings-general").getStatus()); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java new file mode 100644 index 00000000..050052df --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java @@ -0,0 +1,88 @@ +package com.deftdevs.bootstrapi.commons.service.api; + +import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; +import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; + +class AuthenticationServiceTest { + + private AuthenticationService authenticationService; + + private AbstractAuthenticationIdpModel idpModel; + private AuthenticationSsoModel ssoModel; + private Map idpModels; + + @BeforeEach + @SuppressWarnings("unchecked") + void setup() { + authenticationService = mock(AuthenticationService.class, CALLS_REAL_METHODS); + + idpModel = mock(AbstractAuthenticationIdpModel.class); + ssoModel = new AuthenticationSsoModel(); + idpModels = Collections.singletonMap("idp", idpModel); + } + + @Test + void testGetAuthentication() { + doReturn(idpModels).when(authenticationService).getAuthenticationIdps(); + doReturn(ssoModel).when(authenticationService).getAuthenticationSso(); + + final AuthenticationModel authenticationModel = authenticationService.getAuthentication(); + + assertEquals(idpModels, authenticationModel.getIdps()); + assertEquals(ssoModel, authenticationModel.getSso()); + } + + @Test + void testSetAuthenticationAppliesIdpsAndSso() { + doReturn(idpModels).when(authenticationService).setAuthenticationIdps(idpModels); + doReturn(ssoModel).when(authenticationService).setAuthenticationSso(ssoModel); + + final ServiceResult result = authenticationService.setAuthentication( + new AuthenticationModel(idpModels, ssoModel)); + + assertEquals(idpModels, result.getModel().getIdps()); + assertEquals(ssoModel, result.getModel().getSso()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("authentication/idps").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("authentication/sso").getStatus()); + } + + @Test + void testSetAuthenticationSkipsNullSubFields() { + final ServiceResult result = + authenticationService.setAuthentication(new AuthenticationModel()); + + assertTrue(result.getStatus().isEmpty()); + } + + @Test + void testSetAuthenticationRecordsPerSubFieldFailure() { + doReturn(idpModels).when(authenticationService).setAuthenticationIdps(idpModels); + doThrow(new BadRequestException("invalid sso config")) + .when(authenticationService).setAuthenticationSso(ssoModel); + + final ServiceResult result = authenticationService.setAuthentication( + new AuthenticationModel(idpModels, ssoModel)); + + assertEquals(idpModels, result.getModel().getIdps()); + assertNull(result.getModel().getSso()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("authentication/idps").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("authentication/sso").getStatus()); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java new file mode 100644 index 00000000..63b4a32c --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java @@ -0,0 +1,80 @@ +package com.deftdevs.bootstrapi.commons.service.api; + +import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class MailServerServiceTest { + + private MailServerService mailServerService; + + @BeforeEach + void setup() { + mailServerService = mock(MailServerService.class, CALLS_REAL_METHODS); + } + + @Test + void testGetMailServer() { + doReturn(MailServerSmtpModel.EXAMPLE_1).when(mailServerService).getMailServerSmtp(); + doReturn(MailServerPopModel.EXAMPLE_1).when(mailServerService).getMailServerPop(); + + final MailServerModel mailServerModel = mailServerService.getMailServer(); + + assertEquals(MailServerSmtpModel.EXAMPLE_1, mailServerModel.getSmtp()); + assertEquals(MailServerPopModel.EXAMPLE_1, mailServerModel.getPop()); + } + + @Test + void testSetMailServerAppliesSmtpAndPop() { + doReturn(MailServerSmtpModel.EXAMPLE_1).when(mailServerService).setMailServerSmtp(MailServerSmtpModel.EXAMPLE_1); + doReturn(MailServerPopModel.EXAMPLE_1).when(mailServerService).setMailServerPop(MailServerPopModel.EXAMPLE_1); + + final ServiceResult result = mailServerService.setMailServer( + new MailServerModel(MailServerSmtpModel.EXAMPLE_1, MailServerPopModel.EXAMPLE_1)); + + assertEquals(MailServerSmtpModel.EXAMPLE_1, result.getModel().getSmtp()); + assertEquals(MailServerPopModel.EXAMPLE_1, result.getModel().getPop()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("mail-server/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("mail-server/pop").getStatus()); + } + + @Test + void testSetMailServerSkipsNullSubFields() { + final ServiceResult result = mailServerService.setMailServer(new MailServerModel()); + + assertTrue(result.getStatus().isEmpty()); + verify(mailServerService, never()).setMailServerSmtp(MailServerSmtpModel.EXAMPLE_1); + verify(mailServerService, never()).setMailServerPop(MailServerPopModel.EXAMPLE_1); + } + + @Test + void testSetMailServerRecordsPerSubFieldFailure() { + doThrow(new BadRequestException("invalid smtp config")) + .when(mailServerService).setMailServerSmtp(MailServerSmtpModel.EXAMPLE_1); + doReturn(MailServerPopModel.EXAMPLE_1).when(mailServerService).setMailServerPop(MailServerPopModel.EXAMPLE_1); + + final ServiceResult result = mailServerService.setMailServer( + new MailServerModel(MailServerSmtpModel.EXAMPLE_1, MailServerPopModel.EXAMPLE_1)); + + assertNull(result.getModel().getSmtp()); + assertEquals(MailServerPopModel.EXAMPLE_1, result.getModel().getPop()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("mail-server/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("mail-server/pop").getStatus()); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactorTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactorTest.java new file mode 100644 index 00000000..55cd1c81 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/LicenseKeyRedactorTest.java @@ -0,0 +1,44 @@ +package com.deftdevs.bootstrapi.commons.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class LicenseKeyRedactorTest { + + @Test + void redactsNullAsNull() { + assertNull(LicenseKeyRedactor.redact(null)); + } + + @Test + void redactsShortKeyAsObfuscatedWithHash() { + final String redacted = LicenseKeyRedactor.redact("abc"); + assertEquals("***#a9993e36".substring(0, "***#".length() + 4), redacted); + } + + @Test + void redactsLongKeyShowingFirstAndLastFourPlusHash() { + final String key = "AAAA1234567890ZZZZ"; + final String redacted = LicenseKeyRedactor.redact(key); + assertEquals("AAAA...ZZZZ#", redacted.substring(0, "AAAA...ZZZZ#".length())); + assertEquals(4, redacted.length() - "AAAA...ZZZZ#".length()); + } + + @Test + void differentKeysWithSamePrefixAndSuffixGetDifferentHashes() { + final String a = LicenseKeyRedactor.redact("AAAA0000ZZZZ"); + final String b = LicenseKeyRedactor.redact("AAAA1111ZZZZ"); + assertEquals(a.substring(0, "AAAA...ZZZZ#".length()), b.substring(0, "AAAA...ZZZZ#".length())); + assertNotEquals(a, b); + } + + @Test + void redactionIsDeterministic() { + final String key = "AAAA-some-very-long-license-key-payload-ZZZZ"; + assertEquals(LicenseKeyRedactor.redact(key), LicenseKeyRedactor.redact(key)); + } + +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java new file mode 100644 index 00000000..813acb24 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java @@ -0,0 +1,111 @@ +package com.deftdevs.bootstrapi.commons.util; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.xml.bind.annotation.XmlRootElement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ServiceResultUtilTest { + + @XmlRootElement(name = "leaf") + private static class LeafModel { + } + + @XmlRootElement(name = "settings-general") + private static class SubFieldSingleDash { + } + + @XmlRootElement(name = "mail-server-smtp") + private static class SubFieldMultiDash { + } + + @XmlRootElement(name = "settings") + private static class GroupModel { + } + + @XmlRootElement(name = "settings-custom-html") + private static class SubFieldWithDashedSubName { + } + + @XmlRootElement + private static class UnnamedModel { + } + + private static class UnannotatedModel { + } + + @Test + void entityTypeReturnsRawName() { + assertEquals("leaf", ServiceResultUtil.entityType(LeafModel.class)); + } + + @Test + void entityTypeRejectsMissingAnnotation() { + assertThrows(IllegalArgumentException.class, + () -> ServiceResultUtil.entityType(UnannotatedModel.class)); + } + + @Test + void entityTypeRejectsDefaultName() { + assertThrows(IllegalArgumentException.class, + () -> ServiceResultUtil.entityType(UnnamedModel.class)); + } + + @Test + void subEntityKeySplitsOnLastDash() { + assertEquals("settings/general", ServiceResultUtil.subEntityKey(SubFieldSingleDash.class)); + assertEquals("mail-server/smtp", ServiceResultUtil.subEntityKey(SubFieldMultiDash.class)); + } + + @Test + void subEntityKeyRejectsNonCompositeName() { + assertThrows(IllegalArgumentException.class, + () -> ServiceResultUtil.subEntityKey(LeafModel.class)); + } + + @Test + void groupAwareSubEntityKeyStripsGroupPrefix() { + assertEquals("settings/custom-html", + ServiceResultUtil.subEntityKey(GroupModel.class, SubFieldWithDashedSubName.class)); + assertEquals("settings/general", + ServiceResultUtil.subEntityKey(GroupModel.class, SubFieldSingleDash.class)); + } + + @Test + void groupAwareSubEntityKeyRejectsNonMatchingGroup() { + assertThrows(IllegalArgumentException.class, + () -> ServiceResultUtil.subEntityKey(GroupModel.class, SubFieldMultiDash.class)); + } + + @Test + void toErrorStatusWithStandardWaeUsesItsStatusCode() { + final WebApplicationException e = new WebApplicationException("bad input", 400); + final _AllModelStatus status = ServiceResultUtil.toErrorStatus("settings", e); + assertEquals(400, status.getStatus()); + assertEquals("bad input", status.getDetails()); + assertTrue(status.getMessage().contains("settings")); + } + + @Test + void toErrorStatusWithNonStandardCodeDoesNotNpe() { + final WebApplicationException e = new WebApplicationException("rate limited", + Response.status(429).build()); + final _AllModelStatus status = ServiceResultUtil.toErrorStatus("settings", e); + assertEquals(429, status.getStatus()); + } + + @Test + void toErrorStatusForNonWaeIsGenericAndDropsDetails() { + final _AllModelStatus status = ServiceResultUtil.toErrorStatus("settings", + new RuntimeException("internal secret")); + assertEquals(500, status.getStatus()); + assertNull(status.getDetails()); + } +} diff --git a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceFuncTest.java b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceFuncTest.java similarity index 84% rename from commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceFuncTest.java rename to commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceFuncTest.java index 91266a6f..88cb147f 100644 --- a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceFuncTest.java +++ b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceFuncTest.java @@ -1,7 +1,7 @@ package it.com.deftdevs.bootstrapi.commons.rest; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.*; -public abstract class AbstractSettingsResourceFuncTest { +public abstract class AbstractSettingsGeneralResourceFuncTest { private final ObjectMapper objectMapper = new ObjectMapper(); @@ -21,7 +21,7 @@ void testGetSettings() throws Exception { .request(); assertEquals(Response.Status.OK.getStatusCode(), settingsResponse.statusCode()); - final SettingsModel settingsModel = objectMapper.readValue(settingsResponse.body(), SettingsModel.class); + final SettingsGeneralModel settingsModel = objectMapper.readValue(settingsResponse.body(), SettingsGeneralModel.class); assertNotNull(settingsModel.getTitle()); } @@ -31,7 +31,7 @@ void testSetSettings() throws Exception { .request(HttpMethod.PUT, getExampleModel()); assertEquals(Response.Status.OK.getStatusCode(), settingsResponse.statusCode()); - final SettingsModel settingsModel = objectMapper.readValue(settingsResponse.body(), SettingsModel.class); + final SettingsGeneralModel settingsModel = objectMapper.readValue(settingsResponse.body(), SettingsGeneralModel.class); assertEquals(getExampleModel(), settingsModel); } @@ -73,7 +73,7 @@ void testSetSettingsUnauthorized() throws Exception { assertEquals(Response.Status.FORBIDDEN.getStatusCode(), settingsResponse.statusCode()); } - protected SettingsModel getExampleModel() { - return SettingsModel.EXAMPLE_1; + protected SettingsGeneralModel getExampleModel() { + return SettingsGeneralModel.EXAMPLE_1; } } diff --git a/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java new file mode 100644 index 00000000..e3d46d99 --- /dev/null +++ b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/Abstract_AllResourceFuncTest.java @@ -0,0 +1,121 @@ +package it.com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.Response; +import java.net.http.HttpResponse; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Functional tests for the {@code PUT /} (apply complete configuration) endpoint. + *

+ * The happy-path test only submits the general settings sub-field, since that + * is the only configuration that can be applied safely and deterministically + * on all product test instances. The per-sub-field status aggregation and + * partial-failure behavior are covered by unit tests. + */ +public abstract class Abstract_AllResourceFuncTest { + + /** The _all endpoint is the REST root itself. */ + private static final String ALL_PATH = ""; + + protected static final String SETTINGS_GENERAL_KEY = + ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testSetAllAppliesGeneralSettings() throws Exception { + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .request(HttpMethod.PUT, getExampleAllModel()); + assertEquals(Response.Status.OK.getStatusCode(), allResponse.statusCode()); + + final JsonNode allNode = objectMapper.readTree(allResponse.body()); + + final JsonNode generalStatusNode = allNode.path("status").path(SETTINGS_GENERAL_KEY); + assertFalse(generalStatusNode.isMissingNode(), + "expected a '" + SETTINGS_GENERAL_KEY + "' entry in the status map, got: " + allNode.path("status")); + assertEquals(Response.Status.OK.getStatusCode(), generalStatusNode.path("status").asInt()); + + final JsonNode generalNode = allNode.path("settings").path("general"); + assertEquals(getExampleSettingsGeneralModel().getTitle(), generalNode.path("title").asText()); + } + + @Test + void testSetAllEmptyModelIsOkWithEmptyStatus() throws Exception { + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .request(HttpMethod.PUT, Collections.emptyMap()); + assertEquals(Response.Status.OK.getStatusCode(), allResponse.statusCode()); + + final JsonNode allNode = objectMapper.readTree(allResponse.body()); + assertTrue(allNode.path("status").isEmpty(), + "expected an empty status map, got: " + allNode.path("status")); + } + + /** + * PUTs the given payload to the _all endpoint, expects an overall 200 and + * a {@code status} map containing exactly one success entry per given key. + * Keys are usually derived with {@code ServiceResultUtil} so tests share + * the production key-derivation logic instead of repeating literals. + */ + protected void assertSetAllApplied( + final Object allModel, + final Collection expectedSuccessStatusKeys) throws Exception { + + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .request(HttpMethod.PUT, allModel); + assertEquals(Response.Status.OK.getStatusCode(), allResponse.statusCode(), allResponse.body()); + + final JsonNode statusNode = objectMapper.readTree(allResponse.body()).path("status"); + + final Set actualKeys = new TreeSet<>(); + statusNode.fieldNames().forEachRemaining(actualKeys::add); + assertEquals(new TreeSet<>(expectedSuccessStatusKeys), actualKeys, + "status map keys do not match the submitted sub-fields"); + + for (final String key : expectedSuccessStatusKeys) { + assertEquals(Response.Status.OK.getStatusCode(), statusNode.path(key).path("status").asInt(), + "expected a success entry for '" + key + "' in the status map, got: " + statusNode); + } + } + + @Test + void testSetAllUnauthenticated() throws Exception { + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .username("wrong") + .password("password") + .request(HttpMethod.PUT, getExampleAllModel()); + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), allResponse.statusCode()); + } + + @Test + void testSetAllUnauthorized() throws Exception { + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .username("user") + .password("user") + .request(HttpMethod.PUT, getExampleAllModel()); + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), allResponse.statusCode()); + } + + /** + * The product-specific {@code _AllModel} payload to apply. Must contain + * (at least) the general settings returned by {@link #getExampleSettingsGeneralModel()}. + */ + protected abstract Object getExampleAllModel(); + + protected SettingsGeneralModel getExampleSettingsGeneralModel() { + return SettingsGeneralModel.EXAMPLE_1; + } +} diff --git a/confluence/Apis/AllApi.md b/confluence/Apis/AllApi.md new file mode 100644 index 00000000..8174af80 --- /dev/null +++ b/confluence/Apis/AllApi.md @@ -0,0 +1,36 @@ +# AllApi + +All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**setAll**](AllApi.md#setAll) | **PUT** / | Apply a complete configuration | + + + +# **setAll** +> _AllModel setAll(\_AllModel) + +Apply a complete configuration + + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **\_AllModel** | [**_AllModel**](../Models/_AllModel.md)| | | + +### Return type + +[**_AllModel**](../Models/_AllModel.md) + +### Authorization + +[basicAuth](../README.md#basicAuth) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + diff --git a/confluence/Apis/SettingsApi.md b/confluence/Apis/SettingsApi.md index ced46831..723e19c3 100644 --- a/confluence/Apis/SettingsApi.md +++ b/confluence/Apis/SettingsApi.md @@ -108,7 +108,7 @@ This endpoint does not need any parameter. # **getSettings** -> SettingsModel getSettings() +> SettingsGeneralModel getSettings() Get the general settings @@ -117,7 +117,7 @@ This endpoint does not need any parameter. ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization @@ -252,7 +252,7 @@ Set the custom HTML # **setSettings** -> SettingsModel setSettings(SettingsModel) +> SettingsGeneralModel setSettings(SettingsGeneralModel) Set the general settings @@ -260,11 +260,11 @@ Set the general settings |Name | Type | Description | Notes | |------------- | ------------- | ------------- | -------------| -| **SettingsModel** | [**SettingsModel**](../Models/SettingsModel.md)| | [optional] | +| **SettingsGeneralModel** | [**SettingsGeneralModel**](../Models/SettingsGeneralModel.md)| | [optional] | ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization diff --git a/confluence/Models/AuthenticationModel.md b/confluence/Models/AuthenticationModel.md new file mode 100644 index 00000000..b873bdc6 --- /dev/null +++ b/confluence/Models/AuthenticationModel.md @@ -0,0 +1,10 @@ +# AuthenticationModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **idps** | [**Map**](AbstractAuthenticationIdpModel.md) | | [optional] [default to null] | +| **sso** | [**AuthenticationSsoModel**](AuthenticationSsoModel.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/confluence/Models/MailServerModel.md b/confluence/Models/MailServerModel.md new file mode 100644 index 00000000..34c39bf7 --- /dev/null +++ b/confluence/Models/MailServerModel.md @@ -0,0 +1,10 @@ +# MailServerModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **smtp** | [**MailServerSmtpModel**](MailServerSmtpModel.md) | | [optional] [default to null] | +| **pop** | [**MailServerPopModel**](MailServerPopModel.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/confluence/Models/SettingsGeneralModel.md b/confluence/Models/SettingsGeneralModel.md new file mode 100644 index 00000000..8ebe677b --- /dev/null +++ b/confluence/Models/SettingsGeneralModel.md @@ -0,0 +1,13 @@ +# SettingsGeneralModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **baseUrl** | **URI** | | [optional] [default to null] | +| **mode** | **String** | | [optional] [default to null] | +| **title** | **String** | | [optional] [default to null] | +| **contactMessage** | **String** | | [optional] [default to null] | +| **externalUserManagement** | **Boolean** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/confluence/Models/SettingsModel.md b/confluence/Models/SettingsModel.md index f4eb0e7f..a9c90eac 100644 --- a/confluence/Models/SettingsModel.md +++ b/confluence/Models/SettingsModel.md @@ -3,11 +3,10 @@ | Name | Type | Description | Notes | |------------ | ------------- | ------------- | -------------| -| **baseUrl** | **URI** | | [optional] [default to null] | -| **mode** | **String** | | [optional] [default to null] | -| **title** | **String** | | [optional] [default to null] | -| **contactMessage** | **String** | | [optional] [default to null] | -| **externalUserManagement** | **Boolean** | | [optional] [default to null] | +| **general** | [**SettingsGeneralModel**](SettingsGeneralModel.md) | | [optional] [default to null] | +| **security** | [**SettingsSecurityModel**](SettingsSecurityModel.md) | | [optional] [default to null] | +| **branding** | [**SettingsBrandingColorSchemeModel**](SettingsBrandingColorSchemeModel.md) | | [optional] [default to null] | +| **customHtml** | [**SettingsCustomHtmlModel**](SettingsCustomHtmlModel.md) | | [optional] [default to null] | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/confluence/Models/_AllModel.md b/confluence/Models/_AllModel.md new file mode 100644 index 00000000..3a2e4dee --- /dev/null +++ b/confluence/Models/_AllModel.md @@ -0,0 +1,16 @@ +# _AllModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **settings** | [**SettingsModel**](SettingsModel.md) | | [optional] [default to null] | +| **directories** | [**Map**](AbstractDirectoryModel.md) | | [optional] [default to null] | +| **applicationLinks** | [**Map**](ApplicationLinkModel.md) | | [optional] [default to null] | +| **authentication** | [**AuthenticationModel**](AuthenticationModel.md) | | [optional] [default to null] | +| **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | +| **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | +| **permissionsGlobal** | [**PermissionsGlobalModel**](PermissionsGlobalModel.md) | | [optional] [default to null] | +| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/confluence/Models/_AllModelStatus.md b/confluence/Models/_AllModelStatus.md new file mode 100644 index 00000000..ed699765 --- /dev/null +++ b/confluence/Models/_AllModelStatus.md @@ -0,0 +1,11 @@ +# _AllModelStatus +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **status** | **Integer** | | [optional] [default to null] | +| **message** | **String** | | [optional] [default to null] | +| **details** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/confluence/README.md b/confluence/README.md index 0f7ab34a..734314ae 100644 --- a/confluence/README.md +++ b/confluence/README.md @@ -7,6 +7,7 @@ All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* | Class | Method | HTTP request | Description | |------------ | ------------- | ------------- | -------------| +| *AllApi* | [**setAll**](Apis/AllApi.md#setAll) | **PUT** / | Apply a complete configuration | | *ApplicationLinkApi* | [**createApplicationLink**](Apis/ApplicationLinkApi.md#createApplicationLink) | **POST** /application-link | Create an application link | *ApplicationLinkApi* | [**deleteApplicationLink**](Apis/ApplicationLinkApi.md#deleteApplicationLink) | **DELETE** /application-link/{uuid} | Delete an application link | *ApplicationLinkApi* | [**getApplicationLink**](Apis/ApplicationLinkApi.md#getApplicationLink) | **GET** /application-link/{uuid} | Get an application link | @@ -66,6 +67,7 @@ All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* - [ApplicationLinkModel](./Models/ApplicationLinkModel.md) - [AuthenticationIdpOidcModel](./Models/AuthenticationIdpOidcModel.md) - [AuthenticationIdpSamlModel](./Models/AuthenticationIdpSamlModel.md) + - [AuthenticationModel](./Models/AuthenticationModel.md) - [AuthenticationSsoModel](./Models/AuthenticationSsoModel.md) - [CacheModel](./Models/CacheModel.md) - [DirectoryCrowdAdvanced](./Models/DirectoryCrowdAdvanced.md) @@ -88,14 +90,18 @@ All URIs are relative to *https://CONFLUENCE_URL/rest/bootstrapi/1* - [ErrorCollection](./Models/ErrorCollection.md) - [GroupModel](./Models/GroupModel.md) - [LicenseModel](./Models/LicenseModel.md) + - [MailServerModel](./Models/MailServerModel.md) - [MailServerPopModel](./Models/MailServerPopModel.md) - [MailServerSmtpModel](./Models/MailServerSmtpModel.md) - [PermissionsGlobalModel](./Models/PermissionsGlobalModel.md) - [SettingsBrandingColorSchemeModel](./Models/SettingsBrandingColorSchemeModel.md) - [SettingsCustomHtmlModel](./Models/SettingsCustomHtmlModel.md) + - [SettingsGeneralModel](./Models/SettingsGeneralModel.md) - [SettingsModel](./Models/SettingsModel.md) - [SettingsSecurityModel](./Models/SettingsSecurityModel.md) - [UserModel](./Models/UserModel.md) + - [_AllModel](./Models/_AllModel.md) + - [_AllModelStatus](./Models/_AllModelStatus.md) diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java index d0f39f51..aa43ae92 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/config/ServiceConfig.java @@ -1,6 +1,7 @@ package com.deftdevs.bootstrapi.confluence.config; import com.deftdevs.bootstrapi.commons.service.api.*; +import com.deftdevs.bootstrapi.confluence.model._AllModel; import com.deftdevs.bootstrapi.confluence.service.*; import com.deftdevs.bootstrapi.confluence.service.api.CachesService; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; @@ -18,6 +19,18 @@ public class ServiceConfig { @Autowired private HelperConfig helperConfig; + @Bean + public _AllService<_AllModel> _allService() { + return new _AllServiceImpl( + confluenceSettingsService(), + directoriesService(), + applicationLinksService(), + confluenceAuthenticationService(), + licensesService(), + mailServerService(), + permissionsService()); + } + @Bean public ApplicationLinksService applicationLinksService() { return new ApplicationLinksServiceImpl( @@ -42,8 +55,19 @@ public ConfluenceAuthenticationService confluenceAuthenticationService() { @Bean public ConfluenceSettingsService confluenceSettingsService() { - return new SettingsServiceImpl( - atlassianConfig.globalSettingsManager()); + // The branding service is deliberately NOT its own bean: ConfluenceSettingsService + // extends SettingsBrandingService, so a standalone branding bean would make by-type + // injection of SettingsBrandingService ambiguous in the REST layer. + return new ConfluenceSettingsServiceImpl( + settingsService(), + new SettingsBrandingServiceImpl( + atlassianConfig.colourSchemeManager(), + atlassianConfig.siteLogoManager())); + } + + @Bean + public SettingsServiceImpl settingsService() { + return new SettingsServiceImpl(atlassianConfig.globalSettingsManager()); } @Bean @@ -70,13 +94,6 @@ public PermissionsService permissionsService() { atlassianConfig.spacePermissionManager()); } - @Bean - public SettingsBrandingService settingsBrandingService() { - return new SettingsBrandingServiceImpl( - atlassianConfig.colourSchemeManager(), - atlassianConfig.siteLogoManager()); - } - @Bean public UsersService usersService() { return new UsersServiceImpl( diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java new file mode 100644 index 00000000..9da3af29 --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java @@ -0,0 +1,35 @@ +package com.deftdevs.bootstrapi.confluence.model; + +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = SETTINGS) +public class SettingsModel { + + @XmlElement + private SettingsGeneralModel general; + + @XmlElement + private SettingsSecurityModel security; + + @XmlElement + private SettingsBrandingColorSchemeModel branding; + + @XmlElement + private SettingsCustomHtmlModel customHtml; + +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java new file mode 100644 index 00000000..a9131cce --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java @@ -0,0 +1,52 @@ +package com.deftdevs.bootstrapi.confluence.model; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = BootstrAPI._ALL) +public class _AllModel implements _AllModelAccessor { + + @XmlElement + private SettingsModel settings; + + @XmlElement + private Map directories; + + @XmlElement + private Map applicationLinks; + + @XmlElement + private AuthenticationModel authentication; + + @XmlElement + private Map licenses; + + @XmlElement + private MailServerModel mailServer; + + @XmlElement + private PermissionsGlobalModel permissionsGlobal; + + @XmlElement + private Map status; + +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceImpl.java similarity index 69% rename from confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceImpl.java rename to confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceImpl.java index 8809c6c3..948a1436 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceImpl.java @@ -2,8 +2,8 @@ import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceImpl; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceImpl; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; import io.swagger.v3.oas.annotations.tags.Tag; @@ -18,10 +18,10 @@ @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @SystemAdminOnly -public class SettingsResourceImpl extends AbstractSettingsResourceImpl { +public class SettingsGeneralResourceImpl extends AbstractSettingsGeneralResourceImpl { @Inject - public SettingsResourceImpl( + public SettingsGeneralResourceImpl( final ConfluenceSettingsService settingsService) { super(settingsService); diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java new file mode 100644 index 00000000..93dbf4a5 --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java @@ -0,0 +1,64 @@ +package com.deftdevs.bootstrapi.confluence.rest; + +import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.ErrorCollection; +import com.deftdevs.bootstrapi.commons.rest._AbstractAllResourceImpl; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.confluence.model._AllModel; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path(BootstrAPI._ROOT) +@Tag(name = BootstrAPI._ALL) +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@SystemAdminOnly +public class _AllResourceImpl extends _AbstractAllResourceImpl<_AllModel> { + + @Inject + public _AllResourceImpl( + final _AllService<_AllModel> allService) { + + super(allService); + } + + // overridden to document the concrete response model in the generated OpenAPI spec + @PUT + @Operation( + summary = BootstrAPI._ALL_PUT_SUMMARY, + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION, + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "207", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = BootstrAPI.ERROR_COLLECTION_RESPONSE_DESCRIPTION + ), + } + ) + @Override + public Response setAll( + final _AllModel allModel) { + + return super.setAll(allModel); + } + +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java new file mode 100644 index 00000000..b3e7e586 --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceImpl.java @@ -0,0 +1,90 @@ +package com.deftdevs.bootstrapi.confluence.service; + +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService; +import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; + +import java.io.InputStream; + +public class ConfluenceSettingsServiceImpl implements ConfluenceSettingsService { + + private final SettingsServiceImpl settingsService; + private final SettingsBrandingService settingsBrandingService; + + /** + * The general/security/custom-html operations are provided by {@link SettingsServiceImpl}, + * which intentionally exposes {@code getCustomHtml}/{@code setCustomHtml} as + * non-interface methods (there is no Confluence-shared interface for them). + * That forces the concrete type here. + */ + public ConfluenceSettingsServiceImpl( + final SettingsServiceImpl settingsService, + final SettingsBrandingService settingsBrandingService) { + + this.settingsService = settingsService; + this.settingsBrandingService = settingsBrandingService; + } + + @Override + public SettingsGeneralModel getSettingsGeneral() { + return settingsService.getSettingsGeneral(); + } + + @Override + public SettingsGeneralModel setSettingsGeneral(final SettingsGeneralModel settingsGeneralModel) { + return settingsService.setSettingsGeneral(settingsGeneralModel); + } + + @Override + public SettingsSecurityModel getSettingsSecurity() { + return settingsService.getSettingsSecurity(); + } + + @Override + public SettingsSecurityModel setSettingsSecurity(final SettingsSecurityModel settingsSecurityModel) { + return settingsService.setSettingsSecurity(settingsSecurityModel); + } + + @Override + public SettingsCustomHtmlModel getCustomHtml() { + return settingsService.getCustomHtml(); + } + + @Override + public SettingsCustomHtmlModel setCustomHtml(final SettingsCustomHtmlModel settingsCustomHtmlModel) { + return settingsService.setCustomHtml(settingsCustomHtmlModel); + } + + @Override + public SettingsBrandingColorSchemeModel getColourScheme() { + return settingsBrandingService.getColourScheme(); + } + + @Override + public SettingsBrandingColorSchemeModel setColourScheme(final SettingsBrandingColorSchemeModel colourSchemeModel) { + return settingsBrandingService.setColourScheme(colourSchemeModel); + } + + @Override + public InputStream getLogo() { + return settingsBrandingService.getLogo(); + } + + @Override + public void setLogo(final InputStream inputStream) { + settingsBrandingService.setLogo(inputStream); + } + + @Override + public InputStream getFavicon() { + return settingsBrandingService.getFavicon(); + } + + @Override + public void setFavicon(final InputStream inputStream) { + settingsBrandingService.setFavicon(inputStream); + } +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java index f78cefe2..df4f5d16 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/LicensesServiceImpl.java @@ -7,10 +7,13 @@ import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; import com.deftdevs.bootstrapi.commons.model.LicenseModel; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; import com.deftdevs.bootstrapi.confluence.model.util.LicenseModelUtil; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import static com.atlassian.confluence.setup.ConfluenceBootstrapConstants.DEFAULT_LICENSE_REGISTRY_KEY; @@ -43,6 +46,17 @@ public List setLicenses( return getLicenses(); } + @Override + public Map setLicenses( + final Map licenseInputs) { + + final Map result = new LinkedHashMap<>(); + for (final String key : licenseInputs.keySet()) { + result.put(LicenseKeyRedactor.redact(key), addLicense(key)); + } + return result; + } + @Override public LicenseModel addLicense( final String licenseKey) { diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java index 645bc895..cedaaaab 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceImpl.java @@ -3,14 +3,15 @@ import com.atlassian.confluence.setup.settings.CustomHtmlSettings; import com.atlassian.confluence.setup.settings.GlobalSettingsManager; import com.atlassian.confluence.setup.settings.Settings; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; +import com.deftdevs.bootstrapi.commons.service.api.SettingsSecurityService; import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; -import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; - import java.net.URI; -public class SettingsServiceImpl implements ConfluenceSettingsService { +public class SettingsServiceImpl + implements SettingsGeneralService, SettingsSecurityService { private final GlobalSettingsManager globalSettingsManager; @@ -20,11 +21,12 @@ public SettingsServiceImpl( this.globalSettingsManager = globalSettingsManager; } + @Override - public SettingsModel getSettingsGeneral() { + public SettingsGeneralModel getSettingsGeneral() { final Settings settings = globalSettingsManager.getGlobalSettings(); - return SettingsModel.builder() + return SettingsGeneralModel.builder() .baseUrl(URI.create(settings.getBaseUrl())) .title(settings.getSiteTitle()) .contactMessage(settings.getCustomContactMessage()) @@ -32,8 +34,9 @@ public SettingsModel getSettingsGeneral() { .build(); } + @Override - public SettingsModel setSettingsGeneral(SettingsModel settingsModel) { + public SettingsGeneralModel setSettingsGeneral(SettingsGeneralModel settingsModel) { final Settings settings = globalSettingsManager.getGlobalSettings(); if (settingsModel.getBaseUrl() != null) { @@ -57,7 +60,7 @@ public SettingsModel setSettingsGeneral(SettingsModel settingsModel) { return getSettingsGeneral(); } - @Override + public SettingsCustomHtmlModel getCustomHtml() { final CustomHtmlSettings customHtmlSettings = globalSettingsManager.getGlobalSettings().getCustomHtmlSettings(); @@ -68,7 +71,7 @@ public SettingsCustomHtmlModel getCustomHtml() { .build(); } - @Override + public SettingsCustomHtmlModel setCustomHtml( final SettingsCustomHtmlModel settingsCustomHtmlModel) { @@ -92,6 +95,7 @@ public SettingsCustomHtmlModel setCustomHtml( return getCustomHtml(); } + @Override public SettingsSecurityModel getSettingsSecurity() { final Settings settings = globalSettingsManager.getGlobalSettings(); @@ -102,6 +106,7 @@ public SettingsSecurityModel getSettingsSecurity() { .build(); } + @Override public SettingsSecurityModel setSettingsSecurity( final SettingsSecurityModel settingsSecurityModel) { diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java new file mode 100644 index 00000000..91d3ea1d --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java @@ -0,0 +1,82 @@ +package com.deftdevs.bootstrapi.confluence.service; + +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.confluence.model._AllModel; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; + +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATION_LINKS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.DIRECTORIES; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.LICENSES; + +public class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel> { + + private final ConfluenceSettingsService settingsService; + private final DirectoriesService directoriesService; + private final ApplicationLinksService applicationLinksService; + private final ConfluenceAuthenticationService authenticationService; + private final LicensesService licensesService; + private final MailServerService mailServerService; + private final PermissionsService permissionsService; + + public _AllServiceImpl( + final ConfluenceSettingsService settingsService, + final DirectoriesService directoriesService, + final ApplicationLinksService applicationLinksService, + final ConfluenceAuthenticationService authenticationService, + final LicensesService licensesService, + final MailServerService mailServerService, + final PermissionsService permissionsService) { + + this.settingsService = settingsService; + this.directoriesService = directoriesService; + this.applicationLinksService = applicationLinksService; + this.authenticationService = authenticationService; + this.licensesService = licensesService; + this.mailServerService = mailServerService; + this.permissionsService = permissionsService; + } + + @Override + public _AllModel setAll( + final _AllModel allModel) { + + final _AllModel result = new _AllModel(); + final Map statusMap = new HashMap<>(); + + setEntityWithStatus(allModel.getSettings(), + settingsService::setSettings, result::setSettings, statusMap); + + setEntities(DIRECTORIES, allModel.getDirectories(), + directoriesService::setDirectories, result::setDirectories, statusMap); + + setEntities(APPLICATION_LINKS, allModel.getApplicationLinks(), + applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); + + setEntityWithStatus(allModel.getAuthentication(), + authenticationService::setAuthentication, result::setAuthentication, statusMap); + + setEntities(LICENSES, allModel.getLicenses(), + licensesService::setLicenses, result::setLicenses, statusMap); + + setEntityWithStatus(allModel.getMailServer(), + mailServerService::setMailServer, result::setMailServer, statusMap); + + setEntity(ServiceResultUtil.subEntityKey(PermissionsGlobalModel.class), allModel.getPermissionsGlobal(), + permissionsService::setPermissionsGlobal, result::setPermissionsGlobal, statusMap); + + result.setStatus(statusMap); + return result; + } +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java index cb60ebc7..3a0d764f 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceSettingsService.java @@ -1,18 +1,77 @@ package com.deftdevs.bootstrapi.confluence.service.api; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; import com.deftdevs.bootstrapi.commons.service.api.SettingsSecurityService; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; + +import java.util.LinkedHashMap; +import java.util.Map; public interface ConfluenceSettingsService extends - SettingsService, - SettingsSecurityService { + SettingsGeneralService, + SettingsSecurityService, + SettingsBrandingService { SettingsCustomHtmlModel getCustomHtml(); SettingsCustomHtmlModel setCustomHtml( SettingsCustomHtmlModel settingsCustomHtmlModel); + default SettingsModel getSettings() { + return new SettingsModel(getSettingsGeneral(), getSettingsSecurity(), + getColourScheme(), getCustomHtml()); + } + + default ServiceResult setSettings(final SettingsModel settingsModel) { + final SettingsModel result = new SettingsModel(); + final Map status = new LinkedHashMap<>(); + + if (settingsModel.getGeneral() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); + try { + result.setGeneral(setSettingsGeneral(settingsModel.getGeneral())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getSecurity() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsSecurityModel.class); + try { + result.setSecurity(setSettingsSecurity(settingsModel.getSecurity())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getBranding() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsBrandingColorSchemeModel.class); + try { + result.setBranding(setColourScheme(settingsModel.getBranding())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getCustomHtml() != null) { + // sub-name "custom-html" contains a dash, so derive the key from the group model + final String key = ServiceResultUtil.subEntityKey(SettingsModel.class, SettingsCustomHtmlModel.class); + try { + result.setCustomHtml(setCustomHtml(settingsModel.getCustomHtml())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + } diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java new file mode 100644 index 00000000..021f9c48 --- /dev/null +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java @@ -0,0 +1,88 @@ +package com.deftdevs.bootstrapi.confluence.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; +import com.deftdevs.bootstrapi.confluence.model._AllModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class _AllResourceTest { + + @Mock + private _AllService<_AllModel> allService; + + private _AllResourceImpl allResource; + + private _AllModel allModel; + + @BeforeEach + public void setup() { + allResource = new _AllResourceImpl(allService); + + allModel = new _AllModel(); + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + allModel.setSettings(settings); + + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, _AllModelStatus.success()); + status.put(APPLICATION_LINKS, _AllModelStatus.success()); + allModel.setStatus(status); + } + + @Test + public void testSetAll() { + doReturn(allModel).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(200, response.getStatus()); + + final _AllModel responseModel = (_AllModel) response.getEntity(); + assertEquals(allModel, responseModel); + + verify(allService).setAll(allModel); + } + + @Test + public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(500, response.getStatus()); + } + + @Test + public void testSetAllReturns207OnMixedSuccessAndClientError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(207, response.getStatus()); + } +} diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java new file mode 100644 index 00000000..c215f08a --- /dev/null +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java @@ -0,0 +1,107 @@ +package com.deftdevs.bootstrapi.confluence.service; + +import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService; +import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ConfluenceSettingsServiceTest { + + @Mock + private SettingsServiceImpl settingsService; + + @Mock + private SettingsBrandingService settingsBrandingService; + + private ConfluenceSettingsServiceImpl confluenceSettingsService; + + @BeforeEach + void setup() { + confluenceSettingsService = new ConfluenceSettingsServiceImpl(settingsService, settingsBrandingService); + } + + @Test + void testGetSettings() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).getSettingsGeneral(); + doReturn(SettingsSecurityModel.EXAMPLE_1).when(settingsService).getSettingsSecurity(); + doReturn(SettingsBrandingColorSchemeModel.EXAMPLE_1).when(settingsBrandingService).getColourScheme(); + doReturn(SettingsCustomHtmlModel.EXAMPLE_1).when(settingsService).getCustomHtml(); + + final SettingsModel settingsModel = confluenceSettingsService.getSettings(); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, settingsModel.getGeneral()); + assertEquals(SettingsSecurityModel.EXAMPLE_1, settingsModel.getSecurity()); + assertEquals(SettingsBrandingColorSchemeModel.EXAMPLE_1, settingsModel.getBranding()); + assertEquals(SettingsCustomHtmlModel.EXAMPLE_1, settingsModel.getCustomHtml()); + } + + @Test + void testSetSettingsAppliesAllSubFields() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doReturn(SettingsSecurityModel.EXAMPLE_1).when(settingsService).setSettingsSecurity(SettingsSecurityModel.EXAMPLE_1); + doReturn(SettingsBrandingColorSchemeModel.EXAMPLE_1).when(settingsBrandingService).setColourScheme(SettingsBrandingColorSchemeModel.EXAMPLE_1); + doReturn(SettingsCustomHtmlModel.EXAMPLE_1).when(settingsService).setCustomHtml(SettingsCustomHtmlModel.EXAMPLE_1); + + final ServiceResult result = confluenceSettingsService.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + SettingsSecurityModel.EXAMPLE_1, + SettingsBrandingColorSchemeModel.EXAMPLE_1, + SettingsCustomHtmlModel.EXAMPLE_1)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertEquals(SettingsSecurityModel.EXAMPLE_1, result.getModel().getSecurity()); + assertEquals(SettingsBrandingColorSchemeModel.EXAMPLE_1, result.getModel().getBranding()); + assertEquals(SettingsCustomHtmlModel.EXAMPLE_1, result.getModel().getCustomHtml()); + + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/security").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/custom-html").getStatus()); + } + + @Test + void testSetSettingsSkipsNullSubFields() { + final ServiceResult result = confluenceSettingsService.setSettings(new SettingsModel()); + + assertTrue(result.getStatus().isEmpty()); + verify(settingsService, never()).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + } + + @Test + void testSetSettingsRecordsPerSubFieldFailure() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doThrow(new BadRequestException("invalid colour scheme")) + .when(settingsBrandingService).setColourScheme(SettingsBrandingColorSchemeModel.EXAMPLE_1); + + final ServiceResult result = confluenceSettingsService.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + null, + SettingsBrandingColorSchemeModel.EXAMPLE_1, + null)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertNull(result.getModel().getBranding()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + } +} diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java index 06f07919..c0315750 100644 --- a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/SettingsServiceTest.java @@ -5,7 +5,7 @@ import com.atlassian.confluence.setup.settings.CustomHtmlSettings; import com.atlassian.confluence.setup.settings.GlobalSettingsManager; import com.atlassian.confluence.setup.settings.Settings; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; import org.junit.jupiter.api.BeforeEach; @@ -39,9 +39,9 @@ void testGetSettingsGeneral() { doReturn(settings).when(globalSettingsManager).getGlobalSettings(); - final SettingsModel settingsModel = settingsService.getSettingsGeneral(); + final SettingsGeneralModel settingsModel = settingsService.getSettingsGeneral(); - final SettingsModel settingsModelRef = SettingsModel.builder() + final SettingsGeneralModel settingsModelRef = SettingsGeneralModel.builder() .baseUrl(URI.create(settings.getBaseUrl())) .title(settings.getSiteTitle()) .contactMessage(settings.getCustomContactMessage()) @@ -58,20 +58,20 @@ void testSetSettingsGeneral() { final Settings updateSettings = new OtherTestSettings(); - final SettingsModel requestModel = SettingsModel.builder() + final SettingsGeneralModel requestModel = SettingsGeneralModel.builder() .baseUrl(URI.create(updateSettings.getBaseUrl())) .title(updateSettings.getSiteTitle()) .contactMessage(updateSettings.getCustomContactMessage()) .externalUserManagement(updateSettings.isExternalUserManagement()) .build(); - final SettingsModel responseModel = settingsService.setSettingsGeneral(requestModel); + final SettingsGeneralModel responseModel = settingsService.setSettingsGeneral(requestModel); final ArgumentCaptor settingsCaptor = ArgumentCaptor.forClass(Settings.class); verify(globalSettingsManager).updateGlobalSettings(settingsCaptor.capture()); final Settings settings = settingsCaptor.getValue(); - final SettingsModel settingsModel = SettingsModel.builder() + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder() .baseUrl(URI.create(settings.getBaseUrl())) .title(settings.getSiteTitle()) .contactMessage(settings.getCustomContactMessage()) @@ -84,7 +84,7 @@ void testSetSettingsGeneral() { @Test void testSetSettingsDefaultConfig(){ - final SettingsModel settingsModel = SettingsModel.builder().build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().build(); final Settings defaultSettings = new DefaultTestSettings(); doReturn(defaultSettings).when(globalSettingsManager).getGlobalSettings(); diff --git a/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java new file mode 100644 index 00000000..d99768ea --- /dev/null +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java @@ -0,0 +1,172 @@ +package com.deftdevs.bootstrapi.confluence.service; + +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; +import com.deftdevs.bootstrapi.confluence.model._AllModel; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; +import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceSettingsService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class _AllServiceImplTest { + + @Mock + private ConfluenceSettingsService settingsService; + + @Mock + private DirectoriesService directoriesService; + + @Mock + private ApplicationLinksService applicationLinksService; + + @Mock + private ConfluenceAuthenticationService authenticationService; + + @Mock + private LicensesService licensesService; + + @Mock + private MailServerService mailServerService; + + @Mock + private PermissionsService permissionsService; + + private _AllServiceImpl allService; + + @BeforeEach + void setup() { + allService = new _AllServiceImpl( + settingsService, + directoriesService, + applicationLinksService, + authenticationService, + licensesService, + mailServerService, + permissionsService); + } + + @Test + void testSetAllEmptyModelYieldsEmptyStatus() { + final _AllModel result = allService.setAll(new _AllModel()); + + assertTrue(result.getStatus().isEmpty()); + verifyNoInteractions(settingsService, directoriesService, applicationLinksService, + authenticationService, licensesService, mailServerService, permissionsService); + } + + @Test + void testSetAllAppliesAllFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map directories = + Collections.singletonMap("directory", mock(AbstractDirectoryModel.class)); + final Map applicationLinks = + Collections.singletonMap("link", ApplicationLinkModel.EXAMPLE_1); + final AuthenticationModel authentication = new AuthenticationModel(); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + final Map redactedLicenses = + Collections.singletonMap("lice...nse1#abcd", LicenseModel.EXAMPLE_1); + final MailServerModel mailServer = new MailServerModel(MailServerSmtpModel.EXAMPLE_1, null); + final PermissionsGlobalModel permissionsGlobal = new PermissionsGlobalModel(); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doReturn(directories).when(directoriesService).setDirectories(directories); + doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); + doReturn(new ServiceResult<>(authentication, + Collections.singletonMap("authentication/sso", _AllModelStatus.success()))) + .when(authenticationService).setAuthentication(authentication); + doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); + doReturn(new ServiceResult<>(mailServer, + Collections.singletonMap("mail-server/smtp", _AllModelStatus.success()))) + .when(mailServerService).setMailServer(mailServer); + doReturn(permissionsGlobal).when(permissionsService).setPermissionsGlobal(permissionsGlobal); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setDirectories(directories); + allModel.setApplicationLinks(applicationLinks); + allModel.setAuthentication(authentication); + allModel.setLicenses(licenses); + allModel.setMailServer(mailServer); + allModel.setPermissionsGlobal(permissionsGlobal); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertEquals(directories, result.getDirectories()); + assertEquals(applicationLinks, result.getApplicationLinks()); + assertEquals(authentication, result.getAuthentication()); + assertEquals(redactedLicenses, result.getLicenses()); + assertEquals(mailServer, result.getMailServer()); + assertEquals(permissionsGlobal, result.getPermissionsGlobal()); + + final Map status = result.getStatus(); + assertEquals(7, status.size()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("application-links").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("authentication/sso").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-server/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("permissions/global").getStatus()); + } + + @Test + void testSetAllRecordsFailureAndContinuesWithOtherFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doThrow(new WebApplicationException(Response.Status.CONFLICT)) + .when(licensesService).setLicenses(licenses); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setLicenses(licenses); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertNull(result.getLicenses()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get("licenses").getStatus()); + } +} diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsBrandingResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsBrandingResourceFuncTest.java new file mode 100644 index 00000000..c401daf4 --- /dev/null +++ b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsBrandingResourceFuncTest.java @@ -0,0 +1,46 @@ +package it.com.deftdevs.bootstrapi.confluence.rest; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.SettingsBrandingColorSchemeModel; +import com.fasterxml.jackson.databind.ObjectMapper; +import it.com.deftdevs.bootstrapi.commons.rest.HttpRequestHelper; +import org.junit.jupiter.api.Test; + +import javax.ws.rs.core.Response; +import java.net.http.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Guards the branding resource's DI wiring: {@code SettingsBrandingService} must resolve + * to exactly one bean even though the composite {@code ConfluenceSettingsService} also + * implements it (see the note in {@code ServiceConfig#confluenceSettingsService()}). + */ +public class SettingsBrandingResourceFuncTest { + + private static final String COLOR_SCHEME_PATH = + BootstrAPI.SETTINGS + "/" + BootstrAPI.SETTINGS_BRANDING + "/" + BootstrAPI.COLOR_SCHEME; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testGetColourScheme() throws Exception { + final HttpResponse colourSchemeResponse = HttpRequestHelper.builder(COLOR_SCHEME_PATH) + .request(); + assertEquals(Response.Status.OK.getStatusCode(), colourSchemeResponse.statusCode()); + + final SettingsBrandingColorSchemeModel colorSchemeModel = + objectMapper.readValue(colourSchemeResponse.body(), SettingsBrandingColorSchemeModel.class); + assertNotNull(colorSchemeModel); + } + + @Test + void testGetColourSchemeUnauthenticated() throws Exception { + final HttpResponse colourSchemeResponse = HttpRequestHelper.builder(COLOR_SCHEME_PATH) + .username("wrong") + .password("password") + .request(); + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), colourSchemeResponse.statusCode()); + } +} diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceFuncTest.java new file mode 100644 index 00000000..0ce82df3 --- /dev/null +++ b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsGeneralResourceFuncTest.java @@ -0,0 +1,12 @@ +package it.com.deftdevs.bootstrapi.confluence.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceFuncTest; + +public class SettingsGeneralResourceFuncTest extends AbstractSettingsGeneralResourceFuncTest { + + @Override + protected SettingsGeneralModel getExampleModel() { + return SettingsGeneralModel.EXAMPLE_1_NO_MODE; + } +} diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceFuncTest.java deleted file mode 100644 index fbfb0ebb..00000000 --- a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/SettingsResourceFuncTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package it.com.deftdevs.bootstrapi.confluence.rest; - -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceFuncTest; - -public class SettingsResourceFuncTest extends AbstractSettingsResourceFuncTest { - - @Override - protected SettingsModel getExampleModel() { - return SettingsModel.EXAMPLE_1_NO_MODE; - } -} diff --git a/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java new file mode 100644 index 00000000..e689b0bf --- /dev/null +++ b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java @@ -0,0 +1,51 @@ +package it.com.deftdevs.bootstrapi.confluence.rest; + +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.confluence.model.SettingsCustomHtmlModel; +import com.deftdevs.bootstrapi.confluence.model.SettingsModel; +import com.deftdevs.bootstrapi.confluence.model._AllModel; +import it.com.deftdevs.bootstrapi.commons.rest.Abstract_AllResourceFuncTest; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class _AllResourceFuncTest extends Abstract_AllResourceFuncTest { + + @Override + protected Object getExampleAllModel() { + return _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .build()) + .build(); + } + + @Override + protected SettingsGeneralModel getExampleSettingsGeneralModel() { + return SettingsGeneralModel.EXAMPLE_1_NO_MODE; + } + + @Test + void testSetAllAppliesMultipleEntities() throws Exception { + final _AllModel allModel = _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .customHtml(SettingsCustomHtmlModel.EXAMPLE_1) + .build()) + .mailServer(MailServerModel.builder() + .smtp(MailServerSmtpModel.EXAMPLE_2) + .pop(MailServerPopModel.EXAMPLE_2) + .build()) + .build(); + + assertSetAllApplied(allModel, Arrays.asList( + ServiceResultUtil.subEntityKey(SettingsGeneralModel.class), + ServiceResultUtil.subEntityKey(SettingsModel.class, SettingsCustomHtmlModel.class), + ServiceResultUtil.subEntityKey(MailServerSmtpModel.class), + ServiceResultUtil.subEntityKey(MailServerPopModel.class))); + } +} diff --git a/crowd/Apis/AllApi.md b/crowd/Apis/AllApi.md new file mode 100644 index 00000000..c56ea6de --- /dev/null +++ b/crowd/Apis/AllApi.md @@ -0,0 +1,36 @@ +# AllApi + +All URIs are relative to *https://CROWD_URL/rest/bootstrapi/1* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**setAll**](AllApi.md#setAll) | **PUT** / | Apply a complete configuration | + + + +# **setAll** +> _AllModel setAll(\_AllModel) + +Apply a complete configuration + + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **\_AllModel** | [**_AllModel**](../Models/_AllModel.md)| | | + +### Return type + +[**_AllModel**](../Models/_AllModel.md) + +### Authorization + +[basicAuth](../README.md#basicAuth) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + diff --git a/crowd/Apis/SettingsApi.md b/crowd/Apis/SettingsApi.md index 7b783d13..a9b82cdd 100644 --- a/crowd/Apis/SettingsApi.md +++ b/crowd/Apis/SettingsApi.md @@ -35,7 +35,7 @@ This endpoint does not need any parameter. # **getSettings** -> SettingsModel getSettings() +> SettingsGeneralModel getSettings() Get the general settings @@ -44,7 +44,7 @@ This endpoint does not need any parameter. ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization @@ -107,7 +107,7 @@ Set the logo # **setSettings** -> SettingsModel setSettings(SettingsModel) +> SettingsGeneralModel setSettings(SettingsGeneralModel) Set the general settings @@ -115,11 +115,11 @@ Set the general settings |Name | Type | Description | Notes | |------------- | ------------- | ------------- | -------------| -| **SettingsModel** | [**SettingsModel**](../Models/SettingsModel.md)| | [optional] | +| **SettingsGeneralModel** | [**SettingsGeneralModel**](../Models/SettingsGeneralModel.md)| | [optional] | ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization diff --git a/crowd/Models/MailServerModel.md b/crowd/Models/MailServerModel.md new file mode 100644 index 00000000..34c39bf7 --- /dev/null +++ b/crowd/Models/MailServerModel.md @@ -0,0 +1,10 @@ +# MailServerModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **smtp** | [**MailServerSmtpModel**](MailServerSmtpModel.md) | | [optional] [default to null] | +| **pop** | [**MailServerPopModel**](MailServerPopModel.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/crowd/Models/MailServerPopModel.md b/crowd/Models/MailServerPopModel.md new file mode 100644 index 00000000..72025fb3 --- /dev/null +++ b/crowd/Models/MailServerPopModel.md @@ -0,0 +1,16 @@ +# MailServerPopModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **name** | **String** | | [optional] [default to null] | +| **description** | **String** | | [optional] [default to null] | +| **host** | **String** | | [optional] [default to null] | +| **port** | **Integer** | | [optional] [default to null] | +| **protocol** | **String** | | [optional] [default to null] | +| **timeout** | **Long** | | [optional] [default to null] | +| **username** | **String** | | [optional] [default to null] | +| **password** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/crowd/Models/SettingsGeneralModel.md b/crowd/Models/SettingsGeneralModel.md new file mode 100644 index 00000000..8ebe677b --- /dev/null +++ b/crowd/Models/SettingsGeneralModel.md @@ -0,0 +1,13 @@ +# SettingsGeneralModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **baseUrl** | **URI** | | [optional] [default to null] | +| **mode** | **String** | | [optional] [default to null] | +| **title** | **String** | | [optional] [default to null] | +| **contactMessage** | **String** | | [optional] [default to null] | +| **externalUserManagement** | **Boolean** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/crowd/Models/SettingsModel.md b/crowd/Models/SettingsModel.md index f4eb0e7f..6a6a5063 100644 --- a/crowd/Models/SettingsModel.md +++ b/crowd/Models/SettingsModel.md @@ -3,11 +3,8 @@ | Name | Type | Description | Notes | |------------ | ------------- | ------------- | -------------| -| **baseUrl** | **URI** | | [optional] [default to null] | -| **mode** | **String** | | [optional] [default to null] | -| **title** | **String** | | [optional] [default to null] | -| **contactMessage** | **String** | | [optional] [default to null] | -| **externalUserManagement** | **Boolean** | | [optional] [default to null] | +| **general** | [**SettingsGeneralModel**](SettingsGeneralModel.md) | | [optional] [default to null] | +| **branding** | [**SettingsBrandingLoginPageModel**](SettingsBrandingLoginPageModel.md) | | [optional] [default to null] | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/crowd/Models/_AllModel.md b/crowd/Models/_AllModel.md new file mode 100644 index 00000000..5f85397e --- /dev/null +++ b/crowd/Models/_AllModel.md @@ -0,0 +1,18 @@ +# _AllModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **settings** | [**SettingsModel**](SettingsModel.md) | | [optional] [default to null] | +| **directories** | [**Map**](AbstractDirectoryModel.md) | | [optional] [default to null] | +| **applications** | [**Map**](ApplicationModel.md) | | [optional] [default to null] | +| **applicationLinks** | [**Map**](ApplicationLinkModel.md) | | [optional] [default to null] | +| **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | +| **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | +| **mailTemplates** | [**MailTemplatesModel**](MailTemplatesModel.md) | | [optional] [default to null] | +| **sessionConfig** | [**SessionConfigModel**](SessionConfigModel.md) | | [optional] [default to null] | +| **trustedProxies** | **List** | | [optional] [default to null] | +| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/crowd/Models/_AllModelStatus.md b/crowd/Models/_AllModelStatus.md new file mode 100644 index 00000000..ed699765 --- /dev/null +++ b/crowd/Models/_AllModelStatus.md @@ -0,0 +1,11 @@ +# _AllModelStatus +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **status** | **Integer** | | [optional] [default to null] | +| **message** | **String** | | [optional] [default to null] | +| **details** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/crowd/README.md b/crowd/README.md index 7993ee1b..3ae0bcf4 100644 --- a/crowd/README.md +++ b/crowd/README.md @@ -7,6 +7,7 @@ All URIs are relative to *https://CROWD_URL/rest/bootstrapi/1* | Class | Method | HTTP request | Description | |------------ | ------------- | ------------- | -------------| +| *AllApi* | [**setAll**](Apis/AllApi.md#setAll) | **PUT** / | Apply a complete configuration | | *ApplicationApi* | [**createApplication**](Apis/ApplicationApi.md#createApplication) | **POST** /application | Create an application | *ApplicationApi* | [**deleteApplication**](Apis/ApplicationApi.md#deleteApplication) | **DELETE** /application/{id} | Delete an application | *ApplicationApi* | [**getApplication**](Apis/ApplicationApi.md#getApplication) | **GET** /application/{id} | Get an application | @@ -84,12 +85,17 @@ All URIs are relative to *https://CROWD_URL/rest/bootstrapi/1* - [ErrorCollection](./Models/ErrorCollection.md) - [GroupModel](./Models/GroupModel.md) - [LicenseModel](./Models/LicenseModel.md) + - [MailServerModel](./Models/MailServerModel.md) + - [MailServerPopModel](./Models/MailServerPopModel.md) - [MailServerSmtpModel](./Models/MailServerSmtpModel.md) - [MailTemplatesModel](./Models/MailTemplatesModel.md) - [SessionConfigModel](./Models/SessionConfigModel.md) - [SettingsBrandingLoginPageModel](./Models/SettingsBrandingLoginPageModel.md) + - [SettingsGeneralModel](./Models/SettingsGeneralModel.md) - [SettingsModel](./Models/SettingsModel.md) - [UserModel](./Models/UserModel.md) + - [_AllModel](./Models/_AllModel.md) + - [_AllModelStatus](./Models/_AllModelStatus.md) diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java index 8528fb6b..296440e0 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/config/ServiceConfig.java @@ -1,6 +1,7 @@ package com.deftdevs.bootstrapi.crowd.config; import com.deftdevs.bootstrapi.commons.service.api.*; +import com.deftdevs.bootstrapi.crowd.model._AllModel; import com.deftdevs.bootstrapi.crowd.service.*; import com.deftdevs.bootstrapi.crowd.service.api.*; import org.springframework.beans.factory.annotation.Autowired; @@ -16,6 +17,25 @@ public class ServiceConfig { @Autowired private HelperConfig helperConfig; + @Bean + public _AllService<_AllModel> _allService() { + // The composite settings service is deliberately NOT a bean: its type extends + // CrowdSettingsGeneralService and CrowdSettingsBrandingService, so exposing it + // would make by-type injection of those interfaces ambiguous in the REST layer. + return new _AllServiceImpl( + new CrowdSettingsServiceImpl( + crowdSettingsGeneralService(), + settingsBrandingService()), + directoriesService(), + applicationsService(), + applicationLinksService(), + licensesService(), + mailServerService(), + mailTemplatesService(), + sessionConfigService(), + trustedProxiesService()); + } + @Bean public ApplicationLinksService applicationLinksService() { return new ApplicationLinksServiceImpl( diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java index 3107978b..d1a327a2 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsBrandingLoginPageModel.java @@ -1,6 +1,5 @@ package com.deftdevs.bootstrapi.crowd.model; -import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; @@ -9,11 +8,14 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS_BRANDING; + @Data @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = BootstrAPI.SETTINGS_BRANDING_LOGIN_PAGE) +@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING) public class SettingsBrandingLoginPageModel { @XmlElement diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/AllModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java similarity index 53% rename from crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/AllModel.java rename to crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java index 94eb5694..89173705 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/AllModel.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java @@ -1,26 +1,27 @@ package com.deftdevs.bootstrapi.crowd.model; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import lombok.AllArgsConstructor; -import lombok.Data; import lombok.Builder; +import lombok.Data; import lombok.NoArgsConstructor; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; -import java.util.List; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; @Data @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = "all") -public class AllModel { +@XmlRootElement(name = SETTINGS) +public class SettingsModel { @XmlElement - private SettingsModel settings; + private SettingsGeneralModel general; @XmlElement - private List applications; + private SettingsBrandingLoginPageModel branding; } diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java new file mode 100644 index 00000000..ab258631 --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java @@ -0,0 +1,57 @@ +package com.deftdevs.bootstrapi.crowd.model; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = BootstrAPI._ALL) +public class _AllModel implements _AllModelAccessor { + + @XmlElement + private SettingsModel settings; + + @XmlElement + private Map directories; + + @XmlElement + private Map applications; + + @XmlElement + private Map applicationLinks; + + @XmlElement + private Map licenses; + + @XmlElement + private MailServerModel mailServer; + + @XmlElement + private MailTemplatesModel mailTemplates; + + @XmlElement + private SessionConfigModel sessionConfig; + + @XmlElement + private List trustedProxies; + + @XmlElement + private Map status; + +} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceImpl.java similarity index 69% rename from crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceImpl.java rename to crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceImpl.java index 22299041..fd13dea8 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceImpl.java @@ -2,8 +2,8 @@ import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceImpl; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceImpl; import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; import io.swagger.v3.oas.annotations.tags.Tag; @@ -19,10 +19,10 @@ @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @SystemAdminOnly -public class SettingsResourceImpl extends AbstractSettingsResourceImpl { +public class SettingsGeneralResourceImpl extends AbstractSettingsGeneralResourceImpl { @Inject - public SettingsResourceImpl( + public SettingsGeneralResourceImpl( final CrowdSettingsGeneralService settingsService) { super(settingsService); diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java new file mode 100644 index 00000000..b44f3620 --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java @@ -0,0 +1,64 @@ +package com.deftdevs.bootstrapi.crowd.rest; + +import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.ErrorCollection; +import com.deftdevs.bootstrapi.commons.rest._AbstractAllResourceImpl; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.crowd.model._AllModel; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path(BootstrAPI._ROOT) +@Tag(name = BootstrAPI._ALL) +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@SystemAdminOnly +public class _AllResourceImpl extends _AbstractAllResourceImpl<_AllModel> { + + @Inject + public _AllResourceImpl( + final _AllService<_AllModel> allService) { + + super(allService); + } + + // overridden to document the concrete response model in the generated OpenAPI spec + @PUT + @Operation( + summary = BootstrAPI._ALL_PUT_SUMMARY, + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION, + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "207", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = BootstrAPI.ERROR_COLLECTION_RESPONSE_DESCRIPTION + ), + } + ) + @Override + public Response setAll( + final _AllModel allModel) { + + return super.setAll(allModel); + } + +} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java new file mode 100644 index 00000000..477e9f9b --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceImpl.java @@ -0,0 +1,49 @@ +package com.deftdevs.bootstrapi.crowd.service; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsService; + +import java.io.InputStream; + +public class CrowdSettingsServiceImpl implements CrowdSettingsService { + + private final CrowdSettingsGeneralService settingsService; + private final CrowdSettingsBrandingService settingsBrandingService; + + public CrowdSettingsServiceImpl( + final CrowdSettingsGeneralService settingsService, + final CrowdSettingsBrandingService settingsBrandingService) { + + this.settingsService = settingsService; + this.settingsBrandingService = settingsBrandingService; + } + + @Override + public SettingsGeneralModel getSettingsGeneral() { + return settingsService.getSettingsGeneral(); + } + + @Override + public SettingsGeneralModel setSettingsGeneral(final SettingsGeneralModel settingsGeneralModel) { + return settingsService.setSettingsGeneral(settingsGeneralModel); + } + + @Override + public SettingsBrandingLoginPageModel getLoginPage() { + return settingsBrandingService.getLoginPage(); + } + + @Override + public SettingsBrandingLoginPageModel setLoginPage(final SettingsBrandingLoginPageModel settingsBrandingLoginPageModel) { + return settingsBrandingService.setLoginPage(settingsBrandingLoginPageModel); + } + + @Override + public void setLogo(final InputStream inputStream) { + settingsBrandingService.setLogo(inputStream); + } + +} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java index 31a0d9cc..3998434e 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/LicensesServiceImpl.java @@ -6,9 +6,12 @@ import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; import com.deftdevs.bootstrapi.commons.model.LicenseModel; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public class LicensesServiceImpl implements LicensesService { @@ -40,6 +43,17 @@ public List setLicenses( ); } + @Override + public Map setLicenses( + final Map licenseInputs) { + + final Map result = new LinkedHashMap<>(); + for (final String key : licenseInputs.keySet()) { + result.put(LicenseKeyRedactor.redact(key), addLicense(key)); + } + return result; + } + public LicenseModel addLicense( final String licenseKey) { diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java index ec4f6d64..a8cee89d 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java @@ -3,7 +3,7 @@ import com.atlassian.crowd.manager.property.PropertyManager; import com.atlassian.crowd.manager.property.PropertyManagerException; import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; public class SettingsServiceImpl @@ -18,9 +18,9 @@ public SettingsServiceImpl( } @Override - public SettingsModel getSettingsGeneral() { + public SettingsGeneralModel getSettingsGeneral() { try { - return SettingsModel.builder() + return SettingsGeneralModel.builder() .baseUrl(propertyManager.getBaseUrl()) .title(propertyManager.getDeploymentTitle()) .build(); @@ -30,7 +30,7 @@ public SettingsModel getSettingsGeneral() { } @Override - public SettingsModel setSettingsGeneral(SettingsModel settingsModel) { + public SettingsGeneralModel setSettingsGeneral(SettingsGeneralModel settingsModel) { if (settingsModel.getBaseUrl() != null) { propertyManager.setBaseUrl(settingsModel.getBaseUrl()); } diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java new file mode 100644 index 00000000..f2aec961 --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java @@ -0,0 +1,99 @@ +package com.deftdevs.bootstrapi.crowd.service; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.crowd.model.MailTemplatesModel; +import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; +import com.deftdevs.bootstrapi.crowd.model._AllModel; +import com.deftdevs.bootstrapi.crowd.service.api.ApplicationsService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsService; +import com.deftdevs.bootstrapi.crowd.service.api.MailTemplatesService; +import com.deftdevs.bootstrapi.crowd.service.api.SessionConfigService; +import com.deftdevs.bootstrapi.crowd.service.api.TrustedProxiesService; + +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATIONS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATION_LINKS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.DIRECTORIES; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.LICENSES; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.TRUSTED_PROXIES; + +public class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel> { + + private final CrowdSettingsService settingsService; + private final DirectoriesService directoriesService; + private final ApplicationsService applicationsService; + private final ApplicationLinksService applicationLinksService; + private final LicensesService licensesService; + private final MailServerService mailServerService; + private final MailTemplatesService mailTemplatesService; + private final SessionConfigService sessionConfigService; + private final TrustedProxiesService trustedProxiesService; + + public _AllServiceImpl( + final CrowdSettingsService settingsService, + final DirectoriesService directoriesService, + final ApplicationsService applicationsService, + final ApplicationLinksService applicationLinksService, + final LicensesService licensesService, + final MailServerService mailServerService, + final MailTemplatesService mailTemplatesService, + final SessionConfigService sessionConfigService, + final TrustedProxiesService trustedProxiesService) { + + this.settingsService = settingsService; + this.directoriesService = directoriesService; + this.applicationsService = applicationsService; + this.applicationLinksService = applicationLinksService; + this.licensesService = licensesService; + this.mailServerService = mailServerService; + this.mailTemplatesService = mailTemplatesService; + this.sessionConfigService = sessionConfigService; + this.trustedProxiesService = trustedProxiesService; + } + + @Override + public _AllModel setAll( + final _AllModel allModel) { + + final _AllModel result = new _AllModel(); + final Map statusMap = new HashMap<>(); + + setEntityWithStatus(allModel.getSettings(), + settingsService::setSettings, result::setSettings, statusMap); + + setEntities(DIRECTORIES, allModel.getDirectories(), + directoriesService::setDirectories, result::setDirectories, statusMap); + + setEntities(APPLICATIONS, allModel.getApplications(), + applicationsService::setApplications, result::setApplications, statusMap); + + setEntities(APPLICATION_LINKS, allModel.getApplicationLinks(), + applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); + + setEntities(LICENSES, allModel.getLicenses(), + licensesService::setLicenses, result::setLicenses, statusMap); + + setEntityWithStatus(allModel.getMailServer(), + mailServerService::setMailServer, result::setMailServer, statusMap); + + setEntity(ServiceResultUtil.entityType(MailTemplatesModel.class), allModel.getMailTemplates(), + mailTemplatesService::setMailTemplates, result::setMailTemplates, statusMap); + + setEntity(ServiceResultUtil.entityType(SessionConfigModel.class), allModel.getSessionConfig(), + sessionConfigService::setSessionConfig, result::setSessionConfig, statusMap); + + setEntity(TRUSTED_PROXIES, allModel.getTrustedProxies(), + trustedProxiesService::setTrustedProxies, result::setTrustedProxies, statusMap); + + result.setStatus(statusMap); + return result; + } +} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/AllService.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/AllService.java deleted file mode 100644 index 71e5a090..00000000 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/AllService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.deftdevs.bootstrapi.crowd.service.api; - -import com.deftdevs.bootstrapi.crowd.model.AllModel; - -public interface AllService { - - void setAll( - AllModel allModel); - -} diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsGeneralService.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsGeneralService.java index a7098e91..610100cb 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsGeneralService.java +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsGeneralService.java @@ -1,8 +1,8 @@ package com.deftdevs.bootstrapi.crowd.service.api; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; public interface CrowdSettingsGeneralService extends - SettingsService { + SettingsGeneralService { } diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java new file mode 100644 index 00000000..1bea779f --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java @@ -0,0 +1,46 @@ +package com.deftdevs.bootstrapi.crowd.service.api; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; + +import java.util.LinkedHashMap; +import java.util.Map; + +public interface CrowdSettingsService extends + CrowdSettingsGeneralService, + CrowdSettingsBrandingService { + + default SettingsModel getSettings() { + return new SettingsModel(getSettingsGeneral(), getLoginPage()); + } + + default ServiceResult setSettings(final SettingsModel settingsModel) { + final SettingsModel result = new SettingsModel(); + final Map status = new LinkedHashMap<>(); + + if (settingsModel.getGeneral() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); + try { + result.setGeneral(setSettingsGeneral(settingsModel.getGeneral())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getBranding() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsBrandingLoginPageModel.class); + try { + result.setBranding(setLoginPage(settingsModel.getBranding())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + +} diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java new file mode 100644 index 00000000..826e0d3a --- /dev/null +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java @@ -0,0 +1,93 @@ +package com.deftdevs.bootstrapi.crowd.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.crowd.model.ApplicationModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; +import com.deftdevs.bootstrapi.crowd.model._AllModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class _AllResourceTest { + + @Mock + private _AllService<_AllModel> allService; + + private _AllResourceImpl allResource; + + private _AllModel allModel; + + @BeforeEach + public void setup() { + allResource = new _AllResourceImpl(allService); + + allModel = new _AllModel(); + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + allModel.setSettings(settings); + + final Map applications = new HashMap<>(); + applications.put(ApplicationModel.EXAMPLE_1.getName(), ApplicationModel.EXAMPLE_1); + allModel.setApplications(applications); + + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, _AllModelStatus.success()); + status.put(APPLICATIONS, _AllModelStatus.success()); + allModel.setStatus(status); + } + + @Test + public void testSetAll() { + doReturn(allModel).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(200, response.getStatus()); + + final _AllModel responseModel = (_AllModel) response.getEntity(); + assertEquals(allModel, responseModel); + + verify(allService).setAll(allModel); + } + + @Test + public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(500, response.getStatus()); + } + + @Test + public void testSetAllReturns207OnMixedSuccessAndClientError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(207, response.getStatus()); + } +} diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java new file mode 100644 index 00000000..85e932e4 --- /dev/null +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java @@ -0,0 +1,92 @@ +package com.deftdevs.bootstrapi.crowd.service; + +import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsGeneralService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class CrowdSettingsServiceTest { + + @Mock + private CrowdSettingsGeneralService settingsGeneralService; + + @Mock + private CrowdSettingsBrandingService settingsBrandingService; + + private CrowdSettingsServiceImpl crowdSettingsService; + + @BeforeEach + void setup() { + crowdSettingsService = new CrowdSettingsServiceImpl(settingsGeneralService, settingsBrandingService); + } + + @Test + void testGetSettings() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsGeneralService).getSettingsGeneral(); + doReturn(SettingsBrandingLoginPageModel.EXAMPLE_1).when(settingsBrandingService).getLoginPage(); + + final SettingsModel settingsModel = crowdSettingsService.getSettings(); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, settingsModel.getGeneral()); + assertEquals(SettingsBrandingLoginPageModel.EXAMPLE_1, settingsModel.getBranding()); + } + + @Test + void testSetSettingsAppliesAllSubFields() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsGeneralService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doReturn(SettingsBrandingLoginPageModel.EXAMPLE_1).when(settingsBrandingService).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); + + final ServiceResult result = crowdSettingsService.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + SettingsBrandingLoginPageModel.EXAMPLE_1)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertEquals(SettingsBrandingLoginPageModel.EXAMPLE_1, result.getModel().getBranding()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + } + + @Test + void testSetSettingsSkipsNullSubFields() { + final ServiceResult result = crowdSettingsService.setSettings(new SettingsModel()); + + assertTrue(result.getStatus().isEmpty()); + verify(settingsGeneralService, never()).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + verify(settingsBrandingService, never()).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); + } + + @Test + void testSetSettingsRecordsPerSubFieldFailure() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsGeneralService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doThrow(new BadRequestException("invalid login page")) + .when(settingsBrandingService).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); + + final ServiceResult result = crowdSettingsService.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + SettingsBrandingLoginPageModel.EXAMPLE_1)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertNull(result.getModel().getBranding()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("settings/branding").getStatus()); + } +} diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/DirectoriesServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/DirectoriesServiceTest.java index 4fcf66ca..9f89f027 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/DirectoriesServiceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/DirectoriesServiceTest.java @@ -139,7 +139,6 @@ public void testSetDirectoriesAddNewUnsupportedType() { final AbstractDirectoryModel directoryModel = DirectoryModelUtil.toDirectoryModel(directoryAzureAd); final Map directoryModels = Collections.singletonMap(directoryModel.getName(), directoryModel); - final boolean testConnection = false; assertThrows(BadRequestException.class, () -> { spy.setDirectories(directoryModels); diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java index c146d736..c1af5d19 100644 --- a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceTest.java @@ -3,7 +3,7 @@ import com.atlassian.crowd.manager.property.PropertyManager; import com.atlassian.crowd.manager.property.PropertyManagerException; import com.deftdevs.bootstrapi.commons.exception.web.InternalServerErrorException; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -12,7 +12,7 @@ import java.net.URISyntaxException; -import static com.deftdevs.bootstrapi.commons.model.SettingsModel.EXAMPLE_1_NO_MODE; +import static com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel.EXAMPLE_1_NO_MODE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @@ -23,7 +23,7 @@ public class SettingsServiceTest { @Mock private PropertyManager propertyManager; - public static final SettingsModel SETTINGS_MODEL = EXAMPLE_1_NO_MODE; + public static final SettingsGeneralModel SETTINGS_MODEL = EXAMPLE_1_NO_MODE; private SettingsServiceImpl settingsService; diff --git a/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java new file mode 100644 index 00000000..2e09f792 --- /dev/null +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java @@ -0,0 +1,194 @@ +package com.deftdevs.bootstrapi.crowd.service; + +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.crowd.model.ApplicationModel; +import com.deftdevs.bootstrapi.crowd.model.MailTemplatesModel; +import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; +import com.deftdevs.bootstrapi.crowd.model._AllModel; +import com.deftdevs.bootstrapi.crowd.service.api.ApplicationsService; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsService; +import com.deftdevs.bootstrapi.crowd.service.api.MailTemplatesService; +import com.deftdevs.bootstrapi.crowd.service.api.SessionConfigService; +import com.deftdevs.bootstrapi.crowd.service.api.TrustedProxiesService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class _AllServiceImplTest { + + @Mock + private CrowdSettingsService settingsService; + + @Mock + private DirectoriesService directoriesService; + + @Mock + private ApplicationsService applicationsService; + + @Mock + private ApplicationLinksService applicationLinksService; + + @Mock + private LicensesService licensesService; + + @Mock + private MailServerService mailServerService; + + @Mock + private MailTemplatesService mailTemplatesService; + + @Mock + private SessionConfigService sessionConfigService; + + @Mock + private TrustedProxiesService trustedProxiesService; + + private _AllServiceImpl allService; + + @BeforeEach + void setup() { + allService = new _AllServiceImpl( + settingsService, + directoriesService, + applicationsService, + applicationLinksService, + licensesService, + mailServerService, + mailTemplatesService, + sessionConfigService, + trustedProxiesService); + } + + @Test + void testSetAllEmptyModelYieldsEmptyStatus() { + final _AllModel result = allService.setAll(new _AllModel()); + + assertTrue(result.getStatus().isEmpty()); + verifyNoInteractions(settingsService, directoriesService, applicationsService, + applicationLinksService, licensesService, mailServerService, + mailTemplatesService, sessionConfigService, trustedProxiesService); + } + + @Test + void testSetAllAppliesAllFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map directories = + Collections.singletonMap("directory", mock(AbstractDirectoryModel.class)); + final Map applications = + Collections.singletonMap("application", new ApplicationModel()); + final Map applicationLinks = + Collections.singletonMap("link", ApplicationLinkModel.EXAMPLE_1); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + final Map redactedLicenses = + Collections.singletonMap("lice...nse1#abcd", LicenseModel.EXAMPLE_1); + final MailServerModel mailServer = new MailServerModel(MailServerSmtpModel.EXAMPLE_1, null); + final MailTemplatesModel mailTemplates = MailTemplatesModel.EXAMPLE_1; + final SessionConfigModel sessionConfig = new SessionConfigModel(); + final List trustedProxies = Collections.singletonList("192.168.0.1"); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doReturn(directories).when(directoriesService).setDirectories(directories); + doReturn(applications).when(applicationsService).setApplications(applications); + doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); + doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); + doReturn(new ServiceResult<>(mailServer, + Collections.singletonMap("mail-server/smtp", _AllModelStatus.success()))) + .when(mailServerService).setMailServer(mailServer); + doReturn(mailTemplates).when(mailTemplatesService).setMailTemplates(mailTemplates); + doReturn(sessionConfig).when(sessionConfigService).setSessionConfig(sessionConfig); + doReturn(trustedProxies).when(trustedProxiesService).setTrustedProxies(trustedProxies); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setDirectories(directories); + allModel.setApplications(applications); + allModel.setApplicationLinks(applicationLinks); + allModel.setLicenses(licenses); + allModel.setMailServer(mailServer); + allModel.setMailTemplates(mailTemplates); + allModel.setSessionConfig(sessionConfig); + allModel.setTrustedProxies(trustedProxies); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertEquals(directories, result.getDirectories()); + assertEquals(applications, result.getApplications()); + assertEquals(applicationLinks, result.getApplicationLinks()); + assertEquals(redactedLicenses, result.getLicenses()); + assertEquals(mailServer, result.getMailServer()); + assertEquals(mailTemplates, result.getMailTemplates()); + assertEquals(sessionConfig, result.getSessionConfig()); + assertEquals(trustedProxies, result.getTrustedProxies()); + + final Map status = result.getStatus(); + assertEquals(9, status.size()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("applications").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("application-links").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-server/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-templates").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("session-config").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("trusted-proxies").getStatus()); + } + + @Test + void testSetAllRecordsFailureAndContinuesWithOtherFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doThrow(new WebApplicationException(Response.Status.CONFLICT)) + .when(licensesService).setLicenses(licenses); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setLicenses(licenses); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertNull(result.getLicenses()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get("licenses").getStatus()); + } +} diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java new file mode 100644 index 00000000..78543077 --- /dev/null +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/FuncTestExamples.java @@ -0,0 +1,31 @@ +package it.com.deftdevs.bootstrapi.crowd.rest; + +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; + +/** + * Example entities for functional tests. Unlike the {@code EXAMPLE_*} constants on the + * models (which document the API), these values describe the integration test + * environment — the mail server points at the greenmail service started for + * integration test runs (see {@code ci.yaml}), and the general settings are limited + * to the fields supported by Crowd. + */ +public final class FuncTestExamples { + + public static final SettingsGeneralModel SETTINGS_GENERAL = SettingsGeneralModel.builder() + .baseUrl(SettingsGeneralModel.EXAMPLE_1.getBaseUrl()) + .title(SettingsGeneralModel.EXAMPLE_1.getTitle()) + .build(); + + public static final MailServerSmtpModel MAIL_SERVER_SMTP_GREENMAIL = MailServerSmtpModel.builder() + .from("test@example.com") + .prefix("[Test]") + .host("localhost") + .port(3025) + .useTls(false) + .timeout(5000L) + .build(); + + private FuncTestExamples() { + } +} diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java new file mode 100644 index 00000000..622bb6d1 --- /dev/null +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsGeneralResourceFuncTest.java @@ -0,0 +1,13 @@ +package it.com.deftdevs.bootstrapi.crowd.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceFuncTest; + +public class SettingsGeneralResourceFuncTest extends AbstractSettingsGeneralResourceFuncTest { + + @Override + protected SettingsGeneralModel getExampleModel() { + return FuncTestExamples.SETTINGS_GENERAL; + } + +} diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceFuncTest.java deleted file mode 100644 index ca31a892..00000000 --- a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/SettingsResourceFuncTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package it.com.deftdevs.bootstrapi.crowd.rest; - -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceFuncTest; - -public class SettingsResourceFuncTest extends AbstractSettingsResourceFuncTest { - - @Override - protected SettingsModel getExampleModel() { - return SettingsModel.builder() - .baseUrl(SettingsModel.EXAMPLE_1.getBaseUrl()) - .title(SettingsModel.EXAMPLE_1.getTitle()) - .build(); - } - -} diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java new file mode 100644 index 00000000..ffee15c4 --- /dev/null +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java @@ -0,0 +1,55 @@ +package it.com.deftdevs.bootstrapi.crowd.rest; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.crowd.model.MailTemplatesModel; +import com.deftdevs.bootstrapi.crowd.model.SessionConfigModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; +import com.deftdevs.bootstrapi.crowd.model._AllModel; +import it.com.deftdevs.bootstrapi.commons.rest.Abstract_AllResourceFuncTest; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class _AllResourceFuncTest extends Abstract_AllResourceFuncTest { + + @Override + protected Object getExampleAllModel() { + return _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .build()) + .build(); + } + + @Override + protected SettingsGeneralModel getExampleSettingsGeneralModel() { + return FuncTestExamples.SETTINGS_GENERAL; + } + + @Test + void testSetAllAppliesMultipleEntities() throws Exception { + // no POP mail server here: setting POP is unsupported by Crowd + final _AllModel allModel = _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .build()) + .mailServer(MailServerModel.builder() + .smtp(FuncTestExamples.MAIL_SERVER_SMTP_GREENMAIL) + .build()) + .mailTemplates(MailTemplatesModel.EXAMPLE_1) + .sessionConfig(SessionConfigModel.EXAMPLE_2) + .trustedProxies(Arrays.asList("10.0.0.1", "10.0.0.2")) + .build(); + + assertSetAllApplied(allModel, Arrays.asList( + ServiceResultUtil.subEntityKey(SettingsGeneralModel.class), + ServiceResultUtil.subEntityKey(MailServerSmtpModel.class), + ServiceResultUtil.entityType(MailTemplatesModel.class), + ServiceResultUtil.entityType(SessionConfigModel.class), + BootstrAPI.TRUSTED_PROXIES)); + } +} diff --git a/jira/Apis/AllApi.md b/jira/Apis/AllApi.md new file mode 100644 index 00000000..1c17cae5 --- /dev/null +++ b/jira/Apis/AllApi.md @@ -0,0 +1,36 @@ +# AllApi + +All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**setAll**](AllApi.md#setAll) | **PUT** / | Apply a complete configuration | + + + +# **setAll** +> _AllModel setAll(\_AllModel) + +Apply a complete configuration + + Returns the updated configuration. The per-sub-field outcome is reported in the 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details'). License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2'). + +### Parameters + +|Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **\_AllModel** | [**_AllModel**](../Models/_AllModel.md)| | | + +### Return type + +[**_AllModel**](../Models/_AllModel.md) + +### Authorization + +[basicAuth](../README.md#basicAuth) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + diff --git a/jira/Apis/SettingsApi.md b/jira/Apis/SettingsApi.md index 5f33d6c1..adc60745 100644 --- a/jira/Apis/SettingsApi.md +++ b/jira/Apis/SettingsApi.md @@ -36,7 +36,7 @@ This endpoint does not need any parameter. # **getSettings** -> SettingsModel getSettings() +> SettingsGeneralModel getSettings() Get the general settings @@ -45,7 +45,7 @@ This endpoint does not need any parameter. ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization @@ -105,7 +105,7 @@ Set the banner # **setSettings** -> SettingsModel setSettings(SettingsModel) +> SettingsGeneralModel setSettings(SettingsGeneralModel) Set the general settings @@ -113,11 +113,11 @@ Set the general settings |Name | Type | Description | Notes | |------------- | ------------- | ------------- | -------------| -| **SettingsModel** | [**SettingsModel**](../Models/SettingsModel.md)| | [optional] | +| **SettingsGeneralModel** | [**SettingsGeneralModel**](../Models/SettingsGeneralModel.md)| | [optional] | ### Return type -[**SettingsModel**](../Models/SettingsModel.md) +[**SettingsGeneralModel**](../Models/SettingsGeneralModel.md) ### Authorization diff --git a/jira/Models/AuthenticationModel.md b/jira/Models/AuthenticationModel.md new file mode 100644 index 00000000..b873bdc6 --- /dev/null +++ b/jira/Models/AuthenticationModel.md @@ -0,0 +1,10 @@ +# AuthenticationModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **idps** | [**Map**](AbstractAuthenticationIdpModel.md) | | [optional] [default to null] | +| **sso** | [**AuthenticationSsoModel**](AuthenticationSsoModel.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/jira/Models/MailServerModel.md b/jira/Models/MailServerModel.md new file mode 100644 index 00000000..34c39bf7 --- /dev/null +++ b/jira/Models/MailServerModel.md @@ -0,0 +1,10 @@ +# MailServerModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **smtp** | [**MailServerSmtpModel**](MailServerSmtpModel.md) | | [optional] [default to null] | +| **pop** | [**MailServerPopModel**](MailServerPopModel.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/jira/Models/SettingsGeneralModel.md b/jira/Models/SettingsGeneralModel.md new file mode 100644 index 00000000..8ebe677b --- /dev/null +++ b/jira/Models/SettingsGeneralModel.md @@ -0,0 +1,13 @@ +# SettingsGeneralModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **baseUrl** | **URI** | | [optional] [default to null] | +| **mode** | **String** | | [optional] [default to null] | +| **title** | **String** | | [optional] [default to null] | +| **contactMessage** | **String** | | [optional] [default to null] | +| **externalUserManagement** | **Boolean** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/jira/Models/SettingsModel.md b/jira/Models/SettingsModel.md index f4eb0e7f..23d5cc40 100644 --- a/jira/Models/SettingsModel.md +++ b/jira/Models/SettingsModel.md @@ -3,11 +3,9 @@ | Name | Type | Description | Notes | |------------ | ------------- | ------------- | -------------| -| **baseUrl** | **URI** | | [optional] [default to null] | -| **mode** | **String** | | [optional] [default to null] | -| **title** | **String** | | [optional] [default to null] | -| **contactMessage** | **String** | | [optional] [default to null] | -| **externalUserManagement** | **Boolean** | | [optional] [default to null] | +| **general** | [**SettingsGeneralModel**](SettingsGeneralModel.md) | | [optional] [default to null] | +| **security** | [**SettingsSecurityModel**](SettingsSecurityModel.md) | | [optional] [default to null] | +| **banner** | [**SettingsBannerModel**](SettingsBannerModel.md) | | [optional] [default to null] | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/jira/Models/_AllModel.md b/jira/Models/_AllModel.md new file mode 100644 index 00000000..3a2e4dee --- /dev/null +++ b/jira/Models/_AllModel.md @@ -0,0 +1,16 @@ +# _AllModel +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **settings** | [**SettingsModel**](SettingsModel.md) | | [optional] [default to null] | +| **directories** | [**Map**](AbstractDirectoryModel.md) | | [optional] [default to null] | +| **applicationLinks** | [**Map**](ApplicationLinkModel.md) | | [optional] [default to null] | +| **authentication** | [**AuthenticationModel**](AuthenticationModel.md) | | [optional] [default to null] | +| **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | +| **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | +| **permissionsGlobal** | [**PermissionsGlobalModel**](PermissionsGlobalModel.md) | | [optional] [default to null] | +| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/jira/Models/_AllModelStatus.md b/jira/Models/_AllModelStatus.md new file mode 100644 index 00000000..ed699765 --- /dev/null +++ b/jira/Models/_AllModelStatus.md @@ -0,0 +1,11 @@ +# _AllModelStatus +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +| **status** | **Integer** | | [optional] [default to null] | +| **message** | **String** | | [optional] [default to null] | +| **details** | **String** | | [optional] [default to null] | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/jira/README.md b/jira/README.md index 464f61c3..1680c658 100644 --- a/jira/README.md +++ b/jira/README.md @@ -7,6 +7,7 @@ All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* | Class | Method | HTTP request | Description | |------------ | ------------- | ------------- | -------------| +| *AllApi* | [**setAll**](Apis/AllApi.md#setAll) | **PUT** / | Apply a complete configuration | | *ApplicationLinkApi* | [**createApplicationLink**](Apis/ApplicationLinkApi.md#createApplicationLink) | **POST** /application-link | Create an application link | *ApplicationLinkApi* | [**deleteApplicationLink**](Apis/ApplicationLinkApi.md#deleteApplicationLink) | **DELETE** /application-link/{uuid} | Delete an application link | *ApplicationLinkApi* | [**getApplicationLink**](Apis/ApplicationLinkApi.md#getApplicationLink) | **GET** /application-link/{uuid} | Get an application link | @@ -53,6 +54,7 @@ All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* - [ApplicationLinkModel](./Models/ApplicationLinkModel.md) - [AuthenticationIdpOidcModel](./Models/AuthenticationIdpOidcModel.md) - [AuthenticationIdpSamlModel](./Models/AuthenticationIdpSamlModel.md) + - [AuthenticationModel](./Models/AuthenticationModel.md) - [AuthenticationSsoModel](./Models/AuthenticationSsoModel.md) - [DirectoryCrowdAdvanced](./Models/DirectoryCrowdAdvanced.md) - [DirectoryCrowdModel](./Models/DirectoryCrowdModel.md) @@ -74,13 +76,17 @@ All URIs are relative to *https://JIRA_URL/rest/bootstrapi/1* - [ErrorCollection](./Models/ErrorCollection.md) - [GroupModel](./Models/GroupModel.md) - [LicenseModel](./Models/LicenseModel.md) + - [MailServerModel](./Models/MailServerModel.md) - [MailServerPopModel](./Models/MailServerPopModel.md) - [MailServerSmtpModel](./Models/MailServerSmtpModel.md) - [PermissionsGlobalModel](./Models/PermissionsGlobalModel.md) - [SettingsBannerModel](./Models/SettingsBannerModel.md) + - [SettingsGeneralModel](./Models/SettingsGeneralModel.md) - [SettingsModel](./Models/SettingsModel.md) - [SettingsSecurityModel](./Models/SettingsSecurityModel.md) - [UserModel](./Models/UserModel.md) + - [_AllModel](./Models/_AllModel.md) + - [_AllModelStatus](./Models/_AllModelStatus.md) diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/config/ServiceConfig.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/config/ServiceConfig.java index bc0b1f91..0671d8c7 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/config/ServiceConfig.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/config/ServiceConfig.java @@ -1,6 +1,7 @@ package com.deftdevs.bootstrapi.jira.config; import com.deftdevs.bootstrapi.commons.service.api.*; +import com.deftdevs.bootstrapi.jira.model._AllModel; import com.deftdevs.bootstrapi.jira.service.*; import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; @@ -17,6 +18,18 @@ public class ServiceConfig { @Autowired private HelperConfig helperConfig; + @Bean + public _AllService<_AllModel> _allService() { + return new _AllServiceImpl( + jiraSettingsService(), + directoriesService(), + applicationLinksService(), + jiraAuthenticationService(), + licensesService(), + mailServerService(), + permissionsService()); + } + @Bean public ApplicationLinksService applicationLinksService() { return new ApplicationLinksServiceImpl( diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java new file mode 100644 index 00000000..18cb985a --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java @@ -0,0 +1,31 @@ +package com.deftdevs.bootstrapi.jira.model; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = SETTINGS) +public class SettingsModel { + + @XmlElement + private SettingsGeneralModel general; + + @XmlElement + private SettingsSecurityModel security; + + @XmlElement + private SettingsBannerModel banner; + +} diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java new file mode 100644 index 00000000..eb49dfba --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java @@ -0,0 +1,52 @@ +package com.deftdevs.bootstrapi.jira.model; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = BootstrAPI._ALL) +public class _AllModel implements _AllModelAccessor { + + @XmlElement + private SettingsModel settings; + + @XmlElement + private Map directories; + + @XmlElement + private Map applicationLinks; + + @XmlElement + private AuthenticationModel authentication; + + @XmlElement + private Map licenses; + + @XmlElement + private MailServerModel mailServer; + + @XmlElement + private PermissionsGlobalModel permissionsGlobal; + + @XmlElement + private Map status; + +} diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsResourceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceImpl.java similarity index 69% rename from jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsResourceImpl.java rename to jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceImpl.java index f8b5773b..0730eea6 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsResourceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceImpl.java @@ -2,8 +2,8 @@ import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceImpl; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceImpl; import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; import io.swagger.v3.oas.annotations.tags.Tag; @@ -18,10 +18,10 @@ @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @SystemAdminOnly -public class SettingsResourceImpl extends AbstractSettingsResourceImpl { +public class SettingsGeneralResourceImpl extends AbstractSettingsGeneralResourceImpl { @Inject - public SettingsResourceImpl( + public SettingsGeneralResourceImpl( final JiraSettingsService settingsService) { super(settingsService); diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java new file mode 100644 index 00000000..b5ffe089 --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java @@ -0,0 +1,64 @@ +package com.deftdevs.bootstrapi.jira.rest; + +import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.ErrorCollection; +import com.deftdevs.bootstrapi.commons.rest._AbstractAllResourceImpl; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.jira.model._AllModel; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path(BootstrAPI._ROOT) +@Tag(name = BootstrAPI._ALL) +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +@SystemAdminOnly +public class _AllResourceImpl extends _AbstractAllResourceImpl<_AllModel> { + + @Inject + public _AllResourceImpl( + final _AllService<_AllModel> allService) { + + super(allService); + } + + // overridden to document the concrete response model in the generated OpenAPI spec + @PUT + @Operation( + summary = BootstrAPI._ALL_PUT_SUMMARY, + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION, + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "207", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = BootstrAPI.ERROR_COLLECTION_RESPONSE_DESCRIPTION + ), + } + ) + @Override + public Response setAll( + final _AllModel allModel) { + + return super.setAll(allModel); + } + +} diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java index bdf8badc..6f50abfb 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceImpl.java @@ -3,9 +3,12 @@ import com.atlassian.jira.license.JiraLicenseManager; import com.deftdevs.bootstrapi.commons.model.LicenseModel; import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; import com.deftdevs.bootstrapi.jira.model.util.LicenseModelUtil; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -36,6 +39,17 @@ public List setLicenses( return getLicenses(); } + @Override + public Map setLicenses( + final Map licenseInputs) { + + final Map result = new LinkedHashMap<>(); + for (final String key : licenseInputs.keySet()) { + result.put(LicenseKeyRedactor.redact(key), addLicense(key)); + } + return result; + } + @Override public LicenseModel addLicense( final String license) { diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceImpl.java index 961a3437..dad34f63 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceImpl.java @@ -3,7 +3,7 @@ import com.atlassian.jira.config.properties.APKeys; import com.atlassian.jira.config.properties.ApplicationProperties; import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; @@ -28,14 +28,14 @@ public SettingsServiceImpl( } @Override - public SettingsModel getSettingsGeneral() { + public SettingsGeneralModel getSettingsGeneral() { final String baseUrl = applicationProperties.getString(JIRA_BASEURL); final String mode = applicationProperties.getString(JIRA_MODE); final String title = applicationProperties.getString(JIRA_TITLE); final String contactMessage = applicationProperties.getString(JIRA_CONTACT_ADMINISTRATORS_MESSSAGE); final Boolean externalUserManagement = Boolean.parseBoolean(applicationProperties.getString(JIRA_OPTION_USER_EXTERNALMGT)); - final SettingsModel settingsModel = SettingsModel.builder() + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder() .baseUrl(baseUrl != null ? URI.create(baseUrl) : null) .mode(mode) .title(title) @@ -47,8 +47,8 @@ public SettingsModel getSettingsGeneral() { } @Override - public SettingsModel setSettingsGeneral( - final SettingsModel settingsModel) { + public SettingsGeneralModel setSettingsGeneral( + final SettingsGeneralModel settingsModel) { if (settingsModel.getBaseUrl() != null) { applicationProperties.setString(JIRA_BASEURL, settingsModel.getBaseUrl().toString()); diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java new file mode 100644 index 00000000..3b154105 --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java @@ -0,0 +1,82 @@ +package com.deftdevs.bootstrapi.jira.service; + +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service._AbstractAllServiceImpl; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.jira.model._AllModel; +import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; +import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; + +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.APPLICATION_LINKS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.DIRECTORIES; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.LICENSES; + +public class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel> { + + private final JiraSettingsService settingsService; + private final DirectoriesService directoriesService; + private final ApplicationLinksService applicationLinksService; + private final JiraAuthenticationService authenticationService; + private final LicensesService licensesService; + private final MailServerService mailServerService; + private final PermissionsService permissionsService; + + public _AllServiceImpl( + final JiraSettingsService settingsService, + final DirectoriesService directoriesService, + final ApplicationLinksService applicationLinksService, + final JiraAuthenticationService authenticationService, + final LicensesService licensesService, + final MailServerService mailServerService, + final PermissionsService permissionsService) { + + this.settingsService = settingsService; + this.directoriesService = directoriesService; + this.applicationLinksService = applicationLinksService; + this.authenticationService = authenticationService; + this.licensesService = licensesService; + this.mailServerService = mailServerService; + this.permissionsService = permissionsService; + } + + @Override + public _AllModel setAll( + final _AllModel allModel) { + + final _AllModel result = new _AllModel(); + final Map statusMap = new HashMap<>(); + + setEntityWithStatus(allModel.getSettings(), + settingsService::setSettings, result::setSettings, statusMap); + + setEntities(DIRECTORIES, allModel.getDirectories(), + directoriesService::setDirectories, result::setDirectories, statusMap); + + setEntities(APPLICATION_LINKS, allModel.getApplicationLinks(), + applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); + + setEntityWithStatus(allModel.getAuthentication(), + authenticationService::setAuthentication, result::setAuthentication, statusMap); + + setEntities(LICENSES, allModel.getLicenses(), + licensesService::setLicenses, result::setLicenses, statusMap); + + setEntityWithStatus(allModel.getMailServer(), + mailServerService::setMailServer, result::setMailServer, statusMap); + + setEntity(ServiceResultUtil.subEntityKey(PermissionsGlobalModel.class), allModel.getPermissionsGlobal(), + permissionsService::setPermissionsGlobal, result::setPermissionsGlobal, statusMap); + + result.setStatus(statusMap); + return result; + } +} diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java index 7c6b54d8..ed439921 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraSettingsService.java @@ -1,13 +1,20 @@ package com.deftdevs.bootstrapi.jira.service.api; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; import com.deftdevs.bootstrapi.commons.service.api.SettingsSecurityService; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; + +import java.util.LinkedHashMap; +import java.util.Map; public interface JiraSettingsService extends - SettingsService, + SettingsGeneralService, SettingsSecurityService { SettingsBannerModel getSettingsBanner(); @@ -15,4 +22,42 @@ public interface JiraSettingsService extends SettingsBannerModel setSettingsBanner( SettingsBannerModel settingsBannerModel); + default SettingsModel getSettings() { + return new SettingsModel(getSettingsGeneral(), getSettingsSecurity(), getSettingsBanner()); + } + + default ServiceResult setSettings(final SettingsModel settingsModel) { + final SettingsModel result = new SettingsModel(); + final Map status = new LinkedHashMap<>(); + + if (settingsModel.getGeneral() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsGeneralModel.class); + try { + result.setGeneral(setSettingsGeneral(settingsModel.getGeneral())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getSecurity() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsSecurityModel.class); + try { + result.setSecurity(setSettingsSecurity(settingsModel.getSecurity())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (settingsModel.getBanner() != null) { + final String key = ServiceResultUtil.subEntityKey(SettingsBannerModel.class); + try { + result.setBanner(setSettingsBanner(settingsModel.getBanner())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + } diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java new file mode 100644 index 00000000..c5fe65f0 --- /dev/null +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java @@ -0,0 +1,88 @@ +package com.deftdevs.bootstrapi.jira.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; +import com.deftdevs.bootstrapi.jira.model._AllModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class _AllResourceTest { + + @Mock + private _AllService<_AllModel> allService; + + private _AllResourceImpl allResource; + + private _AllModel allModel; + + @BeforeEach + public void setup() { + allResource = new _AllResourceImpl(allService); + + allModel = new _AllModel(); + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + allModel.setSettings(settings); + + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, _AllModelStatus.success()); + status.put(APPLICATION_LINKS, _AllModelStatus.success()); + allModel.setStatus(status); + } + + @Test + public void testSetAll() { + doReturn(allModel).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(200, response.getStatus()); + + final _AllModel responseModel = (_AllModel) response.getEntity(); + assertEquals(allModel, responseModel); + + verify(allService).setAll(allModel); + } + + @Test + public void testSetAllReturns500WhenAnySubFieldFailedWithServerError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "boom", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(500, response.getStatus()); + } + + @Test + public void testSetAllReturns207OnMixedSuccessAndClientError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(SETTINGS + "/" + SETTINGS_GENERAL, _AllModelStatus.success()); + status.put(DIRECTORIES, + _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + result.setStatus(status); + doReturn(result).when(allService).setAll(any()); + + final Response response = allResource.setAll(allModel); + assertEquals(207, response.getStatus()); + } +} diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java index dbd56e8a..72e05366 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/JiraSettingsServiceTest.java @@ -2,17 +2,23 @@ import com.atlassian.jira.config.properties.ApplicationProperties; import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import javax.ws.rs.core.Response; import java.net.URI; import static com.atlassian.jira.config.properties.APKeys.*; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @@ -43,7 +49,7 @@ void testGetSettingsGeneral() { doReturn(CONTACT_MESSAGE).when(applicationProperties).getString(JIRA_CONTACT_ADMINISTRATORS_MESSSAGE); doReturn(EXTERNAL_USER_MANAGEMENT).when(applicationProperties).getString(JIRA_OPTION_USER_EXTERNALMGT); - final SettingsModel settingsModel = settingsService.getSettingsGeneral(); + final SettingsGeneralModel settingsModel = settingsService.getSettingsGeneral(); assertEquals(BASE_URL, settingsModel.getBaseUrl()); assertEquals(MODE_PUBLIC, settingsModel.getMode()); @@ -54,7 +60,7 @@ void testGetSettingsGeneral() { @Test void testSetSettingsGeneral() { - final SettingsModel settingsModel = SettingsModel.builder() + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder() .baseUrl(BASE_URL) .mode(MODE_PUBLIC) .title(TITLE) @@ -73,7 +79,7 @@ void testSetSettingsGeneral() { @Test void testSetSettingsGeneralEmptyModel() { - final SettingsModel settingsModel = SettingsModel.builder().build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().build(); settingsService.setSettingsGeneral(settingsModel); @@ -85,7 +91,7 @@ void testSetSettingsGeneralEmptyModel() { @Test void testSetSettingsGeneralUnsupportedMode() { - final SettingsModel settingsModel = SettingsModel.builder().mode("unsupported").build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().mode("unsupported").build(); assertThrows(BadRequestException.class, () -> { settingsService.setSettingsGeneral(settingsModel); @@ -94,7 +100,7 @@ void testSetSettingsGeneralUnsupportedMode() { @Test void testSetSettingsGeneralInvalidCombination() { - final SettingsModel settingsModel = SettingsModel.builder().mode(MODE_PUBLIC).build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().mode(MODE_PUBLIC).build(); doReturn(true).when(applicationProperties).getOption(JIRA_OPTION_USER_EXTERNALMGT); assertThrows(BadRequestException.class, () -> { @@ -102,4 +108,67 @@ void testSetSettingsGeneralInvalidCombination() { }); } + // composite getSettings/setSettings default methods + + @Test + void testGetSettings() { + final SettingsServiceImpl serviceSpy = spy(settingsService); + doReturn(SettingsGeneralModel.EXAMPLE_1).when(serviceSpy).getSettingsGeneral(); + doReturn(SettingsSecurityModel.EXAMPLE_1).when(serviceSpy).getSettingsSecurity(); + doReturn(SettingsBannerModel.EXAMPLE_1).when(serviceSpy).getSettingsBanner(); + + final SettingsModel settingsModel = serviceSpy.getSettings(); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, settingsModel.getGeneral()); + assertEquals(SettingsSecurityModel.EXAMPLE_1, settingsModel.getSecurity()); + assertEquals(SettingsBannerModel.EXAMPLE_1, settingsModel.getBanner()); + } + + @Test + void testSetSettingsAppliesAllSubFields() { + final SettingsServiceImpl serviceSpy = spy(settingsService); + doReturn(SettingsGeneralModel.EXAMPLE_1).when(serviceSpy).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doReturn(SettingsSecurityModel.EXAMPLE_1).when(serviceSpy).setSettingsSecurity(SettingsSecurityModel.EXAMPLE_1); + doReturn(SettingsBannerModel.EXAMPLE_1).when(serviceSpy).setSettingsBanner(SettingsBannerModel.EXAMPLE_1); + + final ServiceResult result = serviceSpy.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + SettingsSecurityModel.EXAMPLE_1, + SettingsBannerModel.EXAMPLE_1)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertEquals(SettingsSecurityModel.EXAMPLE_1, result.getModel().getSecurity()); + assertEquals(SettingsBannerModel.EXAMPLE_1, result.getModel().getBanner()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/security").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/banner").getStatus()); + } + + @Test + void testSetSettingsSkipsNullSubFields() { + final SettingsServiceImpl serviceSpy = spy(settingsService); + + final ServiceResult result = serviceSpy.setSettings(new SettingsModel()); + + assertEquals(0, result.getStatus().size()); + verify(serviceSpy, never()).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + } + + @Test + void testSetSettingsRecordsPerSubFieldFailure() { + final SettingsServiceImpl serviceSpy = spy(settingsService); + doReturn(SettingsGeneralModel.EXAMPLE_1).when(serviceSpy).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doThrow(new BadRequestException("invalid banner")).when(serviceSpy).setSettingsBanner(SettingsBannerModel.EXAMPLE_1); + + final ServiceResult result = serviceSpy.setSettings(new SettingsModel( + SettingsGeneralModel.EXAMPLE_1, + null, + SettingsBannerModel.EXAMPLE_1)); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertNull(result.getModel().getBanner()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get("settings/banner").getStatus()); + } + } diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceTest.java index 6fb05576..44327650 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/LicensesServiceTest.java @@ -12,11 +12,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import static com.atlassian.extras.api.LicenseType.TESTING; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) @@ -64,6 +67,38 @@ void testSetLicenses() { verify(spy).getLicenses(); } + @Test + void testSetLicensesMapRedactsKeysAndPairsPerKey() { + // Set up two distinct LicenseDetails so we can verify each map entry's + // value comes from that key's own addLicense call (not from index-into-getLicenses). + final LicensedApplications apps = mock(LicensedApplications.class); + doReturn(Collections.singleton(ApplicationKey.valueOf("jira"))).when(apps).getKeys(); + final LicenseDetails detailsA = mock(LicenseDetails.class); + doReturn(apps).when(detailsA).getLicensedApplications(); + doReturn(TESTING).when(detailsA).getLicenseType(); + final LicenseDetails detailsB = mock(LicenseDetails.class); + doReturn(apps).when(detailsB).getLicensedApplications(); + doReturn(TESTING).when(detailsB).getLicenseType(); + + doReturn(detailsA).when(licenseManager).setLicense("KEY_A_payload_aaaa"); + doReturn(detailsB).when(licenseManager).setLicense("KEY_B_payload_bbbb"); + + final Map input = new LinkedHashMap<>(); + input.put("KEY_A_payload_aaaa", null); + input.put("KEY_B_payload_bbbb", null); + + final Map result = licensesService.setLicenses(input); + + assertEquals(2, result.size()); + // Keys in the response must be redacted (not the original keys). + assertTrue(result.keySet().stream().allMatch(k -> k.contains("...") && k.contains("#"))); + assertTrue(result.keySet().stream().noneMatch(k -> k.contains("payload"))); + // Each addLicense was called per input key (verifies we aren't relying + // on JiraLicenseManager.getLicenses() ordering). + verify(licenseManager).setLicense("KEY_A_payload_aaaa"); + verify(licenseManager).setLicense("KEY_B_payload_bbbb"); + } + @Test void testAddLicense() { final LicensedApplications licensedApplications = mock(LicensedApplications.class); diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceTest.java index c4992884..c176c44f 100644 --- a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceTest.java +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/SettingsServiceTest.java @@ -2,7 +2,7 @@ import com.atlassian.jira.config.properties.ApplicationProperties; import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import com.deftdevs.bootstrapi.commons.model.SettingsSecurityModel; import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; import org.junit.jupiter.api.BeforeEach; @@ -45,7 +45,7 @@ void testGetSettingsGeneral() { doReturn(CONTACT_MESSAGE).when(applicationProperties).getString(JIRA_CONTACT_ADMINISTRATORS_MESSSAGE); doReturn(EXTERNAL_USER_MANAGEMENT).when(applicationProperties).getString(JIRA_OPTION_USER_EXTERNALMGT); - final SettingsModel settingsModel = settingsService.getSettingsGeneral(); + final SettingsGeneralModel settingsModel = settingsService.getSettingsGeneral(); assertEquals(BASE_URL, settingsModel.getBaseUrl()); assertEquals(MODE_PUBLIC, settingsModel.getMode()); @@ -56,7 +56,7 @@ void testGetSettingsGeneral() { @Test void testSetSettingsGeneral() { - final SettingsModel settingsModel = SettingsModel.builder() + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder() .baseUrl(BASE_URL) .mode(MODE_PUBLIC) .title(TITLE) @@ -75,7 +75,7 @@ void testSetSettingsGeneral() { @Test void testSetSettingsGeneralEmptyModel() { - final SettingsModel settingsModel = SettingsModel.builder().build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().build(); settingsService.setSettingsGeneral(settingsModel); @@ -109,7 +109,7 @@ void testSetSettingsSecurity() { @Test void testSetSettingsGeneralUnsupportedMode() { - final SettingsModel settingsModel = SettingsModel.builder().mode("unsupported").build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().mode("unsupported").build(); assertThrows(BadRequestException.class, () -> { settingsService.setSettingsGeneral(settingsModel); @@ -118,7 +118,7 @@ void testSetSettingsGeneralUnsupportedMode() { @Test void testSetSettingsGeneralInvalidCombination() { - final SettingsModel settingsModel = SettingsModel.builder().mode(MODE_PUBLIC).build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().mode(MODE_PUBLIC).build(); doReturn(true).when(applicationProperties).getOption(JIRA_OPTION_USER_EXTERNALMGT); assertThrows(BadRequestException.class, () -> { diff --git a/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java new file mode 100644 index 00000000..71496718 --- /dev/null +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java @@ -0,0 +1,172 @@ +package com.deftdevs.bootstrapi.jira.service; + +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.service.api.ApplicationLinksService; +import com.deftdevs.bootstrapi.commons.service.api.DirectoriesService; +import com.deftdevs.bootstrapi.commons.service.api.LicensesService; +import com.deftdevs.bootstrapi.commons.service.api.MailServerService; +import com.deftdevs.bootstrapi.commons.service.api.PermissionsService; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; +import com.deftdevs.bootstrapi.jira.model._AllModel; +import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; +import com.deftdevs.bootstrapi.jira.service.api.JiraSettingsService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class _AllServiceImplTest { + + @Mock + private JiraSettingsService settingsService; + + @Mock + private DirectoriesService directoriesService; + + @Mock + private ApplicationLinksService applicationLinksService; + + @Mock + private JiraAuthenticationService authenticationService; + + @Mock + private LicensesService licensesService; + + @Mock + private MailServerService mailServerService; + + @Mock + private PermissionsService permissionsService; + + private _AllServiceImpl allService; + + @BeforeEach + void setup() { + allService = new _AllServiceImpl( + settingsService, + directoriesService, + applicationLinksService, + authenticationService, + licensesService, + mailServerService, + permissionsService); + } + + @Test + void testSetAllEmptyModelYieldsEmptyStatus() { + final _AllModel result = allService.setAll(new _AllModel()); + + assertTrue(result.getStatus().isEmpty()); + verifyNoInteractions(settingsService, directoriesService, applicationLinksService, + authenticationService, licensesService, mailServerService, permissionsService); + } + + @Test + void testSetAllAppliesAllFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map directories = + Collections.singletonMap("directory", mock(AbstractDirectoryModel.class)); + final Map applicationLinks = + Collections.singletonMap("link", ApplicationLinkModel.EXAMPLE_1); + final AuthenticationModel authentication = new AuthenticationModel(); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + final Map redactedLicenses = + Collections.singletonMap("lice...nse1#abcd", LicenseModel.EXAMPLE_1); + final MailServerModel mailServer = new MailServerModel(MailServerSmtpModel.EXAMPLE_1, null); + final PermissionsGlobalModel permissionsGlobal = new PermissionsGlobalModel(); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doReturn(directories).when(directoriesService).setDirectories(directories); + doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); + doReturn(new ServiceResult<>(authentication, + Collections.singletonMap("authentication/sso", _AllModelStatus.success()))) + .when(authenticationService).setAuthentication(authentication); + doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); + doReturn(new ServiceResult<>(mailServer, + Collections.singletonMap("mail-server/smtp", _AllModelStatus.success()))) + .when(mailServerService).setMailServer(mailServer); + doReturn(permissionsGlobal).when(permissionsService).setPermissionsGlobal(permissionsGlobal); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setDirectories(directories); + allModel.setApplicationLinks(applicationLinks); + allModel.setAuthentication(authentication); + allModel.setLicenses(licenses); + allModel.setMailServer(mailServer); + allModel.setPermissionsGlobal(permissionsGlobal); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertEquals(directories, result.getDirectories()); + assertEquals(applicationLinks, result.getApplicationLinks()); + assertEquals(authentication, result.getAuthentication()); + assertEquals(redactedLicenses, result.getLicenses()); + assertEquals(mailServer, result.getMailServer()); + assertEquals(permissionsGlobal, result.getPermissionsGlobal()); + + final Map status = result.getStatus(); + assertEquals(7, status.size()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("settings/general").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("directories").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("application-links").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("authentication/sso").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("licenses").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("mail-server/smtp").getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get("permissions/global").getStatus()); + } + + @Test + void testSetAllRecordsFailureAndContinuesWithOtherFields() { + final SettingsModel settings = new SettingsModel(); + settings.setGeneral(SettingsGeneralModel.EXAMPLE_1); + final Map licenses = + Collections.singletonMap("licenseKey", LicenseModel.EXAMPLE_1); + + doReturn(new ServiceResult<>(settings, + Collections.singletonMap("settings/general", _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doThrow(new WebApplicationException(Response.Status.CONFLICT)) + .when(licensesService).setLicenses(licenses); + + final _AllModel allModel = new _AllModel(); + allModel.setSettings(settings); + allModel.setLicenses(licenses); + + final _AllModel result = allService.setAll(allModel); + + assertEquals(settings, result.getSettings()); + assertNull(result.getLicenses()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get("settings/general").getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get("licenses").getStatus()); + } +} diff --git a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceFuncTest.java b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceFuncTest.java new file mode 100644 index 00000000..d8d7c2d8 --- /dev/null +++ b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsGeneralResourceFuncTest.java @@ -0,0 +1,5 @@ +package it.com.deftdevs.bootstrapi.jira.rest; + +import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsGeneralResourceFuncTest; + +public class SettingsGeneralResourceFuncTest extends AbstractSettingsGeneralResourceFuncTest { } diff --git a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsResourceFuncTest.java b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsResourceFuncTest.java deleted file mode 100644 index cc6cc1a1..00000000 --- a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/SettingsResourceFuncTest.java +++ /dev/null @@ -1,5 +0,0 @@ -package it.com.deftdevs.bootstrapi.jira.rest; - -import it.com.deftdevs.bootstrapi.commons.rest.AbstractSettingsResourceFuncTest; - -public class SettingsResourceFuncTest extends AbstractSettingsResourceFuncTest { } diff --git a/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java new file mode 100644 index 00000000..82212de9 --- /dev/null +++ b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java @@ -0,0 +1,46 @@ +package it.com.deftdevs.bootstrapi.jira.rest; + +import com.deftdevs.bootstrapi.commons.model.MailServerModel; +import com.deftdevs.bootstrapi.commons.model.MailServerPopModel; +import com.deftdevs.bootstrapi.commons.model.MailServerSmtpModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.jira.model.SettingsBannerModel; +import com.deftdevs.bootstrapi.jira.model.SettingsModel; +import com.deftdevs.bootstrapi.jira.model._AllModel; +import it.com.deftdevs.bootstrapi.commons.rest.Abstract_AllResourceFuncTest; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class _AllResourceFuncTest extends Abstract_AllResourceFuncTest { + + @Override + protected Object getExampleAllModel() { + return _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .build()) + .build(); + } + + @Test + void testSetAllAppliesMultipleEntities() throws Exception { + final _AllModel allModel = _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .banner(SettingsBannerModel.EXAMPLE_1) + .build()) + .mailServer(MailServerModel.builder() + .smtp(MailServerSmtpModel.EXAMPLE_2) + .pop(MailServerPopModel.EXAMPLE_2) + .build()) + .build(); + + assertSetAllApplied(allModel, Arrays.asList( + ServiceResultUtil.subEntityKey(SettingsGeneralModel.class), + ServiceResultUtil.subEntityKey(SettingsBannerModel.class), + ServiceResultUtil.subEntityKey(MailServerSmtpModel.class), + ServiceResultUtil.subEntityKey(MailServerPopModel.class))); + } +}