From 89ae9bacc843c006792eeddab51a1a47fa1c0c1c Mon Sep 17 00:00:00 2001 From: Boubaker Khanfir Date: Fri, 13 Sep 2024 17:01:14 +0100 Subject: [PATCH] feat: Allow to retrieve I18N keys for Sites Name and Description in UI - MEED-3342 - Meeds-io/meeds#1629 After the enhanced behavior made on Meeds-io/gatein-portal#969 and Meeds-io/social#4028, this change will allow to provide sites translated names and descriptions coming from crowdin synchronized properties files. --- .../io/meeds/layout/rest/SiteLayoutRest.java | 43 ++++++ .../layout/service/SiteLayoutService.java | 91 +++++++++++++ .../layout/rest/PageTemplateRestTest.java | 39 ++---- .../rest/PortletInstanceCategoryRestTest.java | 39 ++---- .../layout/rest/PortletInstanceRestTest.java | 45 ++---- .../io/meeds/layout/rest/PortletRestTest.java | 30 +--- .../meeds/layout/rest/SiteLayoutRestTest.java | 128 ++++++++++++++++++ .../layout/service/SiteLayoutServiceTest.java | 96 ++++++++++++- .../vue-app/common/js/SiteLayoutService.js | 26 ++++ .../components/SitePropertiesDrawer.vue | 90 +++++++----- 10 files changed, 468 insertions(+), 159 deletions(-) create mode 100644 layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java diff --git a/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java b/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java index d5854012d..a1417901b 100644 --- a/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java +++ b/layout-service/src/main/java/io/meeds/layout/rest/SiteLayoutRest.java @@ -45,6 +45,7 @@ import org.exoplatform.portal.mop.SiteKey; import org.exoplatform.social.rest.entity.SiteEntity; +import io.meeds.layout.model.NodeLabel; import io.meeds.layout.model.PermissionUpdateModel; import io.meeds.layout.model.SiteCreateModel; import io.meeds.layout.model.SiteUpdateModel; @@ -237,4 +238,46 @@ public ResponseEntity createSite( } } + @GetMapping("{siteId}/labels") + @Operation(summary = "Retrieve site I18N labels", method = "GET", description = "This retrieves site labels") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) + public NodeLabel getSiteLabels( + HttpServletRequest request, + @Parameter(description = "Site id", required = true) + @PathVariable("siteId") + Long siteId) { + try { + return siteLayoutService.getSiteLabels(siteId, request.getRemoteUser()); + } catch (ObjectNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + + @GetMapping("{siteId}/descriptions") + @Operation(summary = "Retrieve site I18N descriptions", method = "GET", description = "This retrieves site descriptions") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Request fulfilled"), + @ApiResponse(responseCode = "403", description = "Forbidden"), + @ApiResponse(responseCode = "404", description = "Not found"), + }) + public NodeLabel getSiteDescriptions( + HttpServletRequest request, + @Parameter(description = "Site id", required = true) + @PathVariable("siteId") + Long siteId) { + try { + return siteLayoutService.getSiteDescriptions(siteId, request.getRemoteUser()); + } catch (ObjectNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage()); + } catch (IllegalAccessException e) { + throw new ResponseStatusException(HttpStatus.FORBIDDEN, e.getMessage()); + } + } + } diff --git a/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java b/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java index 2956da6af..6f2f3c62d 100644 --- a/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java +++ b/layout-service/src/main/java/io/meeds/layout/service/SiteLayoutService.java @@ -18,17 +18,31 @@ */ package io.meeds.layout.service; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.exoplatform.commons.ObjectAlreadyExistsException; import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.UserPortalConfig; import org.exoplatform.portal.config.UserPortalConfigService; import org.exoplatform.portal.config.model.PortalConfig; import org.exoplatform.portal.mop.SiteKey; +import org.exoplatform.portal.mop.SiteType; import org.exoplatform.portal.mop.service.LayoutService; +import org.exoplatform.portal.mop.user.UserPortal; +import org.exoplatform.services.resources.LocaleConfig; +import org.exoplatform.services.resources.LocaleConfigService; +import org.exoplatform.services.resources.LocaleContextInfo; +import io.meeds.layout.model.NodeLabel; import io.meeds.layout.model.PermissionUpdateModel; import io.meeds.layout.model.SiteCreateModel; import io.meeds.layout.model.SiteUpdateModel; @@ -41,12 +55,19 @@ public class SiteLayoutService { @Autowired private LayoutService layoutService; + @Autowired + private LocaleConfigService localeConfigService; + @Autowired private UserPortalConfigService portalConfigService; @Autowired private LayoutAclService aclService; + private Map supportedLanguages; + + private Locale defaultConfiguredLocale; + public PortalConfig getSite(long siteId, String username) throws ObjectNotFoundException, IllegalAccessException { PortalConfig portalConfig = layoutService.getPortalConfig(siteId); if (portalConfig == null) { @@ -153,6 +174,76 @@ public void updateSitePermissions(PermissionUpdateModel permissionUpdateModel, layoutService.save(portalConfig); } + public NodeLabel getSiteLabels(Long siteId, String username) throws ObjectNotFoundException, IllegalAccessException { + return getSiteLabel(siteId, username, true); + } + + public NodeLabel getSiteDescriptions(Long siteId, String username) throws ObjectNotFoundException, IllegalAccessException { + return getSiteLabel(siteId, username, false); + } + + private NodeLabel getSiteLabel(long siteId, String username, boolean isLabel) throws ObjectNotFoundException, + IllegalAccessException { + PortalConfig portalConfig = layoutService.getPortalConfig(siteId); + if (portalConfig == null) { + throw new ObjectNotFoundException(String.format("Site with id %s doesn't exists", siteId)); + } else if (!aclService.canViewSite(new SiteKey(portalConfig.getType(), portalConfig.getName()), username)) { + throw new IllegalAccessException(); + } + Locale defaultLocale = getDefaultLocale(); + + NodeLabel nodeLabel = new NodeLabel(); + nodeLabel.setDefaultLanguage(defaultLocale.getLanguage()); + nodeLabel.setSupportedLanguages(getSupportedLanguages(defaultLocale)); + Map labels = getLabels(portalConfig, defaultLocale, username, isLabel); + nodeLabel.setLabels(labels); + return nodeLabel; + } + + private Map getLabels(PortalConfig portalConfig, Locale defaultLocale, String username, boolean isLabel) { + SiteKey siteKey = new SiteKey(SiteType.valueOf(portalConfig.getType().toUpperCase()), portalConfig.getName()); + UserPortalConfig userPortalConfig = portalConfigService.getUserPortalConfig(siteKey.getName(), username); + UserPortal userPortal = userPortalConfig.getUserPortal(); + + Map labels = new HashMap<>(); + localeConfigService.getLocalConfigs() + .stream() + .map(LocaleConfig::getLocale) + .forEach(locale -> { + String translatedLabel = isLabel ? userPortal.getPortalLabel(siteKey, locale) : + userPortal.getPortalDescription(siteKey, locale); + labels.put(LocaleContextInfo.getLocaleAsString(locale), translatedLabel); + }); + if (!labels.containsKey(defaultLocale.getLanguage()) && !labels.isEmpty()) { + labels.put(defaultLocale.getLanguage(), labels.values().iterator().next()); + } + return labels; + } + + private Map getSupportedLanguages(Locale defaultLocale) { + if (supportedLanguages == null || !defaultConfiguredLocale.equals(defaultLocale)) { + supportedLanguages = CollectionUtils.isEmpty(localeConfigService.getLocalConfigs()) ? + Collections.singletonMap(defaultLocale.toLanguageTag(), + defaultLocale.getDisplayName()) : + localeConfigService.getLocalConfigs() + .stream() + .filter(localeConfig -> !StringUtils.equals(localeConfig.getLocale() + .toLanguageTag(), + "ma")) + .map(LocaleConfig::getLocale) + .collect(Collectors.toMap(Locale::toLanguageTag, + Locale::getDisplayName)); + defaultConfiguredLocale = defaultLocale; + } + return supportedLanguages; + } + + private Locale getDefaultLocale() { + return localeConfigService.getDefaultLocaleConfig() == null ? Locale.ENGLISH : + localeConfigService.getDefaultLocaleConfig() + .getLocale(); + } + private String getAdministratorsPermission() { return "*:" + aclService.getAdministratorsGroup(); } diff --git a/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java index 79d06d64c..68097e704 100644 --- a/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java +++ b/layout-service/src/test/java/io/meeds/layout/rest/PageTemplateRestTest.java @@ -18,6 +18,7 @@ */ package io.meeds.layout.rest; +import static io.meeds.layout.util.JsonUtils.toJsonString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; @@ -51,12 +52,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import com.fasterxml.jackson.core.json.JsonReadFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - import org.exoplatform.commons.exception.ObjectNotFoundException; import io.meeds.layout.model.PageTemplate; @@ -65,7 +60,6 @@ import io.meeds.spring.web.security.WebSecurityConfiguration; import jakarta.servlet.Filter; -import lombok.SneakyThrows; @SpringBootTest(classes = { PageTemplateRest.class, PortalAuthenticationManager.class, }) @ContextConfiguration(classes = { WebSecurityConfiguration.class }) @@ -80,18 +74,6 @@ public class PageTemplateRestTest { private static final String TEST_PASSWORD = "testPassword"; - static final ObjectMapper OBJECT_MAPPER; - - static { - // Workaround when Jackson is defined in shared library with different - // version and without artifact jackson-datatype-jsr310 - OBJECT_MAPPER = JsonMapper.builder() - .configure(JsonReadFeature.ALLOW_MISSING_VALUES, true) - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - .build(); - OBJECT_MAPPER.registerModule(new JavaTimeModule()); - } - @MockBean private PageTemplateService pageTemplateService; @@ -140,7 +122,7 @@ void getPageTemplateWithUser() throws Exception { @Test void createPageTemplateAnonymously() throws Exception { - ResultActions response = mockMvc.perform(post(REST_PATH).content(asJsonString(new PageTemplate())) + ResultActions response = mockMvc.perform(post(REST_PATH).content(toJsonString(new PageTemplate())) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); verifyNoInteractions(pageTemplateService); @@ -150,7 +132,7 @@ void createPageTemplateAnonymously() throws Exception { void createPageTemplateWithUser() throws Exception { PageTemplate pageTemplate = new PageTemplate(); ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) - .content(asJsonString(pageTemplate)) + .content(toJsonString(pageTemplate)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isOk()); verify(pageTemplateService).createPageTemplate(pageTemplate, SIMPLE_USER); @@ -162,14 +144,14 @@ void createPageTemplateWithUserForbidden() throws Exception { when(pageTemplateService.createPageTemplate(pageTemplate, SIMPLE_USER)).thenThrow(IllegalAccessException.class); ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) - .content(asJsonString(pageTemplate)) + .content(toJsonString(pageTemplate)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); } @Test void updatePageTemplateAnonymously() throws Exception { - ResultActions response = mockMvc.perform(put(REST_PATH + "/1").content(asJsonString(new PageTemplate())) + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").content(toJsonString(new PageTemplate())) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); verifyNoInteractions(pageTemplateService); @@ -179,7 +161,7 @@ void updatePageTemplateAnonymously() throws Exception { void updatePageTemplateWithUser() throws Exception { PageTemplate pageTemplate = new PageTemplate(); ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) - .content(asJsonString(pageTemplate)) + .content(toJsonString(pageTemplate)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isOk()); @@ -194,7 +176,7 @@ void updatePageTemplateWithUserForbidden() throws Exception { when(pageTemplateService.updatePageTemplate(pageTemplate, SIMPLE_USER)).thenThrow(IllegalAccessException.class); ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) - .content(asJsonString(pageTemplate)) + .content(toJsonString(pageTemplate)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); } @@ -206,7 +188,7 @@ void updatePageTemplateWithUserNotFound() throws Exception { when(pageTemplateService.updatePageTemplate(pageTemplate, SIMPLE_USER)).thenThrow(ObjectNotFoundException.class); ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) - .content(asJsonString(pageTemplate)) + .content(toJsonString(pageTemplate)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isNotFound()); } @@ -253,9 +235,4 @@ private RequestPostProcessor testSimpleUser() { .authorities(new SimpleGrantedAuthority("users")); } - @SneakyThrows - public static String asJsonString(final Object obj) { - return OBJECT_MAPPER.writeValueAsString(obj); - } - } diff --git a/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceCategoryRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceCategoryRestTest.java index 139861080..ed4280e60 100644 --- a/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceCategoryRestTest.java +++ b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceCategoryRestTest.java @@ -18,6 +18,7 @@ */ package io.meeds.layout.rest; +import static io.meeds.layout.util.JsonUtils.toJsonString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; @@ -51,12 +52,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import com.fasterxml.jackson.core.json.JsonReadFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - import org.exoplatform.commons.exception.ObjectNotFoundException; import io.meeds.layout.model.PortletInstanceCategory; @@ -65,7 +60,6 @@ import io.meeds.spring.web.security.WebSecurityConfiguration; import jakarta.servlet.Filter; -import lombok.SneakyThrows; @SpringBootTest(classes = { PortletInstanceCategoryRest.class, PortalAuthenticationManager.class, }) @ContextConfiguration(classes = { WebSecurityConfiguration.class }) @@ -80,18 +74,6 @@ public class PortletInstanceCategoryRestTest { private static final String TEST_PASSWORD = "testPassword"; - static final ObjectMapper OBJECT_MAPPER; - - static { - // Workaround when Jackson is defined in shared library with different - // version and without artifact jackson-datatype-jsr310 - OBJECT_MAPPER = JsonMapper.builder() - .configure(JsonReadFeature.ALLOW_MISSING_VALUES, true) - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - .build(); - OBJECT_MAPPER.registerModule(new JavaTimeModule()); - } - @MockBean private PortletInstanceService portletInstanceService; @@ -140,7 +122,7 @@ void getPortletInstanceCategoryWithUser() throws Exception { @Test void createPortletInstanceCategoryAnonymously() throws Exception { - ResultActions response = mockMvc.perform(post(REST_PATH).content(asJsonString(new PortletInstanceCategory())) + ResultActions response = mockMvc.perform(post(REST_PATH).content(toJsonString(new PortletInstanceCategory())) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); verifyNoInteractions(portletInstanceService); @@ -150,7 +132,7 @@ void createPortletInstanceCategoryAnonymously() throws Exception { void createPortletInstanceCategoryWithUser() throws Exception { PortletInstanceCategory portletInstanceCategory = new PortletInstanceCategory(); ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) - .content(asJsonString(portletInstanceCategory)) + .content(toJsonString(portletInstanceCategory)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isOk()); verify(portletInstanceService).createPortletInstanceCategory(portletInstanceCategory, SIMPLE_USER); @@ -163,14 +145,14 @@ void createPortletInstanceCategoryWithUserForbidden() throws Exception { SIMPLE_USER)).thenThrow(IllegalAccessException.class); ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) - .content(asJsonString(portletInstanceCategory)) + .content(toJsonString(portletInstanceCategory)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); } @Test void updatePortletInstanceCategoryAnonymously() throws Exception { - ResultActions response = mockMvc.perform(put(REST_PATH + "/1").content(asJsonString(new PortletInstanceCategory())) + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").content(toJsonString(new PortletInstanceCategory())) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); verifyNoInteractions(portletInstanceService); @@ -180,7 +162,7 @@ void updatePortletInstanceCategoryAnonymously() throws Exception { void updatePortletInstanceCategoryWithUser() throws Exception { PortletInstanceCategory portletInstanceCategory = new PortletInstanceCategory(); ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) - .content(asJsonString(portletInstanceCategory)) + .content(toJsonString(portletInstanceCategory)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isOk()); @@ -196,7 +178,7 @@ void updatePortletInstanceCategoryWithUserForbidden() throws Exception { SIMPLE_USER)).thenThrow(IllegalAccessException.class); ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) - .content(asJsonString(portletInstanceCategory)) + .content(toJsonString(portletInstanceCategory)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); } @@ -209,7 +191,7 @@ void updatePortletInstanceCategoryWithUserNotFound() throws Exception { SIMPLE_USER)).thenThrow(ObjectNotFoundException.class); ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) - .content(asJsonString(portletInstanceCategory)) + .content(toJsonString(portletInstanceCategory)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isNotFound()); } @@ -256,9 +238,4 @@ private RequestPostProcessor testSimpleUser() { .authorities(new SimpleGrantedAuthority("users")); } - @SneakyThrows - public static String asJsonString(final Object obj) { - return OBJECT_MAPPER.writeValueAsString(obj); - } - } diff --git a/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java index 0c41b0b84..4cb38905d 100644 --- a/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java +++ b/layout-service/src/test/java/io/meeds/layout/rest/PortletInstanceRestTest.java @@ -18,6 +18,7 @@ */ package io.meeds.layout.rest; +import static io.meeds.layout.util.JsonUtils.toJsonString; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; @@ -52,12 +53,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import com.fasterxml.jackson.core.json.JsonReadFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - import org.exoplatform.commons.exception.ObjectNotFoundException; import io.meeds.layout.model.PortletInstance; @@ -66,7 +61,6 @@ import io.meeds.spring.web.security.WebSecurityConfiguration; import jakarta.servlet.Filter; -import lombok.SneakyThrows; @SpringBootTest(classes = { PortletInstanceRest.class, PortalAuthenticationManager.class, }) @ContextConfiguration(classes = { WebSecurityConfiguration.class }) @@ -75,23 +69,11 @@ @ExtendWith(MockitoExtension.class) public class PortletInstanceRestTest { - private static final String REST_PATH = "/portlet/instances"; // NOSONAR - - private static final String SIMPLE_USER = "simple"; - - private static final String TEST_PASSWORD = "testPassword"; + private static final String REST_PATH = "/portlet/instances"; // NOSONAR - static final ObjectMapper OBJECT_MAPPER; + private static final String SIMPLE_USER = "simple"; - static { - // Workaround when Jackson is defined in shared library with different - // version and without artifact jackson-datatype-jsr310 - OBJECT_MAPPER = JsonMapper.builder() - .configure(JsonReadFeature.ALLOW_MISSING_VALUES, true) - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - .build(); - OBJECT_MAPPER.registerModule(new JavaTimeModule()); - } + private static final String TEST_PASSWORD = "testPassword"; @MockBean private PortletInstanceService portletInstanceService; @@ -141,7 +123,7 @@ void getPortletInstanceWithUser() throws Exception { @Test void createPortletInstanceAnonymously() throws Exception { - ResultActions response = mockMvc.perform(post(REST_PATH).content(asJsonString(new PortletInstance())) + ResultActions response = mockMvc.perform(post(REST_PATH).content(toJsonString(new PortletInstance())) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); verifyNoInteractions(portletInstanceService); @@ -151,7 +133,7 @@ void createPortletInstanceAnonymously() throws Exception { void createPortletInstanceWithUser() throws Exception { PortletInstance portletInstance = new PortletInstance(); ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) - .content(asJsonString(portletInstance)) + .content(toJsonString(portletInstance)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isOk()); verify(portletInstanceService).createPortletInstance(portletInstance, SIMPLE_USER); @@ -164,14 +146,14 @@ void createPortletInstanceWithUserForbidden() throws Exception { SIMPLE_USER)).thenThrow(IllegalAccessException.class); ResultActions response = mockMvc.perform(post(REST_PATH).with(testSimpleUser()) - .content(asJsonString(portletInstance)) + .content(toJsonString(portletInstance)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); } @Test void updatePortletInstanceAnonymously() throws Exception { - ResultActions response = mockMvc.perform(put(REST_PATH + "/1").content(asJsonString(new PortletInstance())) + ResultActions response = mockMvc.perform(put(REST_PATH + "/1").content(toJsonString(new PortletInstance())) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); verifyNoInteractions(portletInstanceService); @@ -181,7 +163,7 @@ void updatePortletInstanceAnonymously() throws Exception { void updatePortletInstanceWithUser() throws Exception { PortletInstance portletInstance = new PortletInstance(); ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) - .content(asJsonString(portletInstance)) + .content(toJsonString(portletInstance)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isOk()); @@ -197,7 +179,7 @@ void updatePortletInstanceWithUserForbidden() throws Exception { SIMPLE_USER)).thenThrow(IllegalAccessException.class); ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) - .content(asJsonString(portletInstance)) + .content(toJsonString(portletInstance)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isForbidden()); } @@ -210,7 +192,7 @@ void updatePortletInstanceWithUserNotFound() throws Exception { SIMPLE_USER)).thenThrow(ObjectNotFoundException.class); ResultActions response = mockMvc.perform(put(REST_PATH + "/1").with(testSimpleUser()) - .content(asJsonString(portletInstance)) + .content(toJsonString(portletInstance)) .contentType(MediaType.APPLICATION_JSON)); response.andExpect(status().isNotFound()); } @@ -257,9 +239,4 @@ private RequestPostProcessor testSimpleUser() { .authorities(new SimpleGrantedAuthority("users")); } - @SneakyThrows - public static String asJsonString(final Object obj) { - return OBJECT_MAPPER.writeValueAsString(obj); - } - } diff --git a/layout-service/src/test/java/io/meeds/layout/rest/PortletRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/PortletRestTest.java index f67798b2d..ab0a0731e 100644 --- a/layout-service/src/test/java/io/meeds/layout/rest/PortletRestTest.java +++ b/layout-service/src/test/java/io/meeds/layout/rest/PortletRestTest.java @@ -42,18 +42,11 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import com.fasterxml.jackson.core.json.JsonReadFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.json.JsonMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - import io.meeds.layout.service.PortletService; import io.meeds.spring.web.security.PortalAuthenticationManager; import io.meeds.spring.web.security.WebSecurityConfiguration; import jakarta.servlet.Filter; -import lombok.SneakyThrows; @SpringBootTest(classes = { PortletRest.class, PortalAuthenticationManager.class, }) @ContextConfiguration(classes = { WebSecurityConfiguration.class }) @@ -62,23 +55,11 @@ @ExtendWith(MockitoExtension.class) public class PortletRestTest { - private static final String REST_PATH = "/portlets"; // NOSONAR - - private static final String SIMPLE_USER = "simple"; - - private static final String TEST_PASSWORD = "testPassword"; + private static final String REST_PATH = "/portlets"; // NOSONAR - static final ObjectMapper OBJECT_MAPPER; + private static final String SIMPLE_USER = "simple"; - static { - // Workaround when Jackson is defined in shared library with different - // version and without artifact jackson-datatype-jsr310 - OBJECT_MAPPER = JsonMapper.builder() - .configure(JsonReadFeature.ALLOW_MISSING_VALUES, true) - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - .build(); - OBJECT_MAPPER.registerModule(new JavaTimeModule()); - } + private static final String TEST_PASSWORD = "testPassword"; @MockBean private PortletService portletService; @@ -141,9 +122,4 @@ private RequestPostProcessor testSimpleUser() { .authorities(new SimpleGrantedAuthority("users")); } - @SneakyThrows - public static String asJsonString(final Object obj) { - return OBJECT_MAPPER.writeValueAsString(obj); - } - } diff --git a/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java b/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java new file mode 100644 index 000000000..7382694b5 --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/rest/SiteLayoutRestTest.java @@ -0,0 +1,128 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2024 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.layout.rest; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +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 org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import org.exoplatform.commons.exception.ObjectNotFoundException; + +import io.meeds.layout.model.NodeLabel; +import io.meeds.layout.service.SiteLayoutService; +import io.meeds.spring.web.security.PortalAuthenticationManager; +import io.meeds.spring.web.security.WebSecurityConfiguration; + +import jakarta.servlet.Filter; + +@SpringBootTest(classes = { SiteLayoutRest.class, PortalAuthenticationManager.class, }) +@ContextConfiguration(classes = { WebSecurityConfiguration.class }) +@AutoConfigureWebMvc +@AutoConfigureMockMvc(addFilters = false) +@ExtendWith(MockitoExtension.class) +public class SiteLayoutRestTest { + + private static final String REST_PATH = "/sites"; // NOSONAR + + @MockBean + private SiteLayoutService siteLayoutService; + + @Autowired + private SecurityFilterChain filterChain; + + @Autowired + private WebApplicationContext context; + + @Mock + private NodeLabel nodeLabel; + + private MockMvc mockMvc; + + @BeforeEach + void setup() { + mockMvc = MockMvcBuilders.webAppContextSetup(context) + .addFilters(filterChain.getFilters().toArray(new Filter[0])) + .build(); + } + + @Test + void getSiteLabels() throws Exception { + doThrow(ObjectNotFoundException.class).when(siteLayoutService) + .getSiteLabels(2l, null); + + ResultActions response = mockMvc.perform(get(REST_PATH + "/2/labels")); + response.andExpect(status().isNotFound()); + + doThrow(IllegalAccessException.class).when(siteLayoutService) + .getSiteLabels(3l, null); + response = mockMvc.perform(get(REST_PATH + "/3/labels")); + response.andExpect(status().isForbidden()); + + doReturn(nodeLabel).when(siteLayoutService) + .getSiteLabels(4l, null); + when(nodeLabel.getDefaultLanguage()).thenReturn("en"); + + response = mockMvc.perform(get(REST_PATH + "/4/labels")); + response.andExpect(status().isOk()) + .andExpect(jsonPath("$.defaultLanguage").value("en")); + } + + @Test + void getSiteDescriptions() throws Exception { + doThrow(ObjectNotFoundException.class).when(siteLayoutService) + .getSiteDescriptions(2l, null); + + ResultActions response = mockMvc.perform(get(REST_PATH + "/2/descriptions")); + response.andExpect(status().isNotFound()); + + doThrow(IllegalAccessException.class).when(siteLayoutService) + .getSiteDescriptions(3l, null); + response = mockMvc.perform(get(REST_PATH + "/3/descriptions")); + response.andExpect(status().isForbidden()); + + doReturn(nodeLabel).when(siteLayoutService) + .getSiteDescriptions(4l, null); + when(nodeLabel.getDefaultLanguage()).thenReturn("en"); + + response = mockMvc.perform(get(REST_PATH + "/4/descriptions")); + response.andExpect(status().isOk()) + .andExpect(jsonPath("$.defaultLanguage").value("en")); + } + +} diff --git a/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java b/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java index 5c1ae9dc3..4ac06435a 100644 --- a/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java +++ b/layout-service/src/test/java/io/meeds/layout/service/SiteLayoutServiceTest.java @@ -19,7 +19,9 @@ package io.meeds.layout.service; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doAnswer; @@ -31,6 +33,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Locale; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -42,11 +45,17 @@ import org.exoplatform.commons.ObjectAlreadyExistsException; import org.exoplatform.commons.exception.ObjectNotFoundException; +import org.exoplatform.portal.config.UserPortalConfig; import org.exoplatform.portal.config.UserPortalConfigService; import org.exoplatform.portal.config.model.PortalConfig; import org.exoplatform.portal.mop.SiteKey; import org.exoplatform.portal.mop.service.LayoutService; +import org.exoplatform.portal.mop.user.UserPortal; +import org.exoplatform.services.resources.LocaleConfig; +import org.exoplatform.services.resources.LocaleConfigService; +import org.exoplatform.services.resources.impl.LocaleConfigImpl; +import io.meeds.layout.model.NodeLabel; import io.meeds.layout.model.PermissionUpdateModel; import io.meeds.layout.model.SiteCreateModel; import io.meeds.layout.model.SiteUpdateModel; @@ -54,7 +63,7 @@ import lombok.SneakyThrows; @SpringBootTest(classes = { - SiteLayoutService.class, + SiteLayoutService.class, }) @ExtendWith(MockitoExtension.class) public class SiteLayoutServiceTest { @@ -72,6 +81,9 @@ public class SiteLayoutServiceTest { @MockBean private LayoutAclService aclService; + @MockBean + private LocaleConfigService localeConfigService; + @Autowired private SiteLayoutService siteLayoutService; @@ -220,4 +232,86 @@ public void updateSitePermissions() { portalConfig.setEditPermission(argThat(editPermission::equals)); } + @Test + @SneakyThrows + public void getSiteLabels() { + assertThrows(ObjectNotFoundException.class, () -> siteLayoutService.getSiteLabels(2l, TEST_USER)); + when(layoutService.getPortalConfig(2l)).thenReturn(portalConfig); + when(portalConfig.getType()).thenReturn("PORTAL"); + when(portalConfig.getName()).thenReturn("test"); + + assertThrows(IllegalAccessException.class, () -> siteLayoutService.getSiteLabels(2l, TEST_USER)); + when(aclService.canViewSite(new SiteKey(portalConfig.getType(), portalConfig.getName()), + TEST_USER)) + .thenReturn(true); + + LocaleConfig enLocaleConfig = new LocaleConfigImpl(); + enLocaleConfig.setLocale(Locale.ENGLISH); + LocaleConfig frLocaleConfig = new LocaleConfigImpl(); + frLocaleConfig.setLocale(Locale.FRENCH); + when(localeConfigService.getLocalConfigs()).thenReturn(Arrays.asList(enLocaleConfig, frLocaleConfig)); + UserPortalConfig userPortalConfig = mock(UserPortalConfig.class); + UserPortal userPortal = mock(UserPortal.class); + when(userPortalConfig.getUserPortal()).thenReturn(userPortal); + when(portalConfigService.getUserPortalConfig("test", TEST_USER)).thenReturn(userPortalConfig); + + SiteKey siteKey = SiteKey.portal("test"); + when(userPortal.getPortalLabel(siteKey, enLocaleConfig.getLocale())).thenReturn("Test Name"); + when(userPortal.getPortalLabel(siteKey, frLocaleConfig.getLocale())).thenReturn("Nom pour le test"); + + NodeLabel siteLabels = siteLayoutService.getSiteLabels(2l, TEST_USER); + assertNotNull(siteLabels); + assertNotNull(siteLabels.getLabels()); + assertNotNull(siteLabels.getSupportedLanguages()); + assertNotNull(siteLabels.getDefaultLanguage()); + assertEquals("en", siteLabels.getDefaultLanguage()); + assertEquals(2, siteLabels.getSupportedLanguages().size()); + assertTrue(siteLabels.getSupportedLanguages().containsKey(enLocaleConfig.getLocale().getLanguage())); + assertTrue(siteLabels.getSupportedLanguages().containsKey(frLocaleConfig.getLocale().getLanguage())); + assertEquals(2, siteLabels.getLabels().size()); + assertEquals("Test Name", siteLabels.getLabels().get(enLocaleConfig.getLocale().getLanguage())); + assertEquals("Nom pour le test", siteLabels.getLabels().get(frLocaleConfig.getLocale().getLanguage())); + } + + @Test + @SneakyThrows + public void getSiteDescriptions() { + assertThrows(ObjectNotFoundException.class, () -> siteLayoutService.getSiteDescriptions(2l, TEST_USER)); + when(layoutService.getPortalConfig(2l)).thenReturn(portalConfig); + when(portalConfig.getType()).thenReturn("PORTAL"); + when(portalConfig.getName()).thenReturn("test"); + + assertThrows(IllegalAccessException.class, () -> siteLayoutService.getSiteDescriptions(2l, TEST_USER)); + when(aclService.canViewSite(new SiteKey(portalConfig.getType(), portalConfig.getName()), + TEST_USER)) + .thenReturn(true); + + LocaleConfig enLocaleConfig = new LocaleConfigImpl(); + enLocaleConfig.setLocale(Locale.ENGLISH); + LocaleConfig frLocaleConfig = new LocaleConfigImpl(); + frLocaleConfig.setLocale(Locale.FRENCH); + when(localeConfigService.getLocalConfigs()).thenReturn(Arrays.asList(enLocaleConfig, frLocaleConfig)); + UserPortalConfig userPortalConfig = mock(UserPortalConfig.class); + UserPortal userPortal = mock(UserPortal.class); + when(userPortalConfig.getUserPortal()).thenReturn(userPortal); + when(portalConfigService.getUserPortalConfig("test", TEST_USER)).thenReturn(userPortalConfig); + + SiteKey siteKey = SiteKey.portal("test"); + when(userPortal.getPortalDescription(siteKey, enLocaleConfig.getLocale())).thenReturn("Test Description"); + when(userPortal.getPortalDescription(siteKey, frLocaleConfig.getLocale())).thenReturn("Description pour le test"); + + NodeLabel siteLabels = siteLayoutService.getSiteDescriptions(2l, TEST_USER); + assertNotNull(siteLabels); + assertNotNull(siteLabels.getLabels()); + assertNotNull(siteLabels.getSupportedLanguages()); + assertNotNull(siteLabels.getDefaultLanguage()); + assertEquals("en", siteLabels.getDefaultLanguage()); + assertEquals(2, siteLabels.getSupportedLanguages().size()); + assertTrue(siteLabels.getSupportedLanguages().containsKey(enLocaleConfig.getLocale().getLanguage())); + assertTrue(siteLabels.getSupportedLanguages().containsKey(frLocaleConfig.getLocale().getLanguage())); + assertEquals(2, siteLabels.getLabels().size()); + assertEquals("Test Description", siteLabels.getLabels().get(enLocaleConfig.getLocale().getLanguage())); + assertEquals("Description pour le test", siteLabels.getLabels().get(frLocaleConfig.getLocale().getLanguage())); + } + } diff --git a/layout-webapp/src/main/webapp/vue-app/common/js/SiteLayoutService.js b/layout-webapp/src/main/webapp/vue-app/common/js/SiteLayoutService.js index 1618e390a..55a739abf 100644 --- a/layout-webapp/src/main/webapp/vue-app/common/js/SiteLayoutService.js +++ b/layout-webapp/src/main/webapp/vue-app/common/js/SiteLayoutService.js @@ -83,6 +83,32 @@ export function getSite(siteType, siteName, lang) { }); } +export function getSiteLabels(siteId) { + return fetch(`/layout/rest/sites/${siteId}/labels`, { + credentials: 'include', + method: 'GET' + }).then(resp => { + if (resp?.ok) { + return resp.json(); + } else { + throw new Error('Error when retrieving site labels'); + } + }); +} + +export function getSiteDescriptions(siteId) { + return fetch(`/layout/rest/sites/${siteId}/descriptions`, { + credentials: 'include', + method: 'GET' + }).then(resp => { + if (resp?.ok) { + return resp.json(); + } else { + throw new Error('Error when retrieving site descriptions'); + } + }); +} + export function updateSite(siteName, siteType, siteLabel, siteDescription, displayed, displayOrder, bannerUploadId, bannerRemoved) { const updateModel = { siteType, diff --git a/layout-webapp/src/main/webapp/vue-app/site-management/components/SitePropertiesDrawer.vue b/layout-webapp/src/main/webapp/vue-app/site-management/components/SitePropertiesDrawer.vue index 8bcd34e9a..f6121055c 100644 --- a/layout-webapp/src/main/webapp/vue-app/site-management/components/SitePropertiesDrawer.vue +++ b/layout-webapp/src/main/webapp/vue-app/site-management/components/SitePropertiesDrawer.vue @@ -19,17 +19,18 @@