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..e6356b13 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"; @@ -49,6 +51,7 @@ public class BootstrAPI { 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"; @@ -57,10 +60,20 @@ public class BootstrAPI { 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, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp';" + + " 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')." + + " If any sub-field fails, the highest sub-field status code is returned with the same response body."; + public static final String _ALL_PUT_FAILURE_RESPONSE_DESCRIPTION = "One or more sub-fields failed to apply. The response code is the highest" + + " sub-field status code; inspect the per-sub-field 'status' map in the response body to see which" + + " fields succeeded and which failed. Request-level errors (e.g. a missing request body) return an" + + " error message list instead."; 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..9de7bf9a 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 @@ -10,6 +10,7 @@ import lombok.experimental.SuperBuilder; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; @@ -17,6 +18,7 @@ @SuperBuilder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(AuthenticationModel.class) @XmlRootElement @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsBrandingModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsBrandingModel.java new file mode 100644 index 00000000..c8f3800e --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsBrandingModel.java @@ -0,0 +1,24 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; + +/** + * Settings group schema for products that support color-scheme branding + * (only Confluence; Crowd brands its login page with a product-specific + * model instead). + */ +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public abstract class AbstractSettingsBrandingModel extends AbstractSettingsSecurityModel { + + @XmlElement + private SettingsBrandingColorSchemeModel branding; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java new file mode 100644 index 00000000..bc46ae55 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsModel.java @@ -0,0 +1,24 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; + +/** + * Canonical settings group schema shared by all products. Extended by + * {@link AbstractSettingsSecurityModel} and {@link AbstractSettingsBrandingModel} + * for the sub-fields only some products support, so that every product's + * concrete settings model inherits its fields from the class its sub-models + * name via {@code @SubEntityOf} — field names then match by construction. + */ +@Data +@SuperBuilder +@NoArgsConstructor +public abstract class AbstractSettingsModel { + + @XmlElement + private SettingsGeneralModel general; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsSecurityModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsSecurityModel.java new file mode 100644 index 00000000..8722b480 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractSettingsSecurityModel.java @@ -0,0 +1,23 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; + +/** + * Settings group schema for products that support security settings + * (Confluence and Jira; Crowd does not). + */ +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public abstract class AbstractSettingsSecurityModel extends AbstractSettingsModel { + + @XmlElement + private SettingsSecurityModel security; + +} 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/AuthenticationSsoModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationSsoModel.java index 24e8b5b6..b9d96cfa 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationSsoModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationSsoModel.java @@ -7,12 +7,14 @@ import lombok.NoArgsConstructor; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; @Data @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(AuthenticationModel.class) @XmlRootElement(name = BootstrAPI.AUTHENTICATION + "-" + BootstrAPI.AUTHENTICATION_SSO) public class AuthenticationSsoModel { diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/AllModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java similarity index 52% rename from crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/AllModel.java rename to commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java index 94eb5694..5329276e 100644 --- a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/AllModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java @@ -1,26 +1,26 @@ -package com.deftdevs.bootstrapi.crowd.model; +package com.deftdevs.bootstrapi.commons.model; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; 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.MAIL_SERVER; @Data @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = "all") -public class AllModel { +@XmlRootElement(name = MAIL_SERVER) +public class MailServerModel { @XmlElement - private SettingsModel settings; + private MailServerSmtpModel smtp; @XmlElement - private List applications; + private MailServerPopModel pop; } diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerPopModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerPopModel.java index 82cc46c1..2a1bda69 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerPopModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerPopModel.java @@ -6,12 +6,14 @@ import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; @Data @SuperBuilder @NoArgsConstructor @EqualsAndHashCode(callSuper = true) +@SubEntityOf(MailServerModel.class) @XmlRootElement(name = BootstrAPI.MAIL_SERVER + "-" + BootstrAPI.MAIL_SERVER_POP) public class MailServerPopModel extends AbstractMailServerProtocolModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java index faf3bfe1..5f7aef5a 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerSmtpModel.java @@ -8,6 +8,7 @@ import lombok.experimental.SuperBuilder; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; @Data @@ -15,6 +16,7 @@ @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(callSuper = true) +@SubEntityOf(MailServerModel.class) @XmlRootElement(name = BootstrAPI.MAIL_SERVER + "-" + BootstrAPI.MAIL_SERVER_SMTP) public class MailServerSmtpModel extends AbstractMailServerProtocolModel { @@ -57,4 +59,14 @@ public boolean getUseTls() { .timeout(5000L) .build(); + // restricted to the fields supported by all products (e.g. Crowd does not echo name and protocol) + public static final MailServerSmtpModel EXAMPLE_2_MINIMAL = MailServerSmtpModel.builder() + .from(EXAMPLE_2.getFrom()) + .prefix(EXAMPLE_2.getPrefix()) + .host(EXAMPLE_2.getHost()) + .port(EXAMPLE_2.getPort()) + .useTls(EXAMPLE_2.getUseTls()) + .timeout(EXAMPLE_2.getTimeout()) + .build(); + } 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..115f2489 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 @@ -8,6 +8,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.*; @@ -16,6 +17,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(AbstractSettingsBrandingModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING + "-" + COLOR_SCHEME) @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 62% 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..5beecce9 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; @@ -10,16 +9,21 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; 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) +@SubEntityOf(AbstractSettingsModel.class) +@XmlRootElement(name = SETTINGS + "-" + SETTINGS_GENERAL) @XmlAccessorType(XmlAccessType.FIELD) -public class SettingsModel { +public class SettingsGeneralModel { @XmlElement private URI baseUrl; @@ -46,7 +50,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 +58,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) @@ -62,4 +66,10 @@ public String getMode() { .externalUserManagement(true) .build(); + // restricted to the fields supported by all products (e.g. Crowd) + public static final SettingsGeneralModel EXAMPLE_1_MINIMAL = SettingsGeneralModel.builder() + .title(EXAMPLE_1.getTitle()) + .baseUrl(EXAMPLE_1.getBaseUrl()) + .build(); + } diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java index 8e41cf35..14924918 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsSecurityModel.java @@ -8,6 +8,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; @@ -17,6 +18,7 @@ @SuperBuilder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(AbstractSettingsSecurityModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_SECURITY) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsSecurityModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/_AbstractAllModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/_AbstractAllModel.java new file mode 100644 index 00000000..d2fb85fd --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/_AbstractAllModel.java @@ -0,0 +1,42 @@ +package com.deftdevs.bootstrapi.commons.model; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; +import java.util.Map; + +/** + * Shared shape of the product _all models: the configuration fields every + * product supports plus the per-sub-field status map of the response. + * Products extend it with their product-specific fields. + * + * @param the product's settings model type + */ +@Data +@SuperBuilder +@NoArgsConstructor +public abstract class _AbstractAllModel implements _AllModelAccessor { + + @XmlElement + private S settings; + + @XmlElement + private Map directories; + + @XmlElement + private Map applicationLinks; + + @XmlElement + private Map licenses; + + @XmlElement + private MailServerModel mailServer; + + @XmlElement + private Map status; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SerializableFunction.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SerializableFunction.java new file mode 100644 index 00000000..c22b443b --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SerializableFunction.java @@ -0,0 +1,14 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import java.io.Serializable; +import java.util.function.Function; + +/** + * A {@link Function} that is also {@link Serializable}. Getter references + * ({@code Model::getField}) assigned to this type retain their method + * metadata at runtime, which {@link com.deftdevs.bootstrapi.commons.util.FieldNames} + * uses to derive the referenced field's serialized name. + */ +@FunctionalInterface +public interface SerializableFunction extends Function, Serializable { +} 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/SubEntityOf.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SubEntityOf.java new file mode 100644 index 00000000..7877e9c0 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/SubEntityOf.java @@ -0,0 +1,25 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares the parent model containing this model as a sub-field, so that + * status-map keys can be derived from a sub-model class alone by walking up + * the model hierarchy (e.g. {@code MailServerPopModel} → field {@code pop} + * of {@code MailServerModel}). The parent must be a commons-visible model + * declaring a field of the annotated type; product models that mirror the + * canonical parent's fields share its keys by construction. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface SubEntityOf { + + /** The parent model class declaring a field of the annotated type. */ + Class value(); + +} 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/AbstractAuthenticationResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractAuthenticationResourceImpl.java index cb4c3247..665568ef 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractAuthenticationResourceImpl.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractAuthenticationResourceImpl.java @@ -7,37 +7,36 @@ import java.util.Map; -public abstract class AbstractAuthenticationResourceImpl> - implements AuthenticationResource { +public abstract class AbstractAuthenticationResourceImpl implements AuthenticationResource { - private final S authenticationService; + private final AuthenticationService authenticationService; protected AbstractAuthenticationResourceImpl( - final S authenticationService) { + final AuthenticationService authenticationService) { this.authenticationService = authenticationService; } @Override - public Map getAuthenticationIdps() { + public Map getAuthenticationIdps() { return authenticationService.getAuthenticationIdps(); } @Override - public Map setAuthenticationIdps( - final Map authenticationIdpModels) { + public Map setAuthenticationIdps( + final Map authenticationIdpModels) { return authenticationService.setAuthenticationIdps(authenticationIdpModels); } @Override - public SM getAuthenticationSso() { + public AuthenticationSsoModel getAuthenticationSso() { return authenticationService.getAuthenticationSso(); } @Override - public SM setAuthenticationSso( - final SM authenticationSsoModel) { + public AuthenticationSsoModel setAuthenticationSso( + final AuthenticationSsoModel authenticationSsoModel) { return authenticationService.setAuthenticationSso(authenticationSsoModel); } 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..6fba678b --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java @@ -0,0 +1,60 @@ +package com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.exception.web.BadRequestException; +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> { + + private final _AllService<_AllModel> allService; + + public _AbstractAllResourceImpl( + final _AllService<_AllModel> allService) { + + this.allService = allService; + } + + @Override + public Response setAll( + final _AllModel allModel) { + + if (allModel == null) { + throw new BadRequestException("A configuration must be provided in the request body"); + } + + 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: + * 200 when the status map is empty or every sub-field succeeded, + * otherwise the highest (most severe) sub-field status code. + *

