diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java index 644ae427..8792b473 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/constants/BootstrAPI.java @@ -2,7 +2,9 @@ public class BootstrAPI { - public static final String ALL = "all"; + public static final String _ALL = "_all"; + public static final String _ROOT = "/"; + public static final String APPLICATION = "application"; public static final String APPLICATIONS = "applications"; public static final String APPLICATION_LINK = "application-link"; @@ -38,29 +40,39 @@ public class BootstrAPI { public static final String MAIL_SERVERS = "mail-servers"; public static final String MAIL_SERVER_POP = "pop"; public static final String MAIL_SERVER_SMTP = "smtp"; + public static final String MAIL_TEMPLATES = "mail-templates"; public static final String PERMISSION = "permission"; public static final String PERMISSIONS = "permissions"; public static final String PERMISSION_ANONYMOUS_ACCESS = "anonymous-access"; public static final String PERMISSIONS_GLOBAL = "global"; public static final String PING = "ping"; + public static final String SESSION_CONFIG = "session-config"; public static final String SETTINGS = "settings"; public static final String SETTINGS_BANNER = "banner"; public static final String SETTINGS_BRANDING = "branding"; public static final String SETTINGS_BRANDING_LOGIN_PAGE = "login-page"; public static final String SETTINGS_BRANDING_LOGO = "logo"; public static final String SETTINGS_CUSTOM_HTML = "custom-html"; + public static final String SETTINGS_GENERAL = "general"; public static final String SETTINGS_SECURITY = "security"; public static final String USER = "user"; public static final String USERS = "users"; + public static final String TRUSTED_PROXIES = "trusted-proxies"; public static final String USER_PASSWORD = "password"; public static final String MEDIA_TYPE_IMAGE = "image/*"; public static final String ERROR_COLLECTION_RESPONSE_DESCRIPTION = "Returns a list of error messages."; + public static final String _ALL_PUT_SUMMARY = "Apply a complete configuration"; + public static final String _ALL_PUT_RESPONSE_DESCRIPTION = "Returns the updated configuration. The per-sub-field outcome is reported in the" + + " 'status' map (2xx for success, 4xx/5xx for failure with a human-readable 'message' and optional 'details')." + + " License keys in the response are redacted (e.g. 'AAAB...wxyz#a1b2')."; + public static final String _ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION = "Configuration applied partially. Inspect the per-sub-field 'status' map" + + " in the response body to see which fields succeeded and which failed."; public static final String SETTINGS_GENERAL_GET_SUMMARY = "Get the general settings"; public static final String SETTINGS_GENERAL_GET_RESPONSE_DESCRIPTION = "Returns the general settings"; public static final String SETTINGS_GENERAL_PUT_SUMMARY = "Set the general settings"; - public static final String SETTINGS_GENERAL_PUT_RESPONSE_DESCRIPTION = "Returns the general security settings"; + public static final String SETTINGS_GENERAL_PUT_RESPONSE_DESCRIPTION = "Returns the updated general settings"; public static final String SETTINGS_SECURITY_GET_SUMMARY = "Get the security settings"; public static final String SETTINGS_SECURITY_GET_RESPONSE_DESCRIPTION = "Returns the security settings"; public static final String SETTINGS_SECURITY_PUT_SUMMARY = "Set the security settings"; diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java index ba5828d8..86ee05b1 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AbstractAuthenticationIdpModel.java @@ -17,7 +17,7 @@ @SuperBuilder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement +@XmlRootElement(name = "authentication-idps") @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java new file mode 100644 index 00000000..909488f4 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/AuthenticationModel.java @@ -0,0 +1,25 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.Map; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.AUTHENTICATION; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = AUTHENTICATION) +public class AuthenticationModel { + + @XmlElement + private Map idps; + + @XmlElement + private AuthenticationSsoModel sso; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java new file mode 100644 index 00000000..5329276e --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/MailServerModel.java @@ -0,0 +1,26 @@ +package com.deftdevs.bootstrapi.commons.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.MAIL_SERVER; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = MAIL_SERVER) +public class MailServerModel { + + @XmlElement + private MailServerSmtpModel smtp; + + @XmlElement + private MailServerPopModel pop; + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java index 29fe6548..66740ab0 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsBrandingColorSchemeModel.java @@ -16,7 +16,7 @@ @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING + "-" + COLOR_SCHEME) +@XmlRootElement(name = SETTINGS + "-" + SETTINGS_BRANDING) @XmlAccessorType(XmlAccessType.FIELD) public class SettingsBrandingColorSchemeModel { diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsModel.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java similarity index 75% rename from commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsModel.java rename to commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java index 1689a097..db8e9dbe 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsModel.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/SettingsGeneralModel.java @@ -1,6 +1,5 @@ package com.deftdevs.bootstrapi.commons.model; -import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Builder; @@ -13,13 +12,16 @@ import javax.xml.bind.annotation.XmlRootElement; import java.net.URI; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS; +import static com.deftdevs.bootstrapi.commons.constants.BootstrAPI.SETTINGS_GENERAL; + @Data @Builder @NoArgsConstructor @AllArgsConstructor -@XmlRootElement(name = BootstrAPI.SETTINGS) +@XmlRootElement(name = SETTINGS + "-" + SETTINGS_GENERAL) @XmlAccessorType(XmlAccessType.FIELD) -public class SettingsModel { +public class SettingsGeneralModel { @XmlElement private URI baseUrl; @@ -46,7 +48,7 @@ public String getMode() { // Example instances for documentation and tests - public static final SettingsModel EXAMPLE_1 = SettingsModel.builder() + public static final SettingsGeneralModel EXAMPLE_1 = SettingsGeneralModel.builder() .title("Example") .baseUrl(URI.create("https://example.com")) .mode("private") @@ -54,7 +56,7 @@ public String getMode() { .externalUserManagement(true) .build(); - public static final SettingsModel EXAMPLE_1_NO_MODE = SettingsModel.builder() + public static final SettingsGeneralModel EXAMPLE_1_NO_MODE = SettingsGeneralModel.builder() .title("Example") .baseUrl(URI.create("https://example.com")) .mode(null) diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java new file mode 100644 index 00000000..53b0f1fb --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/ServiceResult.java @@ -0,0 +1,26 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import java.util.Map; + +public class ServiceResult { + + private final T model; + private final Map status; + + public ServiceResult( + final T model, + final Map status) { + + this.model = model; + this.status = status; + } + + public T getModel() { + return model; + } + + public Map getStatus() { + return status; + } + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java new file mode 100644 index 00000000..c586adff --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelAccessor.java @@ -0,0 +1,9 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import java.util.Map; + +public interface _AllModelAccessor { + + Map getStatus(); + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java new file mode 100644 index 00000000..42d73b65 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/model/type/_AllModelStatus.java @@ -0,0 +1,40 @@ +package com.deftdevs.bootstrapi.commons.model.type; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.ws.rs.core.Response; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@XmlRootElement(name = "status") +public class _AllModelStatus { + + @XmlElement + private int status; + + @XmlElement + private String message; + + @XmlElement + private String details; + + public static _AllModelStatus success() { + return new _AllModelStatus(Response.Status.OK.getStatusCode(), "Success", null); + } + + public static _AllModelStatus error(Response.Status status, String message, String details) { + final int code = status != null + ? status.getStatusCode() + : Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + return new _AllModelStatus(code, message, details); + } + + public static _AllModelStatus error(int statusCode, String message, String details) { + return new _AllModelStatus(statusCode, message, details); + } +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java new file mode 100644 index 00000000..b93da638 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsGeneralResourceImpl.java @@ -0,0 +1,31 @@ +package com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; +import com.deftdevs.bootstrapi.commons.rest.api.SettingsGeneralResource; +import com.deftdevs.bootstrapi.commons.service.api.SettingsGeneralService; + +import javax.ws.rs.core.Response; + +public abstract class AbstractSettingsGeneralResourceImpl> + implements SettingsGeneralResource { + + private final S settingsService; + + public AbstractSettingsGeneralResourceImpl( + final S settingsService) { + + this.settingsService = settingsService; + } + + @Override + public Response getSettings() { + final B settingsModel = settingsService.getSettingsGeneral(); + return Response.ok(settingsModel).build(); + } + + @Override + public Response setSettings(B settingsModel) { + final B updatedSettingsGeneralModel = settingsService.setSettingsGeneral(settingsModel); + return Response.ok(updatedSettingsGeneralModel).build(); + } +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java deleted file mode 100644 index c1c4c405..00000000 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/AbstractSettingsResourceImpl.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.deftdevs.bootstrapi.commons.rest; - -import com.deftdevs.bootstrapi.commons.model.SettingsModel; -import com.deftdevs.bootstrapi.commons.rest.api.SettingsResource; -import com.deftdevs.bootstrapi.commons.service.api.SettingsService; - -import javax.ws.rs.core.Response; - -public abstract class AbstractSettingsResourceImpl> - implements SettingsResource { - - private final S settingsService; - - public AbstractSettingsResourceImpl( - final S settingsService) { - - this.settingsService = settingsService; - } - - @Override - public Response getSettings() { - final B settingsModel = settingsService.getSettingsGeneral(); - return Response.ok(settingsModel).build(); - } - - @Override - public Response setSettings(B settingsModel) { - final B updatedSettingsModel = settingsService.setSettingsGeneral(settingsModel); - return Response.ok(updatedSettingsModel).build(); - } -} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java new file mode 100644 index 00000000..45a5febf --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/_AbstractAllResourceImpl.java @@ -0,0 +1,85 @@ +package com.deftdevs.bootstrapi.commons.rest; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.rest.api._AllResource; +import com.deftdevs.bootstrapi.commons.service.api._AllService; + +import javax.ws.rs.core.Response; +import java.util.Map; + +public abstract class _AbstractAllResourceImpl<_AllModel extends _AllModelAccessor> + implements _AllResource<_AllModel> { + + static final int MULTI_STATUS = 207; + + private final _AllService<_AllModel> allService; + + public _AbstractAllResourceImpl( + final _AllService<_AllModel> allService) { + + this.allService = allService; + } + + @Override + public Response setAll( + final _AllModel allModel) { + + final _AllModel result = allService.setAll(allModel); + final int overallStatus = computeOverallStatus(result.getStatus()); + return Response.status(overallStatus).entity(result).build(); + } + + /** + * Aggregates per-sub-field statuses into a single HTTP response code: + *
    + *
  • All successful (or empty) → 200 OK.
  • + *
  • All entries share the same status code → that code.
  • + *
  • Any 5xx → 500 Internal Server Error.
  • + *
  • Mixed 2xx/4xx (or multiple distinct 4xx) → 207 Multi-Status.
  • + *
+ * 207 is used to signal "partial success" — callers must inspect the + * per-field {@code status} map in the response body to see which + * fields succeeded and which failed. + */ + static int computeOverallStatus( + final Map statusMap) { + + if (statusMap == null || statusMap.isEmpty()) { + return Response.Status.OK.getStatusCode(); + } + + boolean anyServerError = false; + boolean anyClientError = false; + boolean anySuccess = false; + Integer firstClientErrorCode = null; + boolean clientErrorCodesDiffer = false; + + for (_AllModelStatus entry : statusMap.values()) { + final int code = entry.getStatus(); + if (code >= 500) { + anyServerError = true; + } else if (code >= 400) { + anyClientError = true; + if (firstClientErrorCode == null) { + firstClientErrorCode = code; + } else if (firstClientErrorCode != code) { + clientErrorCodesDiffer = true; + } + } else if (code >= 200 && code < 300) { + anySuccess = true; + } + } + + if (anyServerError) { + return Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + } + if (anyClientError) { + if (anySuccess || clientErrorCodesDiffer) { + return MULTI_STATUS; + } + return firstClientErrorCode; + } + return Response.Status.OK.getStatusCode(); + } +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsResource.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsGeneralResource.java similarity index 86% rename from commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsResource.java rename to commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsGeneralResource.java index 8e415842..6bfcbe8a 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsResource.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/SettingsGeneralResource.java @@ -2,7 +2,7 @@ import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; import com.deftdevs.bootstrapi.commons.model.ErrorCollection; -import com.deftdevs.bootstrapi.commons.model.SettingsModel; +import com.deftdevs.bootstrapi.commons.model.SettingsGeneralModel; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -12,14 +12,14 @@ import javax.ws.rs.PUT; import javax.ws.rs.core.Response; -public interface SettingsResource { +public interface SettingsGeneralResource { @GET @Operation( summary = BootstrAPI.SETTINGS_GENERAL_GET_SUMMARY, responses = { @ApiResponse( - responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsModel.class)), + responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsGeneralModel.class)), description = BootstrAPI.SETTINGS_GENERAL_GET_RESPONSE_DESCRIPTION ), @ApiResponse( @@ -35,7 +35,7 @@ public interface SettingsResource { summary = BootstrAPI.SETTINGS_GENERAL_PUT_SUMMARY, responses = { @ApiResponse( - responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsModel.class)), + responseCode = "200", content = @Content(schema = @Schema(implementation = SettingsGeneralModel.class)), description = BootstrAPI.SETTINGS_GENERAL_PUT_RESPONSE_DESCRIPTION ), @ApiResponse( diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java new file mode 100644 index 00000000..62d097a0 --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/rest/api/_AllResource.java @@ -0,0 +1,44 @@ +package com.deftdevs.bootstrapi.commons.rest.api; + +import com.deftdevs.bootstrapi.commons.constants.BootstrAPI; +import com.deftdevs.bootstrapi.commons.model.ErrorCollection; +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import javax.validation.constraints.NotNull; +import javax.ws.rs.PUT; +import javax.ws.rs.core.Response; + +public interface _AllResource<_AllModel extends _AllModelAccessor> { + + /** + * Product implementations should override this method and re-declare the + * {@code @Operation} annotation with their concrete {@code _AllModel} as the + * response schema, so the generated OpenAPI documentation shows the product + * model instead of an empty response body. + */ + @PUT + @Operation( + summary = BootstrAPI._ALL_PUT_SUMMARY, + responses = { + @ApiResponse( + responseCode = "200", + description = BootstrAPI._ALL_PUT_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "207", + description = BootstrAPI._ALL_PUT_PARTIAL_RESPONSE_DESCRIPTION + ), + @ApiResponse( + responseCode = "default", content = @Content(schema = @Schema(implementation = ErrorCollection.class)), + description = BootstrAPI.ERROR_COLLECTION_RESPONSE_DESCRIPTION + ), + } + ) + Response setAll( + @NotNull final _AllModel bean); + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java new file mode 100644 index 00000000..1f6c938c --- /dev/null +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/_AbstractAllServiceImpl.java @@ -0,0 +1,89 @@ +package com.deftdevs.bootstrapi.commons.service; + +import com.deftdevs.bootstrapi.commons.model.type._AllModelAccessor; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.service.api._AllService; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; + +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +public abstract class _AbstractAllServiceImpl<_AllModel extends _AllModelAccessor> implements _AllService<_AllModel> { + + protected void setEntityWithStatus( + final I input, + final Function> updateFunction, + final Consumer resultConsumer, + final Map statusMap) { + + if (input == null) { + return; + } + + try { + final ServiceResult serviceResult = updateFunction.apply(input); + resultConsumer.accept(serviceResult.getModel()); + statusMap.putAll(serviceResult.getStatus()); + } catch (Exception e) { + // The composite updateFunction normally produces fine-grained + // sub-field status entries itself. If it threw before doing so + // (e.g. NPE on a sub-field accessor, or a cast failure on a + // generic parameter), attribute the failure to the top-level + // group derived from the input model's @XmlRootElement name. + final String fallbackKey = ServiceResultUtil.entityType(input.getClass()); + statusMap.put(fallbackKey, ServiceResultUtil.toErrorStatus(fallbackKey, e)); + } + } + + protected _AllModelStatus setEntity( + final String entityType, + final I input, + final Function updateFunction, + final Consumer resultConsumer, + final Map statusMap) { + + if (input == null) { + return null; + } + + try { + final O updatedEntity = updateFunction.apply(input); + resultConsumer.accept(updatedEntity); + final _AllModelStatus status = _AllModelStatus.success(); + statusMap.put(entityType, status); + return status; + } catch (Exception e) { + final _AllModelStatus status = ServiceResultUtil.toErrorStatus(entityType, e); + statusMap.put(entityType, status); + return status; + } + } + + @SuppressWarnings("unchecked") + protected _AllModelStatus setEntities( + final String entityType, + final Map entityMap, + final Function, Map> updateFunction, + final Consumer> resultConsumer, + final Map statusMap) { + + if (entityMap == null || entityMap.isEmpty()) { + return null; + } + + try { + final Map updatedEntities = updateFunction.apply(entityMap); + resultConsumer.accept((Map) updatedEntities); + final _AllModelStatus status = _AllModelStatus.success(); + statusMap.put(entityType, status); + return status; + } catch (Exception e) { + final _AllModelStatus status = ServiceResultUtil.toErrorStatus(entityType, e); + statusMap.put(entityType, status); + return status; + } + } + +} diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java index 871806a1..a248a904 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/AuthenticationService.java @@ -1,12 +1,51 @@ package com.deftdevs.bootstrapi.commons.service.api; import com.deftdevs.bootstrapi.commons.model.AbstractAuthenticationIdpModel; +import com.deftdevs.bootstrapi.commons.model.AuthenticationModel; import com.deftdevs.bootstrapi.commons.model.AuthenticationSsoModel; +import com.deftdevs.bootstrapi.commons.model.type._AllModelStatus; +import com.deftdevs.bootstrapi.commons.model.type.ServiceResult; +import com.deftdevs.bootstrapi.commons.util.ServiceResultUtil; +import java.util.LinkedHashMap; import java.util.Map; public interface AuthenticationService { + @SuppressWarnings("unchecked") + default AuthenticationModel getAuthentication() { + return new AuthenticationModel( + (Map) getAuthenticationIdps(), + getAuthenticationSso()); + } + + @SuppressWarnings("unchecked") + default ServiceResult setAuthentication(final AuthenticationModel authenticationModel) { + final AuthenticationModel result = new AuthenticationModel(); + final Map status = new LinkedHashMap<>(); + + if (authenticationModel.getIdps() != null) { + final String key = ServiceResultUtil.subEntityKey(AbstractAuthenticationIdpModel.class); + try { + result.setIdps((Map) setAuthenticationIdps( + (Map) authenticationModel.getIdps())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + if (authenticationModel.getSso() != null) { + final String key = ServiceResultUtil.subEntityKey(AuthenticationSsoModel.class); + try { + result.setSso(setAuthenticationSso((SB) authenticationModel.getSso())); + status.put(key, _AllModelStatus.success()); + } catch (Exception e) { + status.put(key, ServiceResultUtil.toErrorStatus(key, e)); + } + } + return new ServiceResult<>(result, status); + } + Map getAuthenticationIdps(); Map setAuthenticationIdps( diff --git a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java index 8f07d738..6c39a758 100644 --- a/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java +++ b/commons/src/main/java/com/deftdevs/bootstrapi/commons/service/api/LicensesService.java @@ -3,6 +3,7 @@ import com.deftdevs.bootstrapi.commons.model.LicenseModel; import java.util.List; +import java.util.Map; public interface LicensesService { @@ -21,6 +22,19 @@ public interface LicensesService { List setLicenses( List licenseKeys); + /** + * Set licenses from a map keyed by license key. + *

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

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

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

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

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