+ * Failing loud is preferred over WebDAV-style 207 Multi-Status because + * 207 belongs to the 2xx success class, so scripted callers (e.g. + * {@code curl -f}) would treat partial failure as success unless they + * inspect the body. The apply is declarative and not transactional: + * sub-fields that succeeded stay applied, re-submitting the same payload + * is safe, and the per-sub-field {@code status} map in the response body + * still distinguishes partial from total failure. + */ + static int computeOverallStatus( + final Map statusMap) { + + int overallStatus = Response.Status.OK.getStatusCode(); + if (statusMap != null) { + for (final _AllModelStatus entry : statusMap.values()) { + overallStatus = Math.max(overallStatus, entry.getStatus()); + } + } + return overallStatus; + } +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/AuthenticationResource.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/AuthenticationResource.java index a122265b..564ab83c 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/AuthenticationResource.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/AuthenticationResource.java @@ -18,7 +18,7 @@ import javax.ws.rs.core.MediaType; import java.util.Map; -public interface AuthenticationResource { +public interface AuthenticationResource { @GET @Path(BootstrAPI.AUTHENTICATION_IDPS) @@ -36,7 +36,7 @@ public interface AuthenticationResource getAuthenticationIdps(); + Map getAuthenticationIdps(); @PATCH @Path(BootstrAPI.AUTHENTICATION_IDPS) @@ -55,8 +55,8 @@ public interface AuthenticationResource setAuthenticationIdps( - final Map authenticationIdpModels); + Map setAuthenticationIdps( + final Map authenticationIdpModels); @GET @Path(BootstrAPI.AUTHENTICATION_SSO) @@ -74,7 +74,7 @@ public interface AuthenticationResource { +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..cf49f60a --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java @@ -0,0 +1,22 @@ +package com.deftdevs.bootstrapi.commons.rest.api; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; + +import javax.validation.constraints.NotNull; +import javax.ws.rs.PUT; +import javax.ws.rs.core.Response; + +public interface _AllResource<_AllModel extends _AllModelAccessor> { + + /** + * Product implementations must override this method and declare the + * {@code @Operation} annotation with their concrete {@code _AllModel} as + * the response schema. The annotation is not declared here because + * OpenAPI only reads the overriding method's annotations, so an + * interface-level copy would be shadowed and silently drift. + */ + @PUT + Response setAll( + @NotNull final _AllModel bean); + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/AbstractAuthenticationService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/AbstractAuthenticationService.java index 07409622..e9b0d32d 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/AbstractAuthenticationService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/AbstractAuthenticationService.java @@ -14,8 +14,7 @@ import java.util.function.Function; import java.util.stream.Collectors; -public abstract class AbstractAuthenticationService - implements AuthenticationService { +public abstract class AbstractAuthenticationService implements AuthenticationService { protected final IdpConfigService idpConfigService; protected final SsoConfigService ssoConfigService; @@ -29,21 +28,23 @@ protected AbstractAuthenticationService( } @Override - public Map getAuthenticationIdps() { + public Map getAuthenticationIdps() { return idpConfigService.getIdpConfigs().stream() .map(this::toAuthenticationIdpModel) - .collect(Collectors.toMap(AbstractAuthenticationIdpModel::getName, Function.identity())); + .collect(Collectors.toMap(AbstractAuthenticationIdpModel::getName, Function.identity(), (existing, replacement) -> { + throw new IllegalStateException("Duplicate name key found: " + existing.getName()); + })); } @Override - public Map setAuthenticationIdps( - final Map authenticationIdpModels) { + public Map setAuthenticationIdps( + final Map authenticationIdpModels) { - final Map resultIdpModels = new LinkedHashMap<>(); + final Map resultIdpModels = new LinkedHashMap<>(); - for (Map.Entry entry : authenticationIdpModels.entrySet()) { + for (Map.Entry entry : authenticationIdpModels.entrySet()) { final String idpName = entry.getKey(); - final IB idpModel = entry.getValue(); + final AbstractAuthenticationIdpModel idpModel = entry.getValue(); if (idpModel == null) { // declarative no-op: null model + existing entity → return as-is; @@ -59,7 +60,7 @@ protected AbstractAuthenticationService( idpModel.setName(idpName); } - final IB resultIdpModel = setAuthenticationIdp(idpModel); + final AbstractAuthenticationIdpModel resultIdpModel = setAuthenticationIdp(idpModel); resultIdpModels.put(resultIdpModel.getName(), resultIdpModel); } @@ -67,8 +68,8 @@ protected AbstractAuthenticationService( } @Override - public IB setAuthenticationIdp( - final IB authenticationIdpModel) { + public AbstractAuthenticationIdpModel setAuthenticationIdp( + final AbstractAuthenticationIdpModel authenticationIdpModel) { if (authenticationIdpModel.getName() == null || authenticationIdpModel.getName().trim().isEmpty()) { throw new BadRequestException("The name cannot be empty"); @@ -88,13 +89,13 @@ public IB setAuthenticationIdp( } @Override - public SB getAuthenticationSso() { + public AuthenticationSsoModel getAuthenticationSso() { return toAuthenticationSsoModel(ssoConfigService.getSsoConfig()); } @Override - public SB setAuthenticationSso( - final SB authenticationSsoModel) { + public AuthenticationSsoModel setAuthenticationSso( + final AuthenticationSsoModel authenticationSsoModel) { final SsoConfig existingSsoConfig = ssoConfigService.getSsoConfig(); final SsoConfig ssoConfig = toSsoConfig(authenticationSsoModel, existingSsoConfig); @@ -113,14 +114,14 @@ IdpConfig findIdpConfigByName( return idpConfigsByName.get(name); } - protected abstract IB toAuthenticationIdpModel(IdpConfig idpConfig); + protected abstract AbstractAuthenticationIdpModel toAuthenticationIdpModel(IdpConfig idpConfig); - protected abstract IdpConfig toIdpConfig(IB authenticationIdpModel); + protected abstract IdpConfig toIdpConfig(AbstractAuthenticationIdpModel authenticationIdpModel); - protected abstract IdpConfig toIdpConfig(IB authenticationIdpModel, IdpConfig existingIdpConfig); + protected abstract IdpConfig toIdpConfig(AbstractAuthenticationIdpModel authenticationIdpModel, IdpConfig existingIdpConfig); - protected abstract SB toAuthenticationSsoModel(SsoConfig ssoConfig); + protected abstract AuthenticationSsoModel toAuthenticationSsoModel(SsoConfig ssoConfig); - protected abstract SsoConfig toSsoConfig(SB authenticationSsoModel, SsoConfig existingSsoConfig); + protected abstract SsoConfig toSsoConfig(AuthenticationSsoModel authenticationSsoModel, SsoConfig existingSsoConfig); } 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..f04ddf31 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java @@ -0,0 +1,120 @@ +package com.deftdevs.bootstrapi.commons.service; + +import com.deftdevs.bootstrapi.commons.model.type.SerializableFunction; +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 com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.commons.util.FieldNames; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Base class for the product _all services. Status-map keys mirror the JSON + * structure of the request and are derived from the model classes via + * {@link FieldNames}: top-level fields are recorded under their field name + * ({@code directories}, {@code permissionsGlobal}), and the sub-field + * statuses returned by composite services are prefixed with the composite's + * field name ({@code settings/general}, {@code mailServer/smtp}). + */ +public abstract class _AbstractAllServiceImpl<_AllModel extends _AllModelAccessor> implements _AllService<_AllModel> { + + private final Class allModelClass = resolveAllModelClass(); + + /** + * Resolves the concrete _AllModel class from the subclass's declaration + * ({@code class _AllServiceImpl extends _AbstractAllServiceImpl<_AllModel>}), + * where the type argument is reified despite erasure. + */ + private Class resolveAllModelClass() { + Class subclass = getClass(); + while (subclass.getSuperclass() != _AbstractAllServiceImpl.class) { + subclass = subclass.getSuperclass(); + } + + final Type superclass = subclass.getGenericSuperclass(); + if (superclass instanceof ParameterizedType) { + final Type typeArgument = ((ParameterizedType) superclass).getActualTypeArguments()[0]; + if (typeArgument instanceof Class) { + return (Class) typeArgument; + } + } + throw new IllegalStateException(getClass().getName() + + " must bind the _AllModel type parameter to a concrete model class"); + } + + protected void setEntityWithStatus( + final Class fieldType, + final I input, + final Function> updateFunction, + final Consumer resultConsumer, + final Map statusMap) { + + if (input == null) { + return; + } + + final String field = FieldNames.of(allModelClass, fieldType); + try { + final ServiceResult serviceResult = updateFunction.apply(input); + resultConsumer.accept(serviceResult.getModel()); + serviceResult.getStatus().forEach( + (subField, status) -> statusMap.put(field + "/" + subField, status)); + } 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), attribute the failure to + // the composite's field as a whole. + statusMap.put(field, ServiceResultUtil.toErrorStatus(field, e)); + } + } + + protected void setEntity( + final Class fieldType, + final I input, + final Function updateFunction, + final Consumer resultConsumer, + final Map statusMap) { + + ServiceResultUtil.setEntity(statusMap, FieldNames.of(allModelClass, fieldType), + input, updateFunction, resultConsumer); + } + + /** + * Variant for fields whose type is not a model class (e.g. a list of + * strings): the status key and the input are derived from the getter + * reference itself instead of a type-based field lookup. + */ + protected void setEntity( + final SerializableFunction<_AllModel, I> getter, + final _AllModel allModel, + final Function updateFunction, + final Consumer resultConsumer, + final Map statusMap) { + + ServiceResultUtil.setEntity(statusMap, FieldNames.of(getter), + getter.apply(allModel), updateFunction, resultConsumer); + } + + protected void setEntities( + final Class entityType, + final Map entityMap, + final Function, Map> updateFunction, + final Consumer> resultConsumer, + final Map statusMap) { + + if (entityMap == null || entityMap.isEmpty()) { + return; + } + + ServiceResultUtil.setEntity(statusMap, FieldNames.of(allModelClass, entityType), + entityMap, entities -> new LinkedHashMap<>(updateFunction.apply(entities)), resultConsumer); + } + +} 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..0bb391cb 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,23 +1,46 @@ 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.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import java.util.LinkedHashMap; import java.util.Map; -public interface AuthenticationService { +public interface AuthenticationService { - Map getAuthenticationIdps(); + default AuthenticationModel getAuthentication() { + return new AuthenticationModel( + new LinkedHashMap<>(getAuthenticationIdps()), + getAuthenticationSso()); + } - Map setAuthenticationIdps( - Map authenticationIdpModels); + default ServiceResult setAuthentication(final AuthenticationModel authenticationModel) { + final AuthenticationModel result = new AuthenticationModel(); + final Map status = new LinkedHashMap<>(); - IB setAuthenticationIdp( - IB authenticationIdpModel); + ServiceResultUtil.setSubEntity(status, AbstractAuthenticationIdpModel.class, authenticationModel.getIdps(), + idps -> new LinkedHashMap<>(setAuthenticationIdps(idps)), result::setIdps); + ServiceResultUtil.setSubEntity(status, AuthenticationSsoModel.class, authenticationModel.getSso(), + this::setAuthenticationSso, result::setSso); - SB getAuthenticationSso(); + return new ServiceResult<>(result, status); + } - SB setAuthenticationSso( - SB authenticationSsoModel); + Map getAuthenticationIdps(); + + Map setAuthenticationIdps( + Map authenticationIdpModels); + + AbstractAuthenticationIdpModel setAuthenticationIdp( + AbstractAuthenticationIdpModel authenticationIdpModel); + + AuthenticationSsoModel getAuthenticationSso(); + + AuthenticationSsoModel setAuthenticationSso( + AuthenticationSsoModel authenticationSsoModel); } 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..00c2d9a8 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 @@ -1,8 +1,11 @@ package com.deftdevs.bootstrapi.commons.service.api; import com.deftdevs.bootstrapi.commons.model.LicenseModel; +import com.deftdevs.bootstrapi.commons.util.LicenseKeyRedactor; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public interface LicensesService { @@ -21,6 +24,26 @@ 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} + */ + default Map setLicenses( + final Map licenseInputs) { + + final Map result = new LinkedHashMap<>(); + for (final String licenseKey : licenseInputs.keySet()) { + result.put(LicenseKeyRedactor.redact(licenseKey), addLicense(licenseKey)); + } + return result; + } + /** * 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..c05e0648 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,33 @@ 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.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +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<>(); + + ServiceResultUtil.setSubEntity(status, MailServerSmtpModel.class, mailServerModel.getSmtp(), + this::setMailServerSmtp, result::setSmtp); + ServiceResultUtil.setSubEntity(status, MailServerPopModel.class, mailServerModel.getPop(), + this::setMailServerPop, result::setPop); + + 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/FieldNames.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java new file mode 100644 index 00000000..b195ceec --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/FieldNames.java @@ -0,0 +1,281 @@ +package com.deftdevs.bootstrapi.commons.util; + +import com.deftdevs.bootstrapi.commons.model.type.SerializableFunction; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; + +import javax.xml.bind.annotation.XmlElement; +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Derives serialized field names from the model classes themselves, so + * hierarchies expressed by the models (a field of a given type, a + * {@link SubEntityOf} declaration) never have to be restated as string + * constants. The status-map keys of the _all endpoint are built from these + * names and therefore always match the JSON structure of the request. + *

+ * A field matches when its (type-variable-resolved) declared type is the + * requested type, or when it is a {@link Map} whose value type (or a + * {@link Collection} whose element type) is the requested type. Inherited + * fields are included. Every lookup fails fast when no field or more than + * one field matches, so any ambiguity introduced by a future model change is + * caught by the unit tests of the code deriving names from it. + */ +public final class FieldNames { + + private static final Map CACHE = new ConcurrentHashMap<>(); + + private FieldNames() { + } + + /** + * Returns the serialized name of the field holding values of + * {@code subEntityClass} within its parent model, which the sub-model + * itself declares via {@link SubEntityOf} + * (e.g. {@code MailServerPopModel} → {@code pop}). + */ + public static String of(final Class subEntityClass) { + final SubEntityOf subEntityOf = subEntityClass.getAnnotation(SubEntityOf.class); + if (subEntityOf == null) { + throw new IllegalArgumentException( + subEntityClass.getName() + " must be annotated with @SubEntityOf"); + } + return of(subEntityOf.value(), subEntityClass); + } + + /** + * Returns the serialized name of the (unique) field of {@code modelClass} + * (including inherited fields) that holds values of {@code fieldType}, + * honoring an explicit {@code @XmlElement(name = ...)} override. + */ + public static String of(final Class modelClass, final Class fieldType) { + return CACHE.computeIfAbsent(modelClass.getName() + "#" + fieldType.getName(), + key -> resolve(modelClass, fieldType)); + } + + /** + * Returns the full slash-separated field path from {@code rootClass} to + * the (unique) field holding values of {@code leafType}, walking the + * model hierarchy (e.g. a product {@code _AllModel} + + * {@code SettingsGeneralModel} → {@code settings/general}, + + * {@code MailServerModel} → {@code mailServer}). + */ + /** + * Returns the serialized name of the field read by the given getter + * reference (e.g. {@code _AllModel::getTrustedProxies} → + * {@code trustedProxies}), honoring an explicit + * {@code @XmlElement(name = ...)} override. Use this for fields whose + * type is not a model class (e.g. a list of strings), where a type-based + * lookup would couple the name to "the only field of that type" and + * break as soon as another field of the same type is added. + */ + public static String of(final SerializableFunction getter) { + final SerializedLambda lambda = serializedLambda(getter); + final String className = lambda.getImplClass().replace('/', '.'); + return CACHE.computeIfAbsent(className + "::" + lambda.getImplMethodName(), + key -> resolveGetter(getter, lambda, className)); + } + + private static String resolveGetter(final SerializableFunction getter, + final SerializedLambda lambda, final String className) { + + final String methodName = lambda.getImplMethodName(); + final String fieldName; + if (methodName.startsWith("get") && methodName.length() > 3) { + fieldName = decapitalize(methodName.substring(3)); + } else if (methodName.startsWith("is") && methodName.length() > 2) { + fieldName = decapitalize(methodName.substring(2)); + } else { + throw new IllegalArgumentException( + "Not a getter reference: " + className + "::" + methodName); + } + + final Class modelClass; + try { + modelClass = Class.forName(className, false, getter.getClass().getClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot load the getter's declaring class", e); + } + + for (final Field field : declaredAndInheritedFields(modelClass)) { + if (field.getName().equals(fieldName)) { + return serializedName(field); + } + } + throw new IllegalArgumentException(String.format( + "%s has no field backing %s()", className, methodName)); + } + + /** + * A getter reference assigned to a {@link SerializableFunction} carries + * its method metadata in the {@link SerializedLambda} returned by the + * compiler-generated {@code writeReplace} method. + */ + private static SerializedLambda serializedLambda(final SerializableFunction getter) { + try { + final Method writeReplace = getter.getClass().getDeclaredMethod("writeReplace"); + writeReplace.setAccessible(true); + return (SerializedLambda) writeReplace.invoke(getter); + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException("Cannot introspect the getter reference", e); + } + } + + private static String decapitalize(final String name) { + return Character.toLowerCase(name.charAt(0)) + name.substring(1); + } + + public static String pathOf(final Class rootClass, final Class leafType) { + return CACHE.computeIfAbsent(rootClass.getName() + "->" + leafType.getName(), + key -> resolvePath(rootClass, leafType)); + } + + private static String resolvePath(final Class rootClass, final Class leafType) { + final java.util.List paths = candidatePaths(rootClass, leafType, new java.util.HashSet<>()); + if (paths.isEmpty()) { + throw new IllegalArgumentException(String.format( + "%s has no field path to %s", rootClass.getName(), leafType.getName())); + } + if (paths.size() > 1) { + throw new IllegalArgumentException(String.format( + "%s has more than one field path to %s: %s", + rootClass.getName(), leafType.getName(), String.join(", ", paths))); + } + return paths.get(0); + } + + private static java.util.List candidatePaths(final Class rootClass, final Class leafType, + final java.util.Set> visited) { + + final java.util.List paths = new java.util.ArrayList<>(); + if (!visited.add(rootClass)) { + return paths; + } + + final Map, Type> bindings = typeVariableBindings(rootClass); + for (final Field field : declaredAndInheritedFields(rootClass)) { + if (matches(field, leafType, bindings)) { + paths.add(serializedName(field)); + continue; + } + + final Type resolvedType = resolveType(field.getGenericType(), bindings); + if (resolvedType instanceof Class + && isModel((Class) resolvedType) + && !((Class) resolvedType).isEnum()) { + for (final String subPath : candidatePaths((Class) resolvedType, leafType, visited)) { + paths.add(serializedName(field) + "/" + subPath); + } + } + } + + visited.remove(rootClass); + return paths; + } + + private static String resolve(final Class modelClass, final Class fieldType) { + Field match = null; + + for (final Field field : declaredAndInheritedFields(modelClass)) { + if (!matches(field, fieldType, typeVariableBindings(modelClass))) { + continue; + } + if (match != null) { + throw new IllegalArgumentException(String.format( + "%s has more than one field holding %s: %s, %s", + modelClass.getName(), fieldType.getName(), match.getName(), field.getName())); + } + match = field; + } + + if (match == null) { + throw new IllegalArgumentException(String.format( + "%s has no field holding %s", modelClass.getName(), fieldType.getName())); + } + return serializedName(match); + } + + private static java.util.List declaredAndInheritedFields(final Class modelClass) { + final java.util.List fields = new java.util.ArrayList<>(); + for (Class current = modelClass; current != null && current != Object.class; + current = current.getSuperclass()) { + for (final Field field : current.getDeclaredFields()) { + if (!field.isSynthetic() && !Modifier.isStatic(field.getModifiers())) { + fields.add(field); + } + } + } + return fields; + } + + /** Maps the type variables of superclasses to the concrete types bound by {@code modelClass}. */ + private static Map, Type> typeVariableBindings(final Class modelClass) { + final Map, Type> bindings = new HashMap<>(); + for (Class current = modelClass; current != null && current != Object.class; + current = current.getSuperclass()) { + final Type genericSuperclass = current.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + final ParameterizedType parameterized = (ParameterizedType) genericSuperclass; + final TypeVariable[] variables = current.getSuperclass().getTypeParameters(); + final Type[] arguments = parameterized.getActualTypeArguments(); + for (int i = 0; i < variables.length; i++) { + bindings.put(variables[i], resolveType(arguments[i], bindings)); + } + } + } + return bindings; + } + + private static Type resolveType(final Type type, final Map, Type> bindings) { + if (type instanceof TypeVariable) { + return bindings.getOrDefault(type, type); + } + return type; + } + + private static boolean matches(final Field field, final Class fieldType, + final Map, Type> bindings) { + + final Type resolvedType = resolveType(field.getGenericType(), bindings); + if (resolvedType.equals(fieldType)) { + return true; + } + + if (!(resolvedType instanceof ParameterizedType)) { + return false; + } + + final ParameterizedType parameterized = (ParameterizedType) resolvedType; + final Type[] typeArguments = parameterized.getActualTypeArguments(); + if (Map.class.isAssignableFrom(field.getType()) && typeArguments.length == 2) { + return resolveType(typeArguments[1], bindings).equals(fieldType); + } + if (Collection.class.isAssignableFrom(field.getType()) && typeArguments.length == 1) { + return resolveType(typeArguments[0], bindings).equals(fieldType); + } + return false; + } + + private static String serializedName(final Field field) { + final XmlElement xmlElement = field.getAnnotation(XmlElement.class); + if (xmlElement != null && !"##default".equals(xmlElement.name())) { + return xmlElement.name(); + } + return field.getName(); + } + + private static boolean isModel(final Class type) { + return type.getPackage() != null + && type.getPackage().getName().startsWith("com.deftdevs.bootstrapi"); + } + +} 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..f5c4f76f --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtil.java @@ -0,0 +1,85 @@ +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 java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +public class ServiceResultUtil { + + private static final Logger log = LoggerFactory.getLogger(ServiceResultUtil.class); + + /** + * Applies one sub-field of a composite update and records its outcome in + * the status map: a success entry when the update function returns, or an + * error entry (via {@link #toErrorStatus(String, Exception)}) when it + * throws. Does nothing when {@code input} is null, i.e. when the field was + * not part of the request. + *

+ * The status key is derived from the models via + * {@link FieldNames#of(Class)} from the sub-model's {@link com.deftdevs.bootstrapi.commons.model.type.SubEntityOf} declaration: the sub-field's name within the + * composite's model, so that status keys line up with the JSON structure + * of the request. Composites record their sub-fields under local keys + * ({@code general}, {@code smtp}); the _all endpoint prefixes them with + * the composite's own field name when merging ({@code settings/general}, + * {@code mailServer/smtp}). + */ + public static void setSubEntity( + final Map statusMap, + final Class subEntityClass, + final T input, + final UnaryOperator updateFunction, + final Consumer resultConsumer) { + + setEntity(statusMap, FieldNames.of(subEntityClass), input, updateFunction, resultConsumer); + } + + /** + * Applies one field of a composite update and records its outcome under + * the given status key: a success entry when the update function returns, + * or an error entry (via {@link #toErrorStatus(String, Exception)}) when + * it throws. Does nothing when {@code input} is null, i.e. when the field + * was not part of the request. + */ + public static void setEntity( + final Map statusMap, + final String key, + final I input, + final Function updateFunction, + final Consumer resultConsumer) { + + if (input == null) { + return; + } + + try { + resultConsumer.accept(updateFunction.apply(input)); + statusMap.put(key, _AllModelStatus.success()); + } catch (Exception e) { + statusMap.put(key, toErrorStatus(key, e)); + } + } + + 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/AuthenticationResourceTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/AuthenticationResourceTest.java index cd091657..ad27b9e3 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/AuthenticationResourceTest.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/AuthenticationResourceTest.java @@ -21,7 +21,7 @@ class AuthenticationResourceTest { @Mock - private AuthenticationService authenticationService; + private AuthenticationService authenticationService; private TestAuthenticationResourceImpl resource; 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..416cef67 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImplTest.java @@ -0,0 +1,81 @@ +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 mixedOutcomesYieldTheHighestCode() { + 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 allSameServerErrorBubblesThat() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "x", null)); + map.put("b", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "y", null)); + assertEquals(500, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void mixedSuccessAndServerErrorYields500() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.success()); + map.put("b", _AllModelStatus.error(Response.Status.INTERNAL_SERVER_ERROR, "x", null)); + assertEquals(500, _AbstractAllResourceImpl.computeOverallStatus(map)); + } + + @Test + void mixedSuccessAndClientErrorYields400() { + final Map map = new LinkedHashMap<>(); + map.put("a", _AllModelStatus.success()); + map.put("b", _AllModelStatus.error(Response.Status.BAD_REQUEST, "x", null)); + assertEquals(400, _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 differentClientErrorsYieldTheHighestCode() { + 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(409, _AbstractAllResourceImpl.computeOverallStatus(map)); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestAuthenticationResourceImpl.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestAuthenticationResourceImpl.java index 3fdb67d0..fd5d56da 100644 --- a/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestAuthenticationResourceImpl.java +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/rest/impl/TestAuthenticationResourceImpl.java @@ -5,10 +5,10 @@ import com.deftdevs.bootstrapi.commons.rest.AbstractAuthenticationResourceImpl; import com.deftdevs.bootstrapi.commons.service.api.AuthenticationService; -public class TestAuthenticationResourceImpl extends AbstractAuthenticationResourceImpl> { +public class TestAuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { public TestAuthenticationResourceImpl( - AuthenticationService authenticationService) { + AuthenticationService authenticationService) { super(authenticationService); } 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..7c8c6d81 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImplTest.java @@ -0,0 +1,206 @@ +package com.deftdevs.bootstrapi.commons.service; + +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.assertTrue; + +class _AbstractAllServiceImplTest { + + private static class TestGroupModel { + } + + private static class TestEntityModel { + } + + private static class TestEntryModel { + } + + @SuppressWarnings("unused") + private static class TestAllModel implements _AllModelAccessor { + + private TestGroupModel group; + private TestEntityModel entity; + private Map entries; + private Map status; + + @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<>(); + + service.setEntity(TestEntityModel.class, null, + input -> input, result -> {}, statusMap); + + assertTrue(statusMap.isEmpty()); + } + + @Test + void setEntitySuccessRecordsStatusAndResultUnderTheFieldName() { + final Map statusMap = new HashMap<>(); + final AtomicReference result = new AtomicReference<>(); + final TestEntityModel updated = new TestEntityModel(); + + service.setEntity(TestEntityModel.class, new TestEntityModel(), + input -> updated, result::set, statusMap); + + assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("entity").getStatus()); + assertEquals(updated, result.get()); + } + + @Test + void setEntityUsesWebApplicationExceptionStatusCode() { + final Map statusMap = new HashMap<>(); + + service.setEntity(TestEntityModel.class, new TestEntityModel(), + input -> { + throw new WebApplicationException("conflict", Response.Status.CONFLICT); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.CONFLICT.getStatusCode(), statusMap.get("entity").getStatus()); + assertEquals("Failed to apply entity configuration", statusMap.get("entity").getMessage()); + } + + @Test + void setEntityMapsUnexpectedExceptionTo500WithoutDetails() { + final Map statusMap = new HashMap<>(); + + service.setEntity(TestEntityModel.class, new TestEntityModel(), + input -> { + throw new IllegalStateException("internal details that must not leak"); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), statusMap.get("entity").getStatus()); + assertNull(statusMap.get("entity").getDetails()); + } + + // setEntities + + @Test + void setEntitiesSkipsNullAndEmptyMap() { + final Map statusMap = new HashMap<>(); + + service.setEntities(TestEntryModel.class, null, + entities -> entities, result -> {}, statusMap); + service.setEntities(TestEntryModel.class, Collections.emptyMap(), + entities -> entities, result -> {}, statusMap); + assertTrue(statusMap.isEmpty()); + } + + @Test + void setEntitiesSuccessRecordsStatusAndResultUnderTheFieldName() { + final Map statusMap = new HashMap<>(); + final AtomicReference> result = new AtomicReference<>(); + final Map entities = Collections.singletonMap("key", new TestEntryModel()); + + service.setEntities(TestEntryModel.class, entities, + input -> input, result::set, statusMap); + + assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("entries").getStatus()); + assertEquals(entities, result.get()); + } + + @Test + void setEntitiesRecordsErrorStatusOnException() { + final Map statusMap = new HashMap<>(); + final Map entities = Collections.singletonMap("key", new TestEntryModel()); + + service.setEntities(TestEntryModel.class, entities, + input -> { + throw new WebApplicationException(Response.Status.BAD_REQUEST); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), statusMap.get("entries").getStatus()); + } + + // setEntityWithStatus + + @Test + void setEntityWithStatusSkipsNullInput() { + final Map statusMap = new HashMap<>(); + + service.setEntityWithStatus(TestGroupModel.class, null, + input -> new ServiceResult<>(input, Collections.emptyMap()), + result -> {}, statusMap); + + assertTrue(statusMap.isEmpty()); + } + + @Test + void setEntityWithStatusPrefixesSubFieldStatusesWithTheFieldName() { + final Map statusMap = new HashMap<>(); + final AtomicReference result = new AtomicReference<>(); + final TestGroupModel updated = new TestGroupModel(); + + final Map subStatus = new LinkedHashMap<>(); + subStatus.put("first", _AllModelStatus.success()); + subStatus.put("second", _AllModelStatus.error(Response.Status.BAD_REQUEST, "bad", null)); + + service.setEntityWithStatus(TestGroupModel.class, new TestGroupModel(), + input -> new ServiceResult<>(updated, subStatus), + result::set, statusMap); + + assertEquals(updated, result.get()); + assertEquals(2, statusMap.size()); + assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("group/first").getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), statusMap.get("group/second").getStatus()); + } + + @Test + void setEntityWithStatusFallsBackToTheFieldNameOnThrow() { + final Map statusMap = new HashMap<>(); + + service.setEntityWithStatus(TestGroupModel.class, new TestGroupModel(), + input -> { + throw new IllegalStateException("boom"); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), statusMap.get("group").getStatus()); + } + + @Test + void setEntityWithStatusUsesWebApplicationExceptionStatusCodeOnThrow() { + final Map statusMap = new HashMap<>(); + + service.setEntityWithStatus(TestGroupModel.class, new TestGroupModel(), + input -> { + throw new WebApplicationException(Response.Status.CONFLICT); + }, + result -> {}, statusMap); + + assertEquals(Response.Status.CONFLICT.getStatusCode(), statusMap.get("group").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..0ffcd32b --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationServiceTest.java @@ -0,0 +1,89 @@ +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 com.deftdevs.bootstrapi.commons.util.FieldNames; +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(FieldNames.of(AuthenticationModel.class, AbstractAuthenticationIdpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(AuthenticationModel.class, AuthenticationSsoModel.class)).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(FieldNames.of(AuthenticationModel.class, AbstractAuthenticationIdpModel.class)).getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get(FieldNames.of(AuthenticationModel.class, AuthenticationSsoModel.class)).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..6df75245 --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/service/api/MailServerServiceTest.java @@ -0,0 +1,81 @@ +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 com.deftdevs.bootstrapi.commons.util.FieldNames; +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(FieldNames.of(MailServerModel.class, MailServerSmtpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(MailServerModel.class, MailServerPopModel.class)).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(FieldNames.of(MailServerModel.class, MailServerSmtpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(MailServerModel.class, MailServerPopModel.class)).getStatus()); + } +} diff --git a/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java new file mode 100644 index 00000000..38593edf --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/FieldNamesTest.java @@ -0,0 +1,118 @@ +package com.deftdevs.bootstrapi.commons.util; + +import org.junit.jupiter.api.Test; + +import javax.xml.bind.annotation.XmlElement; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FieldNamesTest { + + private static class LeafModel { + } + + private static class OtherLeafModel { + } + + private static class GroupModel { + private LeafModel leaf; + private Map others; + } + + private static class ThirdLeafModel { + } + + private static class RootModel { + private GroupModel group; + private Map directLeaves; + private List names; + + @XmlElement(name = "renamed") + private ThirdLeafModel other; + + public List getNames() { + return names; + } + + public ThirdLeafModel getOther() { + return other; + } + + public RootModel getSelf() { + return this; + } + } + + private static class AmbiguousModel { + private LeafModel first; + private LeafModel second; + } + + @Test + void ofResolvesDirectField() { + assertEquals("leaf", FieldNames.of(GroupModel.class, LeafModel.class)); + } + + @Test + void ofResolvesMapValueField() { + assertEquals("others", FieldNames.of(GroupModel.class, OtherLeafModel.class)); + } + + @Test + void ofResolvesCollectionElementField() { + assertEquals("names", FieldNames.of(RootModel.class, String.class)); + } + + @Test + void ofHonorsXmlElementName() { + assertEquals("renamed", FieldNames.of(RootModel.class, ThirdLeafModel.class)); + } + + @Test + void ofRejectsAmbiguousFields() { + assertThrows(IllegalArgumentException.class, + () -> FieldNames.of(AmbiguousModel.class, LeafModel.class)); + } + + @Test + void ofRejectsUnknownFieldType() { + assertThrows(IllegalArgumentException.class, + () -> FieldNames.of(GroupModel.class, RootModel.class)); + } + + @Test + void ofGetterResolvesTheBackingField() { + assertEquals("names", FieldNames.of(RootModel::getNames)); + } + + @Test + void ofGetterHonorsXmlElementName() { + assertEquals("renamed", FieldNames.of(RootModel::getOther)); + } + + @Test + void ofGetterRejectsMethodsWithoutBackingField() { + assertThrows(IllegalArgumentException.class, + () -> FieldNames.of(RootModel::getSelf)); + } + + @Test + void pathOfResolvesTopLevelField() { + assertEquals("group", FieldNames.pathOf(RootModel.class, GroupModel.class)); + } + + @Test + void pathOfWalksIntoNestedModels() { + assertEquals("group/others", FieldNames.pathOf(RootModel.class, OtherLeafModel.class)); + } + + @Test + void pathOfRejectsAmbiguousPaths() { + // LeafModel is reachable both as root.directLeaves and root.group.leaf + assertThrows(IllegalArgumentException.class, + () -> FieldNames.pathOf(RootModel.class, LeafModel.class)); + } +} 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..0ad3589c --- /dev/null +++ b/commons/src/test/java/com/deftdevs/bootstrapi/commons/util/ServiceResultUtilTest.java @@ -0,0 +1,90 @@ +package com.deftdevs.bootstrapi.commons.util; + +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; +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.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.assertTrue; + +class ServiceResultUtilTest { + + @SuppressWarnings("unused") + private static class GroupModel { + private GeneralModel general; + } + + @SubEntityOf(GroupModel.class) + private static class GeneralModel { + } + + @Test + void setSubEntitySkipsNullInput() { + final Map statusMap = new LinkedHashMap<>(); + + ServiceResultUtil.setSubEntity(statusMap, GeneralModel.class, null, + input -> input, result -> {}); + + assertTrue(statusMap.isEmpty()); + } + + @Test + void setSubEntityRecordsSuccessAndResultUnderTheFieldName() { + final Map statusMap = new LinkedHashMap<>(); + final AtomicReference result = new AtomicReference<>(); + final GeneralModel updated = new GeneralModel(); + + ServiceResultUtil.setSubEntity(statusMap, GeneralModel.class, new GeneralModel(), + input -> updated, result::set); + + assertEquals(updated, result.get()); + assertEquals(Response.Status.OK.getStatusCode(), statusMap.get("general").getStatus()); + } + + @Test + void setSubEntityRecordsErrorStatusOnException() { + final Map statusMap = new LinkedHashMap<>(); + final AtomicReference result = new AtomicReference<>(); + + ServiceResultUtil.setSubEntity(statusMap, GeneralModel.class, new GeneralModel(), + input -> { + throw new WebApplicationException("bad", Response.Status.BAD_REQUEST); + }, + result::set); + + assertNull(result.get()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), statusMap.get("general").getStatus()); + } + + @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/_AbstractAllResourceFuncTest.java b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceFuncTest.java new file mode 100644 index 00000000..b854da11 --- /dev/null +++ b/commons/src/test/java/it/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceFuncTest.java @@ -0,0 +1,136 @@ +package it.com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +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 _AbstractAllResourceFuncTest { + + /** The _all endpoint is the REST root itself. */ + private static final String ALL_PATH = ""; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void testSetAllAppliesGeneralSettings() throws Exception { + final String settingsGeneralKey = getSettingsGeneralStatusKey(); + + 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(settingsGeneralKey); + assertFalse(generalStatusNode.isMissingNode(), + "expected a '" + settingsGeneralKey + "' entry in the status map, got: " + allNode.path("status")); + assertEquals(Response.Status.OK.getStatusCode(), generalStatusNode.path("status").asInt()); + + // the status key mirrors the request's field path, so it also locates the model in the body + JsonNode generalNode = allNode; + for (final String segment : settingsGeneralKey.split("/")) { + generalNode = generalNode.path(segment); + } + assertEquals(getExampleSettingsGeneralModel().getTitle(), generalNode.path("title").asText()); + } + + @Test + void testSetAllWithoutBodyIsBadRequest() throws Exception { + final HttpResponse allResponse = HttpRequestHelper.builder(ALL_PATH) + .request(HttpMethod.PUT, null); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), allResponse.statusCode()); + } + + @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(); + + /** + * The status-map key of the general settings, derived from the product's + * models (e.g. {@code FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)}). + */ + protected abstract String getSettingsGeneralStatusKey(); + + 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..8e44878b --- /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, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 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'). If any sub-field fails, the highest sub-field status code is returned with the same response body. + +### 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..82c41779 --- /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] | +| **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | +| **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | +| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | +| **authentication** | [**AuthenticationModel**](AuthenticationModel.md) | | [optional] [default to null] | +| **permissionsGlobal** | [**PermissionsGlobalModel**](PermissionsGlobalModel.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..7bb8765e 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,14 @@ public ConfluenceAuthenticationService confluenceAuthenticationService() { @Bean public ConfluenceSettingsService confluenceSettingsService() { + // 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 SettingsServiceImpl( - atlassianConfig.globalSettingsManager()); + atlassianConfig.globalSettingsManager(), + new SettingsBrandingServiceImpl( + atlassianConfig.colourSchemeManager(), + atlassianConfig.siteLogoManager())); } @Bean @@ -70,13 +89,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/SettingsCustomHtmlModel.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsCustomHtmlModel.java index 8c9f2f99..cced9a2e 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsCustomHtmlModel.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsCustomHtmlModel.java @@ -8,6 +8,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.*; @@ -16,6 +17,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(SettingsModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_CUSTOM_HTML) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsCustomHtmlModel { 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..335c386a --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/SettingsModel.java @@ -0,0 +1,24 @@ +package com.deftdevs.bootstrapi.confluence.model; + +import com.deftdevs.bootstrapi.commons.model.AbstractSettingsBrandingModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@XmlRootElement(name = SETTINGS) +public class SettingsModel extends AbstractSettingsBrandingModel { + + @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..eecc2e79 --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/model/_AllModel.java @@ -0,0 +1,28 @@ +package com.deftdevs.bootstrapi.confluence.model; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model._AbstractAllModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@XmlRootElement(name = BootstrAPI._ALL) +public class _AllModel extends _AbstractAllModel { + + @XmlElement + private AuthenticationModel authentication; + + @XmlElement + private PermissionsGlobalModel permissionsGlobal; + +} diff --git a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/AuthenticationResourceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/AuthenticationResourceImpl.java index 25da5d2d..f012982a 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/AuthenticationResourceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/AuthenticationResourceImpl.java @@ -2,8 +2,6 @@ import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; -import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.rest.AbstractAuthenticationResourceImpl; import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; @@ -12,7 +10,7 @@ @Path(BootstrAPI.AUTHENTICATION) @SystemAdminOnly -public class AuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { +public class AuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { @Inject public AuthenticationResourceImpl( 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..73aa980a --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceImpl.java @@ -0,0 +1,68 @@ +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 = "4XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "5XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_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/AuthenticationServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/AuthenticationServiceImpl.java index 66462a14..7279d6f5 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/AuthenticationServiceImpl.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/AuthenticationServiceImpl.java @@ -12,7 +12,7 @@ import com.deftdevs.bootstrapi.confluence.service.api.ConfluenceAuthenticationService; public class AuthenticationServiceImpl - extends AbstractAuthenticationService + extends AbstractAuthenticationService implements ConfluenceAuthenticationService { public AuthenticationServiceImpl( 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..63b6ffc5 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,28 +3,34 @@ 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.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; import java.net.URI; public class SettingsServiceImpl implements ConfluenceSettingsService { private final GlobalSettingsManager globalSettingsManager; + private final SettingsBrandingService settingsBrandingService; public SettingsServiceImpl( - final GlobalSettingsManager globalSettingsManager) { + final GlobalSettingsManager globalSettingsManager, + final SettingsBrandingService settingsBrandingService) { this.globalSettingsManager = globalSettingsManager; + this.settingsBrandingService = settingsBrandingService; } @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()) @@ -33,7 +39,7 @@ public SettingsModel getSettingsGeneral() { } @Override - public SettingsModel setSettingsGeneral(SettingsModel settingsModel) { + public SettingsGeneralModel setSettingsGeneral(SettingsGeneralModel settingsModel) { final Settings settings = globalSettingsManager.getGlobalSettings(); if (settingsModel.getBaseUrl() != null) { @@ -116,7 +122,38 @@ public SettingsSecurityModel setSettingsSecurity( settings.setWebSudoTimeout(settingsSecurityModel.getWebSudoTimeout()); } + globalSettingsManager.updateGlobalSettings(settings); + return getSettingsSecurity(); } + @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/_AllServiceImpl.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java new file mode 100644 index 00000000..3e13e66b --- /dev/null +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImpl.java @@ -0,0 +1,84 @@ +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.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.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 java.util.LinkedHashMap; +import java.util.Map; + + +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 LinkedHashMap<>(); + + setEntityWithStatus(SettingsModel.class, allModel.getSettings(), + settingsService::setSettings, result::setSettings, statusMap); + + setEntities(AbstractDirectoryModel.class, allModel.getDirectories(), + directoriesService::setDirectories, result::setDirectories, statusMap); + + setEntities(ApplicationLinkModel.class, allModel.getApplicationLinks(), + applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); + + setEntityWithStatus(AuthenticationModel.class, allModel.getAuthentication(), + authenticationService::setAuthentication, result::setAuthentication, statusMap); + + setEntities(LicenseModel.class, allModel.getLicenses(), + licensesService::setLicenses, result::setLicenses, statusMap); + + setEntityWithStatus(MailServerModel.class, allModel.getMailServer(), + mailServerService::setMailServer, result::setMailServer, statusMap); + + setEntity(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/ConfluenceAuthenticationService.java b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceAuthenticationService.java index c548b34e..66771481 100644 --- a/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceAuthenticationService.java +++ b/confluence/src/main/java/com/deftdevs/bootstrapi/confluence/service/api/ConfluenceAuthenticationService.java @@ -1,10 +1,8 @@ package com.deftdevs.bootstrapi.confluence.service.api; -import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; -import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.service.api.AuthenticationService; public interface ConfluenceAuthenticationService extends - AuthenticationService { + AuthenticationService { } 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..dccb109c 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,53 @@ 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.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +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 SettingsModel.builder() + .general(getSettingsGeneral()) + .security(getSettingsSecurity()) + .branding(getColourScheme()) + .customHtml(getCustomHtml()) + .build(); + } + + default ServiceResult setSettings(final SettingsModel settingsModel) { + final SettingsModel result = new SettingsModel(); + final Map status = new LinkedHashMap<>(); + + ServiceResultUtil.setSubEntity(status, SettingsGeneralModel.class, settingsModel.getGeneral(), + this::setSettingsGeneral, result::setGeneral); + ServiceResultUtil.setSubEntity(status, SettingsSecurityModel.class, settingsModel.getSecurity(), + this::setSettingsSecurity, result::setSecurity); + ServiceResultUtil.setSubEntity(status, SettingsBrandingColorSchemeModel.class, settingsModel.getBranding(), + this::setColourScheme, result::setBranding); + ServiceResultUtil.setSubEntity(status, SettingsCustomHtmlModel.class, settingsModel.getCustomHtml(), + this::setCustomHtml, result::setCustomHtml); + + 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..0e533a6e --- /dev/null +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/rest/_AllResourceTest.java @@ -0,0 +1,91 @@ +package com.deftdevs.bootstrapi.confluence.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.util.FieldNames; +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(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _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 testSetAllReturns500OnMixedSuccessAndServerError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _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 testSetAllReturns400OnMixedSuccessAndClientError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _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(400, 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..abd24f3d --- /dev/null +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/ConfluenceSettingsServiceTest.java @@ -0,0 +1,110 @@ +package com.deftdevs.bootstrapi.confluence.service; + +import com.atlassian.confluence.setup.settings.GlobalSettingsManager; +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.util.FieldNames; +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.spy; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ConfluenceSettingsServiceTest { + + @Mock + private GlobalSettingsManager globalSettingsManager; + + @Mock + private SettingsBrandingService settingsBrandingService; + + private SettingsServiceImpl settingsService; + + @BeforeEach + void setup() { + settingsService = spy(new SettingsServiceImpl(globalSettingsManager, 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 = settingsService.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 = settingsService.setSettings(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .security(SettingsSecurityModel.EXAMPLE_1) + .branding(SettingsBrandingColorSchemeModel.EXAMPLE_1) + .customHtml(SettingsCustomHtmlModel.EXAMPLE_1) + .build()); + + 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(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsSecurityModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBrandingColorSchemeModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsCustomHtmlModel.class)).getStatus()); + } + + @Test + void testSetSettingsSkipsNullSubFields() { + final ServiceResult result = settingsService.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 = settingsService.setSettings(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .branding(SettingsBrandingColorSchemeModel.EXAMPLE_1) + .build()); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertNull(result.getModel().getBranding()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBrandingColorSchemeModel.class)).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..6dcb7a52 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; @@ -26,11 +26,14 @@ class SettingsServiceTest { @Mock private GlobalSettingsManager globalSettingsManager; + @Mock + private com.deftdevs.bootstrapi.commons.service.api.SettingsBrandingService settingsBrandingService; + private SettingsServiceImpl settingsService; @BeforeEach public void setup() { - settingsService = new SettingsServiceImpl(globalSettingsManager); + settingsService = new SettingsServiceImpl(globalSettingsManager, settingsBrandingService); } @Test @@ -39,9 +42,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 +61,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 +87,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(); @@ -158,6 +161,7 @@ void testSetSettingsSecurity() { spy.setSettingsSecurity(settingsSecurityModel); verify(settings).setWebSudoEnabled(settingsSecurityModel.getWebSudoEnabled()); verify(settings).setWebSudoTimeout(settingsSecurityModel.getWebSudoTimeout()); + verify(globalSettingsManager).updateGlobalSettings(settings); } } 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..0b71b84e --- /dev/null +++ b/confluence/src/test/java/com/deftdevs/bootstrapi/confluence/service/_AllServiceImplTest.java @@ -0,0 +1,174 @@ +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.AuthenticationSsoModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.FieldNames; +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(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doReturn(directories).when(directoriesService).setDirectories(directories); + doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); + doReturn(new ServiceResult<>(authentication, + Collections.singletonMap(FieldNames.of(AuthenticationModel.class, AuthenticationSsoModel.class), _AllModelStatus.success()))) + .when(authenticationService).setAuthentication(authentication); + doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); + doReturn(new ServiceResult<>(mailServer, + Collections.singletonMap(FieldNames.of(MailServerModel.class, MailServerSmtpModel.class), _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(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, AbstractDirectoryModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, ApplicationLinkModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, AuthenticationSsoModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, LicenseModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, PermissionsGlobalModel.class)).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(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _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(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get(FieldNames.of(_AllModel.class, LicenseModel.class)).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..85059036 --- /dev/null +++ b/confluence/src/test/java/it/com/deftdevs/bootstrapi/confluence/rest/_AllResourceFuncTest.java @@ -0,0 +1,56 @@ +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.FieldNames; +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._AbstractAllResourceFuncTest; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class _AllResourceFuncTest extends _AbstractAllResourceFuncTest { + + @Override + protected Object getExampleAllModel() { + return _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .build()) + .build(); + } + + @Override + protected String getSettingsGeneralStatusKey() { + return FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class); + } + + @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( + FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), + FieldNames.pathOf(_AllModel.class, SettingsCustomHtmlModel.class), + FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class), + FieldNames.pathOf(_AllModel.class, MailServerPopModel.class))); + } +} diff --git a/crowd/Apis/AllApi.md b/crowd/Apis/AllApi.md new file mode 100644 index 00000000..8d1c4ae0 --- /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, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 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'). If any sub-field fails, the highest sub-field status code is returned with the same response body. Crowd does not support applying a POP mail server configuration. + +### 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..6f85b170 --- /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] | +| **applicationLinks** | [**Map**](ApplicationLinkModel.md) | | [optional] [default to null] | +| **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | +| **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | +| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | +| **applications** | [**Map**](ApplicationModel.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] | + +[[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..cedb0db1 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,20 @@ public class ServiceConfig { @Autowired private HelperConfig helperConfig; + @Bean + public _AllService<_AllModel> _allService() { + return new _AllServiceImpl( + crowdSettingsService(), + directoriesService(), + applicationsService(), + applicationLinksService(), + licensesService(), + mailServerService(), + mailTemplatesService(), + sessionConfigService(), + trustedProxiesService()); + } + @Bean public ApplicationLinksService applicationLinksService() { return new ApplicationLinksServiceImpl( @@ -34,15 +49,15 @@ public ApplicationsService applicationsService() { } @Bean - public CrowdSettingsBrandingService settingsBrandingService() { - return new SettingsBrandingServiceImpl( - atlassianConfig.propertyManager()); - } - - @Bean - public CrowdSettingsGeneralService crowdSettingsGeneralService() { + public CrowdSettingsService crowdSettingsService() { + // The single settings bean implements the composite interface; the branding + // implementation is deliberately NOT its own bean, so by-type injection of + // CrowdSettingsGeneralService and CrowdSettingsBrandingService in the REST + // layer resolves unambiguously to this bean. return new SettingsServiceImpl( - atlassianConfig.propertyManager()); + atlassianConfig.propertyManager(), + new SettingsBrandingServiceImpl( + atlassianConfig.propertyManager())); } @Bean 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..0f356ec6 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,18 +1,21 @@ package com.deftdevs.bootstrapi.crowd.model; -import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; import lombok.NoArgsConstructor; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; + @Data @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(SettingsModel.class) @XmlRootElement(name = BootstrAPI.SETTINGS_BRANDING_LOGIN_PAGE) public class SettingsBrandingLoginPageModel { diff --git a/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java new file mode 100644 index 00000000..15fbc990 --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/SettingsModel.java @@ -0,0 +1,24 @@ +package com.deftdevs.bootstrapi.crowd.model; + +import com.deftdevs.bootstrapi.commons.model.AbstractSettingsModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@XmlRootElement(name = SETTINGS) +public class SettingsModel extends AbstractSettingsModel { + + @XmlElement + 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..1c4a4142 --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/model/_AllModel.java @@ -0,0 +1,34 @@ +package com.deftdevs.bootstrapi.crowd.model; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model._AbstractAllModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; +import java.util.Map; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@XmlRootElement(name = BootstrAPI._ALL) +public class _AllModel extends _AbstractAllModel { + + @XmlElement + private Map applications; + + @XmlElement + private MailTemplatesModel mailTemplates; + + @XmlElement + private SessionConfigModel sessionConfig; + + @XmlElement + private List trustedProxies; + +} 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..202b70bb --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceImpl.java @@ -0,0 +1,69 @@ +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 + + " Crowd does not support applying a POP mail server configuration.", + responses = { + @ApiResponse( + responseCode = "200", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "4XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "5XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_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/SettingsServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/SettingsServiceImpl.java index ec4f6d64..f32a0fd5 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,24 +3,30 @@ 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.crowd.service.api.CrowdSettingsGeneralService; +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.CrowdSettingsService; -public class SettingsServiceImpl - implements CrowdSettingsGeneralService { +import java.io.InputStream; + +public class SettingsServiceImpl implements CrowdSettingsService { private final PropertyManager propertyManager; + private final CrowdSettingsBrandingService settingsBrandingService; public SettingsServiceImpl( - final PropertyManager propertyManager) { + final PropertyManager propertyManager, + final CrowdSettingsBrandingService settingsBrandingService) { this.propertyManager = propertyManager; + this.settingsBrandingService = settingsBrandingService; } @Override - public SettingsModel getSettingsGeneral() { + public SettingsGeneralModel getSettingsGeneral() { try { - return SettingsModel.builder() + return SettingsGeneralModel.builder() .baseUrl(propertyManager.getBaseUrl()) .title(propertyManager.getDeploymentTitle()) .build(); @@ -30,7 +36,7 @@ public SettingsModel getSettingsGeneral() { } @Override - public SettingsModel setSettingsGeneral(SettingsModel settingsModel) { + public SettingsGeneralModel setSettingsGeneral(SettingsGeneralModel settingsModel) { if (settingsModel.getBaseUrl() != null) { propertyManager.setBaseUrl(settingsModel.getBaseUrl()); } @@ -40,4 +46,19 @@ public SettingsModel setSettingsGeneral(SettingsModel settingsModel) { return getSettingsGeneral(); } + @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/_AllServiceImpl.java b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImpl.java new file mode 100644 index 00000000..746c31a9 --- /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.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._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.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 java.util.LinkedHashMap; +import java.util.Map; + + +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 LinkedHashMap<>(); + + setEntityWithStatus(SettingsModel.class, allModel.getSettings(), + settingsService::setSettings, result::setSettings, statusMap); + + setEntities(AbstractDirectoryModel.class, allModel.getDirectories(), + directoriesService::setDirectories, result::setDirectories, statusMap); + + setEntities(ApplicationModel.class, allModel.getApplications(), + applicationsService::setApplications, result::setApplications, statusMap); + + setEntities(ApplicationLinkModel.class, allModel.getApplicationLinks(), + applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); + + setEntities(LicenseModel.class, allModel.getLicenses(), + licensesService::setLicenses, result::setLicenses, statusMap); + + setEntityWithStatus(MailServerModel.class, allModel.getMailServer(), + mailServerService::setMailServer, result::setMailServer, statusMap); + + setEntity(MailTemplatesModel.class, allModel.getMailTemplates(), + mailTemplatesService::setMailTemplates, result::setMailTemplates, statusMap); + + setEntity(SessionConfigModel.class, allModel.getSessionConfig(), + sessionConfigService::setSessionConfig, result::setSessionConfig, statusMap); + + setEntity(_AllModel::getTrustedProxies, allModel, + 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..a1d3ed0c --- /dev/null +++ b/crowd/src/main/java/com/deftdevs/bootstrapi/crowd/service/api/CrowdSettingsService.java @@ -0,0 +1,36 @@ +package com.deftdevs.bootstrapi.crowd.service.api; + +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +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 SettingsModel.builder() + .general(getSettingsGeneral()) + .branding(getLoginPage()) + .build(); + } + + default ServiceResult setSettings(final SettingsModel settingsModel) { + final SettingsModel result = new SettingsModel(); + final Map status = new LinkedHashMap<>(); + + ServiceResultUtil.setSubEntity(status, SettingsGeneralModel.class, settingsModel.getGeneral(), + this::setSettingsGeneral, result::setGeneral); + ServiceResultUtil.setSubEntity(status, SettingsBrandingLoginPageModel.class, settingsModel.getBranding(), + this::setLoginPage, result::setBranding); + + 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..4f07481e --- /dev/null +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/rest/_AllResourceTest.java @@ -0,0 +1,96 @@ +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.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.util.FieldNames; +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(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _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 testSetAllReturns500OnMixedSuccessAndServerError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _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 testSetAllReturns400OnMixedSuccessAndClientError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _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(400, 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..0ac5a722 --- /dev/null +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/CrowdSettingsServiceTest.java @@ -0,0 +1,96 @@ +package com.deftdevs.bootstrapi.crowd.service; + +import com.atlassian.crowd.manager.property.PropertyManager; +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.commons.util.FieldNames; +import com.deftdevs.bootstrapi.crowd.model.SettingsBrandingLoginPageModel; +import com.deftdevs.bootstrapi.crowd.model.SettingsModel; +import com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService; +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.spy; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class CrowdSettingsServiceTest { + + @Mock + private PropertyManager propertyManager; + + @Mock + private CrowdSettingsBrandingService settingsBrandingService; + + private SettingsServiceImpl settingsService; + + @BeforeEach + void setup() { + settingsService = spy(new SettingsServiceImpl(propertyManager, settingsBrandingService)); + } + + @Test + void testGetSettings() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).getSettingsGeneral(); + doReturn(SettingsBrandingLoginPageModel.EXAMPLE_1).when(settingsBrandingService).getLoginPage(); + + final SettingsModel settingsModel = settingsService.getSettings(); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, settingsModel.getGeneral()); + assertEquals(SettingsBrandingLoginPageModel.EXAMPLE_1, settingsModel.getBranding()); + } + + @Test + void testSetSettingsAppliesAllSubFields() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doReturn(SettingsBrandingLoginPageModel.EXAMPLE_1).when(settingsBrandingService).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); + + final ServiceResult result = settingsService.setSettings(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .branding(SettingsBrandingLoginPageModel.EXAMPLE_1) + .build()); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertEquals(SettingsBrandingLoginPageModel.EXAMPLE_1, result.getModel().getBranding()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBrandingLoginPageModel.class)).getStatus()); + } + + @Test + void testSetSettingsSkipsNullSubFields() { + final ServiceResult result = settingsService.setSettings(new SettingsModel()); + + assertTrue(result.getStatus().isEmpty()); + verify(settingsService, never()).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + verify(settingsBrandingService, never()).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); + } + + @Test + void testSetSettingsRecordsPerSubFieldFailure() { + doReturn(SettingsGeneralModel.EXAMPLE_1).when(settingsService).setSettingsGeneral(SettingsGeneralModel.EXAMPLE_1); + doThrow(new BadRequestException("invalid login page")) + .when(settingsBrandingService).setLoginPage(SettingsBrandingLoginPageModel.EXAMPLE_1); + + final ServiceResult result = settingsService.setSettings(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .branding(SettingsBrandingLoginPageModel.EXAMPLE_1) + .build()); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertNull(result.getModel().getBranding()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBrandingLoginPageModel.class)).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..32350e43 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,13 +23,16 @@ public class SettingsServiceTest { @Mock private PropertyManager propertyManager; - public static final SettingsModel SETTINGS_MODEL = EXAMPLE_1_NO_MODE; + @Mock + private com.deftdevs.bootstrapi.crowd.service.api.CrowdSettingsBrandingService settingsBrandingService; + + public static final SettingsGeneralModel SETTINGS_MODEL = EXAMPLE_1_NO_MODE; private SettingsServiceImpl settingsService; @BeforeEach public void setup() { - settingsService = new SettingsServiceImpl(propertyManager); + settingsService = new SettingsServiceImpl(propertyManager, settingsBrandingService); } @Test 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..7fe5acf5 --- /dev/null +++ b/crowd/src/test/java/com/deftdevs/bootstrapi/crowd/service/_AllServiceImplTest.java @@ -0,0 +1,195 @@ +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.util.FieldNames; +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(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _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(FieldNames.of(MailServerModel.class, MailServerSmtpModel.class), _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(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, AbstractDirectoryModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, ApplicationModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, ApplicationLinkModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, LicenseModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, MailTemplatesModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, SessionConfigModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, String.class)).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(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _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(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get(FieldNames.of(_AllModel.class, LicenseModel.class)).getStatus()); + } +} diff --git a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/MailServerSmtpResourceFuncTest.java b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/MailServerSmtpResourceFuncTest.java index b075bb11..73ae9866 100644 --- a/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/MailServerSmtpResourceFuncTest.java +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/MailServerSmtpResourceFuncTest.java @@ -17,13 +17,6 @@ protected void testGetMailServerSmtpNotConfigured() { @Override protected MailServerSmtpModel getExampleModel() { - return MailServerSmtpModel.builder() - .from("test@example.com") - .prefix("[Test]") - .host("localhost") - .port(3025) - .useTls(false) - .timeout(5000L) - .build(); + return MailServerSmtpModel.EXAMPLE_2_MINIMAL; } } 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..12f22981 --- /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 SettingsGeneralModel.EXAMPLE_1_MINIMAL; + } + +} 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..07436483 --- /dev/null +++ b/crowd/src/test/java/it/com/deftdevs/bootstrapi/crowd/rest/_AllResourceFuncTest.java @@ -0,0 +1,59 @@ +package it.com.deftdevs.bootstrapi.crowd.rest; + +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.FieldNames; +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._AbstractAllResourceFuncTest; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class _AllResourceFuncTest extends _AbstractAllResourceFuncTest { + + @Override + protected Object getExampleAllModel() { + return _AllModel.builder() + .settings(SettingsModel.builder() + .general(getExampleSettingsGeneralModel()) + .build()) + .build(); + } + + @Override + protected String getSettingsGeneralStatusKey() { + return FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class); + } + + @Override + protected SettingsGeneralModel getExampleSettingsGeneralModel() { + return SettingsGeneralModel.EXAMPLE_1_MINIMAL; + } + + @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(MailServerSmtpModel.EXAMPLE_2_MINIMAL) + .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( + FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), + FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class), + FieldNames.of(_AllModel.class, MailTemplatesModel.class), + FieldNames.of(_AllModel.class, SessionConfigModel.class), + FieldNames.of(_AllModel.class, String.class))); + } +} diff --git a/jira/Apis/AllApi.md b/jira/Apis/AllApi.md new file mode 100644 index 00000000..c636e98c --- /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, keyed by the request's field paths (e.g. 'settings/general', 'mailServer/smtp'; 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'). If any sub-field fails, the highest sub-field status code is returned with the same response body. + +### 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..82c41779 --- /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] | +| **licenses** | [**Map**](LicenseModel.md) | | [optional] [default to null] | +| **mailServer** | [**MailServerModel**](MailServerModel.md) | | [optional] [default to null] | +| **status** | [**Map**](_AllModelStatus.md) | | [optional] [default to null] | +| **authentication** | [**AuthenticationModel**](AuthenticationModel.md) | | [optional] [default to null] | +| **permissionsGlobal** | [**PermissionsGlobalModel**](PermissionsGlobalModel.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/SettingsBannerModel.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsBannerModel.java index d739a6b7..5c3efc4c 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsBannerModel.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsBannerModel.java @@ -8,6 +8,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import com.deftdevs.bootstrapi.commons.model.type.SubEntityOf; import javax.xml.bind.annotation.XmlRootElement; import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; @@ -17,6 +18,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor +@SubEntityOf(SettingsModel.class) @XmlRootElement(name = SETTINGS + "-" + SETTINGS_BANNER) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsBannerModel { 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..eb10ac59 --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/SettingsModel.java @@ -0,0 +1,24 @@ +package com.deftdevs.bootstrapi.jira.model; + +import com.deftdevs.bootstrapi.commons.model.AbstractSettingsSecurityModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@XmlRootElement(name = SETTINGS) +public class SettingsModel extends AbstractSettingsSecurityModel { + + @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..640565c9 --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/model/_AllModel.java @@ -0,0 +1,28 @@ +package com.deftdevs.bootstrapi.jira.model; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model._AbstractAllModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; +import com.deftdevs.bootstrapi.commons.model.PermissionsGlobalModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@Data +@SuperBuilder +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@XmlRootElement(name = BootstrAPI._ALL) +public class _AllModel extends _AbstractAllModel { + + @XmlElement + private AuthenticationModel authentication; + + @XmlElement + private PermissionsGlobalModel permissionsGlobal; + +} diff --git a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/AuthenticationResourceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/AuthenticationResourceImpl.java index 76a31824..fe1fc850 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/AuthenticationResourceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/AuthenticationResourceImpl.java @@ -2,8 +2,6 @@ import com.atlassian.plugins.rest.api.security.annotation.SystemAdminOnly; import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; -import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; -import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.rest.AbstractAuthenticationResourceImpl; import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; @@ -12,7 +10,7 @@ @Path(BootstrAPI.AUTHENTICATION) @SystemAdminOnly -public class AuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { +public class AuthenticationResourceImpl extends AbstractAuthenticationResourceImpl { @Inject public AuthenticationResourceImpl( 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..ef4baba6 --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceImpl.java @@ -0,0 +1,68 @@ +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 = "4XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "5XX", content = @Content(schema = @Schema(implementation = _AllModel.class)), + description = BootstrAPI._ALL_PUT_FAILURE_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/AuthenticationServiceImpl.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/AuthenticationServiceImpl.java index 9ce75727..27a4a3c3 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/AuthenticationServiceImpl.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/AuthenticationServiceImpl.java @@ -12,7 +12,7 @@ import com.deftdevs.bootstrapi.jira.service.api.JiraAuthenticationService; public class AuthenticationServiceImpl - extends AbstractAuthenticationService + extends AbstractAuthenticationService implements JiraAuthenticationService { public AuthenticationServiceImpl( 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..7e951bd1 --- /dev/null +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImpl.java @@ -0,0 +1,84 @@ +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.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.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 java.util.LinkedHashMap; +import java.util.Map; + + +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 LinkedHashMap<>(); + + setEntityWithStatus(SettingsModel.class, allModel.getSettings(), + settingsService::setSettings, result::setSettings, statusMap); + + setEntities(AbstractDirectoryModel.class, allModel.getDirectories(), + directoriesService::setDirectories, result::setDirectories, statusMap); + + setEntities(ApplicationLinkModel.class, allModel.getApplicationLinks(), + applicationLinksService::setApplicationLinks, result::setApplicationLinks, statusMap); + + setEntityWithStatus(AuthenticationModel.class, allModel.getAuthentication(), + authenticationService::setAuthentication, result::setAuthentication, statusMap); + + setEntities(LicenseModel.class, allModel.getLicenses(), + licensesService::setLicenses, result::setLicenses, statusMap); + + setEntityWithStatus(MailServerModel.class, allModel.getMailServer(), + mailServerService::setMailServer, result::setMailServer, statusMap); + + setEntity(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/JiraAuthenticationService.java b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraAuthenticationService.java index c4c733ab..f07762d6 100644 --- a/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraAuthenticationService.java +++ b/jira/src/main/java/com/deftdevs/bootstrapi/jira/service/api/JiraAuthenticationService.java @@ -1,10 +1,8 @@ package com.deftdevs.bootstrapi.jira.service.api; -import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; -import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; import com.deftdevs.bootstrapi.commons.service.api.AuthenticationService; public interface JiraAuthenticationService extends - AuthenticationService { + AuthenticationService { } 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..48a44470 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.ServiceResult; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +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,26 @@ public interface JiraSettingsService extends SettingsBannerModel setSettingsBanner( SettingsBannerModel settingsBannerModel); + default SettingsModel getSettings() { + return SettingsModel.builder() + .general(getSettingsGeneral()) + .security(getSettingsSecurity()) + .banner(getSettingsBanner()) + .build(); + } + + default ServiceResult setSettings(final SettingsModel settingsModel) { + final SettingsModel result = new SettingsModel(); + final Map status = new LinkedHashMap<>(); + + ServiceResultUtil.setSubEntity(status, SettingsGeneralModel.class, settingsModel.getGeneral(), + this::setSettingsGeneral, result::setGeneral); + ServiceResultUtil.setSubEntity(status, SettingsSecurityModel.class, settingsModel.getSecurity(), + this::setSettingsSecurity, result::setSecurity); + ServiceResultUtil.setSubEntity(status, SettingsBannerModel.class, settingsModel.getBanner(), + this::setSettingsBanner, result::setBanner); + + 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..ac01d1f8 --- /dev/null +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/rest/_AllResourceTest.java @@ -0,0 +1,91 @@ +package com.deftdevs.bootstrapi.jira.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.AbstractDirectoryModel; +import com.deftdevs.bootstrapi.commons.model.ApplicationLinkModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.util.FieldNames; +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(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _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 testSetAllReturns500OnMixedSuccessAndServerError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _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 testSetAllReturns400OnMixedSuccessAndClientError() { + final _AllModel result = new _AllModel(); + final Map status = new HashMap<>(); + status.put(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), _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(400, 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..f65e4428 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,24 @@ 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.commons.util.FieldNames; +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 +50,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 +61,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 +80,7 @@ void testSetSettingsGeneral() { @Test void testSetSettingsGeneralEmptyModel() { - final SettingsModel settingsModel = SettingsModel.builder().build(); + final SettingsGeneralModel settingsModel = SettingsGeneralModel.builder().build(); settingsService.setSettingsGeneral(settingsModel); @@ -85,7 +92,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 +101,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 +109,68 @@ 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(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .security(SettingsSecurityModel.EXAMPLE_1) + .banner(SettingsBannerModel.EXAMPLE_1) + .build()); + + 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(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsSecurityModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBannerModel.class)).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(SettingsModel.builder() + .general(SettingsGeneralModel.EXAMPLE_1) + .banner(SettingsBannerModel.EXAMPLE_1) + .build()); + + assertEquals(SettingsGeneralModel.EXAMPLE_1, result.getModel().getGeneral()); + assertNull(result.getModel().getBanner()); + assertEquals(Response.Status.OK.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), result.getStatus().get(FieldNames.of(SettingsModel.class, SettingsBannerModel.class)).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..d0e09ea7 --- /dev/null +++ b/jira/src/test/java/com/deftdevs/bootstrapi/jira/service/_AllServiceImplTest.java @@ -0,0 +1,174 @@ +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.AuthenticationSsoModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.FieldNames; +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(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _AllModelStatus.success()))) + .when(settingsService).setSettings(settings); + doReturn(directories).when(directoriesService).setDirectories(directories); + doReturn(applicationLinks).when(applicationLinksService).setApplicationLinks(applicationLinks); + doReturn(new ServiceResult<>(authentication, + Collections.singletonMap(FieldNames.of(AuthenticationModel.class, AuthenticationSsoModel.class), _AllModelStatus.success()))) + .when(authenticationService).setAuthentication(authentication); + doReturn(redactedLicenses).when(licensesService).setLicenses(licenses); + doReturn(new ServiceResult<>(mailServer, + Collections.singletonMap(FieldNames.of(MailServerModel.class, MailServerSmtpModel.class), _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(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, AbstractDirectoryModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, ApplicationLinkModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, AuthenticationSsoModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, LicenseModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class)).getStatus()); + assertEquals(Response.Status.OK.getStatusCode(), status.get(FieldNames.of(_AllModel.class, PermissionsGlobalModel.class)).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(FieldNames.of(SettingsModel.class, SettingsGeneralModel.class), _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(FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class)).getStatus()); + assertEquals(Response.Status.CONFLICT.getStatusCode(), result.getStatus().get(FieldNames.of(_AllModel.class, LicenseModel.class)).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..6b80aac7 --- /dev/null +++ b/jira/src/test/java/it/com/deftdevs/bootstrapi/jira/rest/_AllResourceFuncTest.java @@ -0,0 +1,51 @@ +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.FieldNames; +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._AbstractAllResourceFuncTest; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class _AllResourceFuncTest extends _AbstractAllResourceFuncTest { + + @Override + protected String getSettingsGeneralStatusKey() { + return FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class); + } + + @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( + FieldNames.pathOf(_AllModel.class, SettingsGeneralModel.class), + FieldNames.pathOf(_AllModel.class, SettingsBannerModel.class), + FieldNames.pathOf(_AllModel.class, MailServerSmtpModel.class), + FieldNames.pathOf(_AllModel.class, MailServerPopModel.class))); + } +}