diff --git a/pom.xml b/pom.xml index 56738815..0747624b 100644 --- a/pom.xml +++ b/pom.xml @@ -161,6 +161,12 @@ + + + com.powsybl + powsybl-ws-commons + 1.30.0 + org.gridsuite diff --git a/src/main/java/org/gridsuite/directory/server/DirectoryException.java b/src/main/java/org/gridsuite/directory/server/DirectoryException.java deleted file mode 100644 index a5f585b1..00000000 --- a/src/main/java/org/gridsuite/directory/server/DirectoryException.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2021, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.gridsuite.directory.server; - -import lombok.NonNull; - -import java.util.Objects; -import java.util.UUID; - -/** - * @author Abdelsalem Hedhili - */ -public class DirectoryException extends RuntimeException { - - private final Type type; - - public DirectoryException(Type type) { - super(Objects.requireNonNull(type.name())); - this.type = type; - } - - public DirectoryException(Type type, String message) { - super(message); - this.type = type; - } - - public static DirectoryException createNotificationUnknown(@NonNull String action) { - return new DirectoryException(Type.UNKNOWN_NOTIFICATION, String.format("The notification type '%s' is unknown", action)); - } - - public static DirectoryException createElementNotFound(@NonNull String type, @NonNull UUID uuid) { - return new DirectoryException(Type.NOT_FOUND, String.format("%s '%s' not found !", type, uuid)); - } - - public static DirectoryException createElementNameAlreadyExists(@NonNull String name) { - return new DirectoryException(Type.NAME_ALREADY_EXISTS, String.format("Element with the same name '%s' already exists in the directory !", name)); - } - - Type getType() { - return type; - } - - public enum Type { - NOT_ALLOWED, - NOT_FOUND, - NOT_DIRECTORY, - IS_DIRECTORY, - UNKNOWN_NOTIFICATION, - NAME_ALREADY_EXISTS, - MOVE_IN_DESCENDANT_NOT_ALLOWED, - } -} diff --git a/src/main/java/org/gridsuite/directory/server/DirectoryService.java b/src/main/java/org/gridsuite/directory/server/DirectoryService.java index 36cc7c8b..e395c89a 100644 --- a/src/main/java/org/gridsuite/directory/server/DirectoryService.java +++ b/src/main/java/org/gridsuite/directory/server/DirectoryService.java @@ -9,6 +9,7 @@ import lombok.NonNull; import org.gridsuite.directory.server.dto.*; import org.gridsuite.directory.server.dto.elasticsearch.DirectoryElementInfos; +import org.gridsuite.directory.server.error.DirectoryException; import org.gridsuite.directory.server.repository.*; import org.gridsuite.directory.server.services.*; import org.springframework.dao.DataIntegrityViolationException; @@ -24,8 +25,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.gridsuite.directory.server.error.DirectoryBusinessErrorCode.*; import static java.lang.Boolean.TRUE; -import static org.gridsuite.directory.server.DirectoryException.Type.*; import static org.gridsuite.directory.server.dto.ElementAttributes.toElementAttributes; import static org.gridsuite.directory.server.dto.PermissionType.*; @@ -94,7 +95,7 @@ public ElementAttributes createElement(ElementAttributes elementAttributes, UUID private ElementAttributes createElementWithNotif(ElementAttributes elementAttributes, UUID parentDirectoryUuid, String userId, boolean generateNewName) { if (elementAttributes.getElementName().isBlank()) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(DIRECTORY_ELEMENT_NAME_BLANK, "Element name must not be blank"); } assertDirectoryExist(parentDirectoryUuid); DirectoryElementEntity elementEntity = insertElement(elementAttributes, parentDirectoryUuid, userId, generateNewName); @@ -111,16 +112,17 @@ private ElementAttributes createElementWithNotif(ElementAttributes elementAttrib } public ElementAttributes duplicateElement(UUID elementId, UUID newElementId, UUID targetDirectoryId, String userId) { - DirectoryElementEntity directoryElementEntity = directoryElementRepository.findById(elementId).orElseThrow(() -> new DirectoryException(NOT_FOUND)); + DirectoryElementEntity directoryElementEntity = directoryElementRepository.findById(elementId) + .orElseThrow(() -> DirectoryException.createElementNotFound(ELEMENT, elementId)); String elementType = directoryElementEntity.getType(); UUID parentDirectoryUuid = targetDirectoryId != null ? targetDirectoryId : directoryElementEntity.getParentId(); ElementAttributes elementAttributes = ElementAttributes.builder() - .type(elementType) - .elementUuid(newElementId) - .owner(userId) - .description(directoryElementEntity.getDescription()) - .elementName(directoryElementEntity.getName()) - .build(); + .type(elementType) + .elementUuid(newElementId) + .owner(userId) + .description(directoryElementEntity.getDescription()) + .elementName(directoryElementEntity.getName()) + .build(); assertDirectoryExist(parentDirectoryUuid); DirectoryElementEntity elementEntity = insertElement(elementAttributes, parentDirectoryUuid, userId, true); @@ -132,14 +134,14 @@ public ElementAttributes duplicateElement(UUID elementId, UUID newElementId, UUI } private void assertRootDirectoryNotExist(String rootName) { - if (TRUE.equals(repositoryService.isRootDirectoryExist(rootName))) { - throw new DirectoryException(NOT_ALLOWED); + if (repositoryService.isRootDirectoryExist(rootName)) { + throw DirectoryException.of(DIRECTORY_ELEMENT_NAME_CONFLICT, "Root directory '%s' already exists", rootName); } } private void assertDirectoryExist(UUID dirUuid) { if (!getElement(dirUuid).getType().equals(DIRECTORY)) { - throw new DirectoryException(NOT_DIRECTORY); + throw DirectoryException.of(DIRECTORY_NOT_DIRECTORY, "Element '%s' is not a directory", dirUuid); } } @@ -152,14 +154,14 @@ private DirectoryElementEntity insertElement(ElementAttributes elementAttributes Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS); DirectoryElementEntity elementEntity = new DirectoryElementEntity(elementAttributes.getElementUuid() == null ? UUID.randomUUID() : elementAttributes.getElementUuid(), - parentDirectoryUuid, - elementAttributes.getElementName(), - elementAttributes.getType(), - elementAttributes.getOwner(), - elementAttributes.getDescription(), - now, - now, - elementAttributes.getOwner()); + parentDirectoryUuid, + elementAttributes.getElementName(), + elementAttributes.getType(), + elementAttributes.getOwner(), + elementAttributes.getDescription(), + now, + now, + elementAttributes.getOwner()); return tryInsertElement(elementEntity, parentDirectoryUuid, userId, generateNewName); } @@ -192,7 +194,7 @@ public ElementAttributes createRootDirectory(RootDirectoryAttributes rootDirecto private ElementAttributes createRootDirectoryWithNotif(RootDirectoryAttributes rootDirectoryAttributes, String userId) { if (rootDirectoryAttributes.getElementName().isBlank()) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(DIRECTORY_ELEMENT_NAME_BLANK, "Root directory name must not be blank"); } assertRootDirectoryNotExist(rootDirectoryAttributes.getElementName()); @@ -202,12 +204,12 @@ private ElementAttributes createRootDirectoryWithNotif(RootDirectoryAttributes r insertReadGlobalUsersPermission(elementUuid); // here we know a root directory has no parent notificationService.emitDirectoryChanged( - elementUuid, - elementAttributes.getElementName(), - userId, - null, - true, - NotificationType.ADD_DIRECTORY + elementUuid, + elementAttributes.getElementName(), + userId, + null, + true, + NotificationType.ADD_DIRECTORY ); return elementAttributes; } @@ -226,20 +228,20 @@ public void createElementInDirectoryPath(String directoryPath, ElementAttributes //we create the root directory if it doesn't exist if (parentDirectoryUuid == null) { parentDirectoryUuid = createRootDirectoryWithNotif( - new RootDirectoryAttributes( - s, - userId, - null, - now, - now, - userId), - userId).getElementUuid(); + new RootDirectoryAttributes( + s, + userId, + null, + now, + now, + userId), + userId).getElementUuid(); } else { //and then we create the rest of the path parentDirectoryUuid = createElementWithNotif( - toElementAttributes(UUID.randomUUID(), s, DIRECTORY, userId, null, now, now, userId), - parentDirectoryUuid, - userId, false).getElementUuid(); + toElementAttributes(UUID.randomUUID(), s, DIRECTORY, userId, null, now, now, userId), + parentDirectoryUuid, + userId, false).getElementUuid(); } } else { parentDirectoryUuid = currentDirectoryUuid; @@ -251,11 +253,11 @@ public void createElementInDirectoryPath(String directoryPath, ElementAttributes private Map getSubDirectoriesCounts(List subDirectories, List types, String userId) { List readableSubDirectories = subDirectories.stream().filter(dirId -> hasReadPermissions(userId, List.of(dirId))).toList(); return repositoryService.findAllByParentIdInAndTypeIn(readableSubDirectories, types).stream() - .filter(child -> hasReadPermissions(userId, List.of(child.getId()))) - .collect(Collectors.groupingBy( - DirectoryElementRepository.ElementParentage::getParentId, - Collectors.counting() - )); + .filter(child -> hasReadPermissions(userId, List.of(child.getId()))) + .collect(Collectors.groupingBy( + DirectoryElementRepository.ElementParentage::getParentId, + Collectors.counting() + )); } public List getDirectoryElements(UUID directoryUuid, List types, Boolean recursive, String userId) { @@ -272,10 +274,10 @@ public List getDirectoryElements(UUID directoryUuid, List descendents = repositoryService.findAllDescendants(directoryUuid).stream().toList(); return descendents - .stream() - .filter(e -> (types.isEmpty() || types.contains(e.getType())) && hasReadPermissions(userId, List.of(e.getId()))) - .map(ElementAttributes::toElementAttributes) - .toList(); + .stream() + .filter(e -> (types.isEmpty() || types.contains(e.getType())) && hasReadPermissions(userId, List.of(e.getId()))) + .map(ElementAttributes::toElementAttributes) + .toList(); } else { return getAllDirectoryElementsStream(directoryUuid, types, userId).toList(); } @@ -283,16 +285,16 @@ public List getDirectoryElements(UUID directoryUuid, List getOnlyElementsStream(UUID directoryUuid, List types, String userId) { return getAllDirectoryElementsStream(directoryUuid, types, userId) - .filter(elementAttributes -> !elementAttributes.getType().equals(DIRECTORY)); + .filter(elementAttributes -> !elementAttributes.getType().equals(DIRECTORY)); } private Stream getAllDirectoryElementsStream(UUID directoryUuid, List types, String userId) { List directoryElements = repositoryService.findAllByParentId(directoryUuid); Map subdirectoriesCountsMap = getSubDirectoriesCountsMap(types, directoryElements, userId); return directoryElements - .stream() - .filter(e -> (e.getType().equals(DIRECTORY) || types.isEmpty() || types.contains(e.getType())) && hasReadPermissions(userId, List.of(e.getId()))) - .map(e -> toElementAttributes(e, subdirectoriesCountsMap.getOrDefault(e.getId(), 0L))); + .stream() + .filter(e -> (e.getType().equals(DIRECTORY) || types.isEmpty() || types.contains(e.getType())) && hasReadPermissions(userId, List.of(e.getId()))) + .map(e -> toElementAttributes(e, subdirectoriesCountsMap.getOrDefault(e.getId(), 0L))); } public List getRootDirectories(List types, String userId) { @@ -304,8 +306,8 @@ public List getRootDirectories(List types, String use } Map subdirectoriesCountsMap = getSubDirectoriesCountsMap(types, directoryElements, userId); return directoryElements.stream() - .map(e -> toElementAttributes(e, subdirectoriesCountsMap.getOrDefault(e.getId(), 0L))) - .toList(); + .map(e -> toElementAttributes(e, subdirectoriesCountsMap.getOrDefault(e.getId(), 0L))) + .toList(); } private Map getSubDirectoriesCountsMap(List types, List directoryElements, String userId) { @@ -316,8 +318,10 @@ public void updateElement(UUID elementUuid, ElementAttributes newElementAttribut DirectoryElementEntity directoryElement = getDirectoryElementEntity(elementUuid); if (!directoryElement.isAttributesUpdatable(newElementAttributes, userId) || !directoryElement.getName().equals(newElementAttributes.getElementName()) && - directoryHasElementOfNameAndType(directoryElement.getParentId(), newElementAttributes.getElementName(), directoryElement.getType(), userId)) { - throw new DirectoryException(NOT_ALLOWED); + directoryHasElementOfNameAndType(directoryElement.getParentId(), newElementAttributes.getElementName(), directoryElement.getType(), userId)) { + throw DirectoryException.of(DIRECTORY_PERMISSION_DENIED, + "Update forbidden for element '%s': invalid permissions or duplicate name", + directoryElement.getId()); } DirectoryElementEntity elementEntity = repositoryService.saveElement(directoryElement.update(newElementAttributes)); @@ -334,10 +338,6 @@ public void updateElementLastModifiedAttributes(UUID elementUuid, Instant lastMo @Transactional public void moveElementsDirectory(List elementsUuids, UUID newDirectoryUuid, String userId) { - if (elementsUuids.isEmpty()) { - throw new DirectoryException(NOT_ALLOWED); - } - validateNewDirectory(newDirectoryUuid); elementsUuids.forEach(elementUuid -> moveElementDirectory(getDirectoryElementEntity(elementUuid), newDirectoryUuid, userId)); @@ -374,7 +374,9 @@ private void moveElementDirectory(DirectoryElementEntity element, UUID newDirect private void validateElementForMove(DirectoryElementEntity element, UUID newDirectoryUuid, Set descendentsUuids, String userId) { if (newDirectoryUuid == element.getId() || descendentsUuids.contains(newDirectoryUuid)) { - throw new DirectoryException(MOVE_IN_DESCENDANT_NOT_ALLOWED); + throw DirectoryException.of(DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED, + "Cannot move element '%s' into one of its descendants", + element.getId()); } if (directoryHasElementOfNameAndType(newDirectoryUuid, element.getName(), element.getType(), userId)) { @@ -389,10 +391,10 @@ private void updateElementParentDirectory(DirectoryElementEntity element, UUID n private void validateNewDirectory(UUID newDirectoryUuid) { DirectoryElementEntity newDirectory = repositoryService.getElementEntity(newDirectoryUuid) - .orElseThrow(() -> DirectoryException.createElementNotFound(DIRECTORY, newDirectoryUuid)); + .orElseThrow(() -> DirectoryException.createElementNotFound(DIRECTORY, newDirectoryUuid)); if (!newDirectory.getType().equals(DIRECTORY)) { - throw new DirectoryException(NOT_DIRECTORY); + throw DirectoryException.of(DIRECTORY_NOT_DIRECTORY, "Target '%s' is not a directory", newDirectoryUuid); } } @@ -437,9 +439,10 @@ private void deleteSubElements(UUID elementUuid, String userId) { /** * Method to delete multiple elements within a single repository - DIRECTORIES can't be deleted this way - * @param elementsUuids list of elements uuids to delete + * + * @param elementsUuids list of elements uuids to delete * @param parentDirectoryUuid expected parent uuid of each element - element with another parent UUID won't be deleted - * @param userId user making the deletion + * @param userId user making the deletion */ public void deleteElements(List elementsUuids, UUID parentDirectoryUuid, String userId) { // getting elements by "elementUuids", filtered if they don't belong to parentDirectoryUuid, or if they are directories @@ -473,7 +476,8 @@ public List getPath(UUID elementUuid) { } public String getElementName(UUID elementUuid) { - DirectoryElementEntity element = repositoryService.getElementEntity(elementUuid).orElseThrow(() -> new DirectoryException(NOT_FOUND)); + DirectoryElementEntity element = repositoryService.getElementEntity(elementUuid) + .orElseThrow(() -> DirectoryException.createElementNotFound(ELEMENT, elementUuid)); return element.getName(); } @@ -505,19 +509,19 @@ public List getElements(List ids, boolean strictMode, L //if the user is not an admin we filter out elements he doesn't have the permission on if (!roleService.isUserExploreAdmin()) { elementEntities = elementEntities.stream().filter(directoryElementEntity -> - hasReadPermissions(userId, List.of(directoryElementEntity.getId())) + hasReadPermissions(userId, List.of(directoryElementEntity.getId())) ).toList(); } if (strictMode && elementEntities.size() != ids.stream().distinct().count()) { - throw new DirectoryException(NOT_FOUND); + throw DirectoryException.of(DIRECTORY_SOME_ELEMENTS_ARE_MISSING, "Some requested elements are missing"); } Map subElementsCount = getSubDirectoriesCounts(elementEntities.stream().map(DirectoryElementEntity::getId).toList(), types, userId); return elementEntities.stream() - .map(attribute -> toElementAttributes(attribute, subElementsCount.getOrDefault(attribute.getId(), 0L))) - .toList(); + .map(attribute -> toElementAttributes(attribute, subElementsCount.getOrDefault(attribute.getId(), 0L))) + .toList(); } public int getCasesCount(String userId) { @@ -525,12 +529,7 @@ public int getCasesCount(String userId) { } public void notify(@NonNull String notificationName, @NonNull UUID elementUuid, @NonNull String userId) { - NotificationType notification; - try { - notification = NotificationType.valueOf(notificationName.toUpperCase()); - } catch (IllegalArgumentException e) { - throw DirectoryException.createNotificationUnknown(notificationName); - } + NotificationType notification = NotificationType.valueOf(notificationName.toUpperCase()); if (notification == NotificationType.UPDATE_DIRECTORY) { ElementAttributes elementAttributes = getElement(elementUuid); @@ -538,7 +537,7 @@ public void notify(@NonNull String notificationName, @NonNull UUID elementUuid, notifyDirectoryHasChanged(parentUuid != null ? parentUuid : elementUuid, userId, elementAttributes.getElementName()); } else { - throw DirectoryException.createNotificationUnknown(notification.name()); + throw new IllegalArgumentException(String.format("The notification type '%s' is unknown", notification.name())); } } @@ -548,7 +547,7 @@ private String nameCandidate(String elementName, int n) { public String getDuplicateNameCandidate(UUID directoryUuid, String elementName, String elementType, String userId) { if (!repositoryService.canRead(directoryUuid, userId)) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(DIRECTORY_PERMISSION_DENIED, "User '%s' cannot access directory '%s'", userId, directoryUuid); } var idLikes = new HashSet<>(repositoryService.getNameByTypeAndParentIdAndNameStartWith(elementType, directoryUuid, elementName)); if (!idLikes.contains(elementName)) { @@ -572,7 +571,7 @@ public UUID getDirectoryUuidFromPath(List directoryPath) { for (String s : directoryPath) { UUID currentDirectoryUuid = getDirectoryUuid(s, parentDirectoryUuid); if (currentDirectoryUuid == null) { - throw new DirectoryException(NOT_FOUND); + throw DirectoryException.of(DIRECTORY_ELEMENT_NOT_FOUND, "Directory '%s' not found in path", s); } else { parentDirectoryUuid = currentDirectoryUuid; } @@ -605,13 +604,13 @@ private void notifyDirectoryHasChanged(UUID directoryUuid, String userId, String private void notifyDirectoryHasChanged(UUID directoryUuid, String userId, String elementName, String error, boolean isDirectoryMoving) { Objects.requireNonNull(directoryUuid); notificationService.emitDirectoryChanged( - directoryUuid, - elementName, - userId, - error, - repositoryService.isRootDirectory(directoryUuid), - isDirectoryMoving, - NotificationType.UPDATE_DIRECTORY + directoryUuid, + elementName, + userId, + error, + repositoryService.isRootDirectory(directoryUuid), + isDirectoryMoving, + NotificationType.UPDATE_DIRECTORY ); } @@ -629,13 +628,13 @@ private void notifyRootDirectoryDeleted(UUID rootDirectoryUuid, String userId, S private void notifyRootDirectoryDeleted(UUID rootDirectoryUuid, String userId, String elementName, String error, boolean isDirectoryMoving) { Objects.requireNonNull(rootDirectoryUuid); notificationService.emitDirectoryChanged( - rootDirectoryUuid, - elementName, - userId, - error, - true, - isDirectoryMoving, - NotificationType.DELETE_DIRECTORY + rootDirectoryUuid, + elementName, + userId, + error, + true, + isDirectoryMoving, + NotificationType.DELETE_DIRECTORY ); } @@ -643,15 +642,15 @@ private void notifyRootDirectoryDeleted(UUID rootDirectoryUuid, String userId, S * Checks if a user has the specified permission on given elements. * Checks parent permissions first, then target directory, then child permissions if recursive check is enabled. * - * @param userId User ID checking permissions for - * @param elementUuids List of element UUIDs to check permissions on + * @param userId User ID checking permissions for + * @param elementUuids List of element UUIDs to check permissions on * @param targetDirectoryUuid Optional target directory UUID (for move operations) - * @param permissionType Type of permission to check (READ, WRITE, MANAGE) - * @param recursiveCheck Whether to check permissions recursively on children + * @param permissionType Type of permission to check (READ, WRITE, MANAGE) + * @param recursiveCheck Whether to check permissions recursively on children * @return PermissionCheckResult indicating where permission check failed, or ALLOWED if successful */ public PermissionCheckResult checkDirectoriesPermission(String userId, List elementUuids, UUID targetDirectoryUuid, - PermissionType permissionType, boolean recursiveCheck) { + PermissionType permissionType, boolean recursiveCheck) { return switch (permissionType) { case READ -> checkReadPermission(userId, elementUuids); case WRITE -> checkWritePermission(userId, elementUuids, targetDirectoryUuid, recursiveCheck); @@ -662,14 +661,14 @@ public PermissionCheckResult checkDirectoriesPermission(String userId, List elementUuids) { return hasReadPermissions(userId, elementUuids) ? - PermissionCheckResult.ALLOWED : - PermissionCheckResult.PARENT_PERMISSION_DENIED; + PermissionCheckResult.ALLOWED : + PermissionCheckResult.PARENT_PERMISSION_DENIED; } private PermissionCheckResult checkManagePermission(String userId, List elementUuids) { return hasManagePermission(userId, elementUuids) ? - PermissionCheckResult.ALLOWED : - PermissionCheckResult.PARENT_PERMISSION_DENIED; + PermissionCheckResult.ALLOWED : + PermissionCheckResult.PARENT_PERMISSION_DENIED; } private PermissionCheckResult checkWritePermission(String userId, List elementUuids, UUID targetDirectoryUuid, boolean recursiveCheck) { @@ -693,9 +692,9 @@ private PermissionCheckResult checkWritePermission(String userId, List ele for (DirectoryElementEntity element : elements) { if (element.getType().equals(DIRECTORY)) { List descendantsUuids = repositoryService.findAllDescendants(element.getId()) - .stream() - .filter(e -> e.getType().equals(DIRECTORY)) - .map(DirectoryElementEntity::getId).toList(); + .stream() + .filter(e -> e.getType().equals(DIRECTORY)) + .map(DirectoryElementEntity::getId).toList(); if (!descendantsUuids.isEmpty() && !checkPermission(userId, descendantsUuids, WRITE)) { return PermissionCheckResult.CHILD_PERMISSION_DENIED; } @@ -709,8 +708,8 @@ private PermissionCheckResult checkWritePermission(String userId, List ele public boolean hasReadPermissions(String userId, List elementUuids) { return roleService.isUserExploreAdmin() || directoryElementRepository.findAllByIdIn(elementUuids).stream().allMatch(element -> - //If it's a directory we check its own write permission else we check the permission on the element parent directory - checkPermission(userId, List.of(element.getType().equals(DIRECTORY) ? element.getId() : element.getParentId()), READ) + //If it's a directory we check its own write permission else we check the permission on the element parent directory + checkPermission(userId, List.of(element.getType().equals(DIRECTORY) ? element.getId() : element.getParentId()), READ) ); } @@ -728,22 +727,22 @@ private boolean checkPermission(String userId, List elementUuids, Permissi } //Finally check group permission return userAdminService.getUserGroups(userId) - .stream() - .map(UserGroupDTO::id) - .anyMatch(groupId -> - checkPermission(permissionRepository.findById(new PermissionId(uuid, "", groupId.toString())), permissionType) - ); + .stream() + .map(UserGroupDTO::id) + .anyMatch(groupId -> + checkPermission(permissionRepository.findById(new PermissionId(uuid, "", groupId.toString())), permissionType) + ); }); } private boolean checkPermission(Optional permissionEntity, PermissionType permissionType) { return permissionEntity - .map(p -> switch (permissionType) { - case READ -> Boolean.TRUE.equals(p.getRead()); - case WRITE -> Boolean.TRUE.equals(p.getWrite()); - case MANAGE -> Boolean.TRUE.equals(p.getManage()); - }) - .orElse(false); + .map(p -> switch (permissionType) { + case READ -> Boolean.TRUE.equals(p.getRead()); + case WRITE -> Boolean.TRUE.equals(p.getWrite()); + case MANAGE -> Boolean.TRUE.equals(p.getManage()); + }) + .orElse(false); } private boolean hasManagePermission(String userId, List elementUuids) { @@ -789,7 +788,7 @@ private void addPermissionForGroup(UUID elementUuid, String groupId, PermissionT public void validatePermissionsGetAccess(UUID directoryUuid, String userId) { if (!hasReadPermissions(userId, List.of(directoryUuid))) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(DIRECTORY_PERMISSION_DENIED, "User '%s' is not allowed to view directory '%s'", userId, directoryUuid); } } @@ -798,7 +797,7 @@ public void validatePermissionsGetAccess(UUID directoryUuid, String userId) { * Returns exactly one PermissionDTO for each permission type (READ, WRITE, MANAGE). * * @param directoryUuid The UUID of the directory - * @param userId The ID of the user requesting the permissions + * @param userId The ID of the user requesting the permissions * @return A list of exactly three permission DTOs (READ, WRITE, MANAGE) * @throws DirectoryException if the user doesn't have access or the directory doesn't exist */ @@ -813,8 +812,8 @@ public List getDirectoryPermissions(UUID directoryUuid, String us Map groupPermissionLevels = extractGroupPermissionLevels(permissions); return Arrays.stream(PermissionType.values()) - .map(type -> createPermissionDto(type, allUsersPermissionLevel, groupPermissionLevels)) - .collect(Collectors.toList()); + .map(type -> createPermissionDto(type, allUsersPermissionLevel, groupPermissionLevels)) + .collect(Collectors.toList()); } /** @@ -823,15 +822,15 @@ public List getDirectoryPermissions(UUID directoryUuid, String us * If allUsers is false, groups list will contain only groups with exactly this permission type */ private PermissionDTO createPermissionDto( - PermissionType permissionType, - PermissionType allUsersPermissionLevel, - Map groupPermissionLevels) { + PermissionType permissionType, + PermissionType allUsersPermissionLevel, + Map groupPermissionLevels) { boolean hasAllUsersPermission = hasPermissionLevel(allUsersPermissionLevel, permissionType); List groupsWithPermission = hasAllUsersPermission - ? Collections.emptyList() - : getGroupsWithExactPermission(groupPermissionLevels, permissionType); + ? Collections.emptyList() + : getGroupsWithExactPermission(groupPermissionLevels, permissionType); return new PermissionDTO(hasAllUsersPermission, groupsWithPermission, permissionType); } @@ -840,20 +839,20 @@ private PermissionDTO createPermissionDto( * Gets all groups that have exactly the specified permission type */ private List getGroupsWithExactPermission( - Map groupPermissionLevels, - PermissionType exactPermissionType) { + Map groupPermissionLevels, + PermissionType exactPermissionType) { return groupPermissionLevels.entrySet().stream() - .filter(entry -> entry.getValue() == exactPermissionType) - .map(entry -> { - try { - return UUID.fromString(entry.getKey()); - } catch (IllegalArgumentException e) { - return null; - } - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .filter(entry -> entry.getValue() == exactPermissionType) + .map(entry -> { + try { + return UUID.fromString(entry.getKey()); + } catch (IllegalArgumentException e) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); } /** @@ -866,10 +865,10 @@ private boolean hasPermissionLevel(PermissionType actualLevel, PermissionType re return switch (requiredLevel) { case READ -> actualLevel == PermissionType.READ || - actualLevel == PermissionType.WRITE || - actualLevel == PermissionType.MANAGE; + actualLevel == PermissionType.WRITE || + actualLevel == PermissionType.MANAGE; case WRITE -> actualLevel == PermissionType.WRITE || - actualLevel == PermissionType.MANAGE; + actualLevel == PermissionType.MANAGE; case MANAGE -> actualLevel == PermissionType.MANAGE; }; } @@ -879,10 +878,10 @@ private boolean hasPermissionLevel(PermissionType actualLevel, PermissionType re */ private PermissionType extractGlobalPermissionLevel(List permissions) { return permissions.stream() - .filter(p -> ALL_USERS.equals(p.getUserId())) - .findFirst() - .map(this::determineHighestPermission) - .orElse(null); + .filter(p -> ALL_USERS.equals(p.getUserId())) + .findFirst() + .map(this::determineHighestPermission) + .orElse(null); } /** @@ -890,18 +889,18 @@ private PermissionType extractGlobalPermissionLevel(List permi */ private Map extractGroupPermissionLevels(List permissions) { return permissions.stream() - .filter(p -> !p.getUserGroupId().isEmpty()) - .collect(Collectors.toMap( - PermissionEntity::getUserGroupId, - this::determineHighestPermission, - (existing, replacement) -> shouldUpdatePermission(existing, replacement) ? replacement : existing, - HashMap::new - )); + .filter(p -> !p.getUserGroupId().isEmpty()) + .collect(Collectors.toMap( + PermissionEntity::getUserGroupId, + this::determineHighestPermission, + (existing, replacement) -> shouldUpdatePermission(existing, replacement) ? replacement : existing, + HashMap::new + )); } private void validatePermissionUpdateAccess(UUID directoryUuid, String userId) { if (!hasManagePermission(userId, List.of(directoryUuid))) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(DIRECTORY_PERMISSION_DENIED, "User '%s' is not allowed to update permissions on directory '%s'", userId, directoryUuid); } } @@ -1033,7 +1032,7 @@ private void applyPermissionConfiguration(UUID directoryUuid, PermissionConfigur } else { // Apply group permissions config.groupPermissions().forEach((groupId, permissionType) -> - addPermissionForGroup(directoryUuid, groupId, permissionType) + addPermissionForGroup(directoryUuid, groupId, permissionType) ); } @@ -1044,7 +1043,7 @@ private void applyPermissionConfiguration(UUID directoryUuid, PermissionConfigur */ private void applyGroupPermissions(UUID directoryUuid, Map groupPermissions, Set targetPermissions) { groupPermissions.entrySet().stream() - .filter(entry -> targetPermissions.contains(entry.getValue())) - .forEach(entry -> addPermissionForGroup(directoryUuid, entry.getKey(), entry.getValue())); + .filter(entry -> targetPermissions.contains(entry.getValue())) + .forEach(entry -> addPermissionForGroup(directoryUuid, entry.getKey(), entry.getValue())); } } diff --git a/src/main/java/org/gridsuite/directory/server/PropertyServerNameProvider.java b/src/main/java/org/gridsuite/directory/server/PropertyServerNameProvider.java new file mode 100644 index 00000000..dca6fb3f --- /dev/null +++ b/src/main/java/org/gridsuite/directory/server/PropertyServerNameProvider.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.directory.server; + +import com.powsybl.ws.commons.error.ServerNameProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @author Mohamed Ben-rejeb {@literal } + */ +@Component +public class PropertyServerNameProvider implements ServerNameProvider { + + private final String name; + + public PropertyServerNameProvider(@Value("${spring.application.name:directory-server}") String name) { + this.name = name; + } + + @Override + public String serverName() { + return name; + } +} diff --git a/src/main/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandler.java b/src/main/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandler.java deleted file mode 100644 index e85b9518..00000000 --- a/src/main/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandler.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (c) 2021, RTE (http://www.rte-france.com) - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ -package org.gridsuite.directory.server; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; - -import static org.gridsuite.directory.server.DirectoryException.Type.*; - -/** - * @author Abdelsalem Hedhili - */ -@ControllerAdvice -public class RestResponseEntityExceptionHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(RestResponseEntityExceptionHandler.class); - - @ExceptionHandler(value = {DirectoryException.class}) - protected ResponseEntity handleException(RuntimeException exception) { - DirectoryException directoryException = (DirectoryException) exception; - LOGGER.debug(exception.getMessage(), exception); - return switch (directoryException.getType()) { - case NOT_ALLOWED -> ResponseEntity.status(HttpStatus.FORBIDDEN).body(NOT_ALLOWED); - case IS_DIRECTORY -> ResponseEntity.status(HttpStatus.FORBIDDEN).body(IS_DIRECTORY); - case NOT_DIRECTORY -> ResponseEntity.status(HttpStatus.FORBIDDEN).body(NOT_DIRECTORY); - case NOT_FOUND -> ResponseEntity.status(HttpStatus.NOT_FOUND).body(NOT_FOUND); - case UNKNOWN_NOTIFICATION -> ResponseEntity.status(HttpStatus.BAD_REQUEST).body(UNKNOWN_NOTIFICATION); - case NAME_ALREADY_EXISTS -> - ResponseEntity.status(HttpStatus.CONFLICT).body(directoryException.getMessage()); - case MOVE_IN_DESCENDANT_NOT_ALLOWED -> - ResponseEntity.status(HttpStatus.FORBIDDEN).body(MOVE_IN_DESCENDANT_NOT_ALLOWED); - }; - } -} diff --git a/src/main/java/org/gridsuite/directory/server/error/DirectoryBusinessErrorCode.java b/src/main/java/org/gridsuite/directory/server/error/DirectoryBusinessErrorCode.java new file mode 100644 index 00000000..b50ab957 --- /dev/null +++ b/src/main/java/org/gridsuite/directory/server/error/DirectoryBusinessErrorCode.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.directory.server.error; + +import com.powsybl.ws.commons.error.BusinessErrorCode; + +/** + * @author Mohamed Ben-rejeb {@literal } + * + * Business error codes emitted by the directory service. + */ +public enum DirectoryBusinessErrorCode implements BusinessErrorCode { + DIRECTORY_PERMISSION_DENIED("directory.permissionDenied"), + DIRECTORY_ELEMENT_NAME_BLANK("directory.elementNameBlank"), + DIRECTORY_NOT_DIRECTORY("directory.notDirectory"), + DIRECTORY_ELEMENT_NAME_CONFLICT("directory.elementNameConflict"), + DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED("directory.moveInDescendantNotAllowed"), + DIRECTORY_SOME_ELEMENTS_ARE_MISSING("directory.someElementsAreMissing"), + DIRECTORY_ELEMENT_NOT_FOUND("directory.elementNotFound"); + private final String code; + + DirectoryBusinessErrorCode(String code) { + this.code = code; + } + + public String value() { + return code; + } +} diff --git a/src/main/java/org/gridsuite/directory/server/error/DirectoryException.java b/src/main/java/org/gridsuite/directory/server/error/DirectoryException.java new file mode 100644 index 00000000..2522d49b --- /dev/null +++ b/src/main/java/org/gridsuite/directory/server/error/DirectoryException.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2021, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.directory.server.error; + +import com.powsybl.ws.commons.error.AbstractBusinessException; +import lombok.NonNull; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.UUID; + +/** + * @author Abdelsalem Hedhili + * @author Mohamed Ben-rejeb {@literal } + */ +public class DirectoryException extends AbstractBusinessException { + + private final DirectoryBusinessErrorCode errorCode; + + public DirectoryException(DirectoryBusinessErrorCode errorCode, String message) { + super(Objects.requireNonNull(message, "message must not be null")); + this.errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null"); + } + + public static DirectoryException createElementNotFound(@NonNull String type, @NonNull UUID uuid) { + return new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NOT_FOUND, + String.format("%s '%s' not found !", type, uuid)); + } + + public static DirectoryException createElementNameAlreadyExists(@NonNull String name) { + return new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NAME_CONFLICT, + String.format("Element with the same name '%s' already exists in the directory !", name)); + } + + public static DirectoryException of(DirectoryBusinessErrorCode errorCode, String message, Object... args) { + return new DirectoryException(errorCode, args.length == 0 ? message : String.format(message, args)); + } + + @NotNull + @Override + public DirectoryBusinessErrorCode getBusinessErrorCode() { + return errorCode; + } + +} diff --git a/src/main/java/org/gridsuite/directory/server/error/RestResponseEntityExceptionHandler.java b/src/main/java/org/gridsuite/directory/server/error/RestResponseEntityExceptionHandler.java new file mode 100644 index 00000000..e6861c77 --- /dev/null +++ b/src/main/java/org/gridsuite/directory/server/error/RestResponseEntityExceptionHandler.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2021, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.directory.server.error; + +import com.powsybl.ws.commons.error.AbstractBaseRestExceptionHandler; +import com.powsybl.ws.commons.error.ServerNameProvider; +import org.jetbrains.annotations.NotNull; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; + +/** + * @author Abdelsalem Hedhili + * @author Mohamed Ben-rejeb {@literal } + */ +@ControllerAdvice +public class RestResponseEntityExceptionHandler + extends AbstractBaseRestExceptionHandler { + + public RestResponseEntityExceptionHandler(ServerNameProvider serverNameProvider) { + super(serverNameProvider); + } + + @NotNull + @Override + protected DirectoryBusinessErrorCode getBusinessCode(DirectoryException ex) { + return ex.getBusinessErrorCode(); + } + + @Override + protected HttpStatus mapStatus(DirectoryBusinessErrorCode errorCode) { + return switch (errorCode) { + case DIRECTORY_ELEMENT_NOT_FOUND, DIRECTORY_SOME_ELEMENTS_ARE_MISSING -> HttpStatus.NOT_FOUND; + case DIRECTORY_ELEMENT_NAME_CONFLICT -> HttpStatus.CONFLICT; + case DIRECTORY_PERMISSION_DENIED, + DIRECTORY_ELEMENT_NAME_BLANK, + DIRECTORY_NOT_DIRECTORY, + DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED -> HttpStatus.FORBIDDEN; + }; + } + +} diff --git a/src/test/java/org/gridsuite/directory/server/AccessRightsControlTest.java b/src/test/java/org/gridsuite/directory/server/AccessRightsControlTest.java index 3f617d0a..5b5ddb0c 100644 --- a/src/test/java/org/gridsuite/directory/server/AccessRightsControlTest.java +++ b/src/test/java/org/gridsuite/directory/server/AccessRightsControlTest.java @@ -311,7 +311,7 @@ public void testElements() throws Exception { public void testExistence() throws Exception { // Insert root directory with same name not allowed UUID rootUuid1 = insertRootDirectory("user1", "root1"); - insertRootDirectory("user1", "root1", HttpStatus.FORBIDDEN); + insertRootDirectory("user1", "root1", HttpStatus.CONFLICT); // Insert elements with same name in a directory not allowed UUID dirUuid1 = insertSubElement(rootUuid1, toElementAttributes(null, "dir1", DIRECTORY, "user1")); diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java new file mode 100644 index 00000000..01223081 --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.directory.server; + +import org.gridsuite.directory.server.error.DirectoryBusinessErrorCode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mohamed Ben-rejeb {@literal } + */ +class DirectoryBusinessErrorCodeTest { + + @ParameterizedTest + @EnumSource(DirectoryBusinessErrorCode.class) + void valueMatchesEnumName(DirectoryBusinessErrorCode code) { + assertThat(code.value()).startsWith("directory."); + } +} diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java new file mode 100644 index 00000000..94e30cbd --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.directory.server; + +import org.gridsuite.directory.server.error.DirectoryBusinessErrorCode; +import org.gridsuite.directory.server.error.DirectoryException; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mohamed Ben-rejeb {@literal } + */ +class DirectoryExceptionTest { + + @Test + void staticFactoriesProduceExpectedMessages() { + DirectoryException notFound = DirectoryException.createElementNotFound("Folder", UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); + assertThat(notFound.getMessage()).contains("Folder"); + assertThat(notFound.getBusinessErrorCode()).isEqualTo(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NOT_FOUND); + + DirectoryException conflict = DirectoryException.createElementNameAlreadyExists("report"); + assertThat(conflict.getMessage()).contains("report"); + assertThat(conflict.getBusinessErrorCode()).isEqualTo(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NAME_CONFLICT); + + DirectoryException formatted = DirectoryException.of(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NAME_BLANK, + "Element '%s' invalid", "x"); + assertThat(formatted.getMessage()).isEqualTo("Element 'x' invalid"); + assertThat(formatted.getBusinessErrorCode()).isEqualTo(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NAME_BLANK); + } + +} diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java index f151d7e3..4f6649ac 100644 --- a/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java +++ b/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java @@ -9,6 +9,7 @@ import org.gridsuite.directory.server.dto.ElementAttributes; import org.gridsuite.directory.server.dto.RootDirectoryAttributes; import org.gridsuite.directory.server.elasticsearch.DirectoryElementInfosRepository; +import org.gridsuite.directory.server.error.DirectoryException; import org.gridsuite.directory.server.repository.DirectoryElementEntity; import org.gridsuite.directory.server.repository.DirectoryElementRepository; import org.gridsuite.directory.server.utils.elasticsearch.DisableElasticsearch; @@ -28,6 +29,7 @@ import static org.gridsuite.directory.server.dto.ElementAttributes.toElementAttributes; import static org.gridsuite.directory.server.utils.DirectoryTestUtils.createElement; import static org.gridsuite.directory.server.utils.DirectoryTestUtils.createRootElement; +import static org.gridsuite.directory.server.error.DirectoryBusinessErrorCode.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -118,7 +120,7 @@ void testDirectoryElementUniqueness() { // Insert the same element in the same directory throws an exception DirectoryException directoryException = assertThrows(DirectoryException.class, () -> directoryService.createElement(elementAttributes, rootUuid, "User1", false)); - assertEquals(DirectoryException.Type.NAME_ALREADY_EXISTS, directoryException.getType()); + assertEquals(DIRECTORY_ELEMENT_NAME_CONFLICT, directoryException.getBusinessErrorCode()); assertEquals(DirectoryException.createElementNameAlreadyExists(elementAttributes.getElementName()).getMessage(), directoryException.getMessage()); // Insert the same element in the same directory with new name generation does not throw an exception @@ -137,7 +139,7 @@ void testDirectoryElementUniqueness() { InOrder inOrder = inOrder(directoryService); when(directoryService.getDuplicateNameCandidate(root2Uuid, elementAttributes.getElementName(), elementAttributes.getType(), "User1")).thenReturn(elementAttributes.getElementName()); directoryException = assertThrows(DirectoryException.class, () -> directoryService.duplicateElement(element2Uuid, root2Uuid, root2Uuid, "User1")); - assertEquals(DirectoryException.Type.NAME_ALREADY_EXISTS, directoryException.getType()); + assertEquals(DIRECTORY_ELEMENT_NAME_CONFLICT, directoryException.getBusinessErrorCode()); assertEquals(DirectoryException.createElementNameAlreadyExists(elementAttributes.getElementName()).getMessage(), directoryException.getMessage()); inOrder.verify(directoryService, calls(MAX_RETRY)).getDuplicateNameCandidate(root2Uuid, elementAttributes.getElementName(), elementAttributes.getType(), "User1"); } @@ -203,7 +205,7 @@ void testMoveElement() { // move directory to it's descendent List list = List.of(dirUuid); // Just for Sonar issue (assertThrows) DirectoryException exception1 = assertThrows(DirectoryException.class, () -> directoryService.moveElementsDirectory(list, subDirUuid, "user1")); - assertEquals(DirectoryException.Type.MOVE_IN_DESCENDANT_NOT_ALLOWED, exception1.getType()); + assertEquals(DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED, exception1.getBusinessErrorCode()); } @Test @@ -231,6 +233,6 @@ void testMoveInNotDirectory() { List list = List.of(elementUuid1); // Just for Sonar issue (assertThrows) DirectoryException exception2 = assertThrows(DirectoryException.class, () -> directoryService.moveElementsDirectory(list, elementUuid2, "user1")); - assertEquals(DirectoryException.Type.NOT_DIRECTORY, exception2.getType()); + assertEquals(DIRECTORY_NOT_DIRECTORY, exception2.getBusinessErrorCode()); } } diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryTest.java index 471af1b2..f156c632 100644 --- a/src/test/java/org/gridsuite/directory/server/DirectoryTest.java +++ b/src/test/java/org/gridsuite/directory/server/DirectoryTest.java @@ -31,7 +31,6 @@ import org.gridsuite.directory.server.repository.PermissionRepository; import org.gridsuite.directory.server.services.UserAdminService; import org.gridsuite.directory.server.utils.MatcherJson; -import org.hamcrest.core.IsEqual; import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.Before; @@ -62,7 +61,6 @@ import static com.vladmihalcea.sql.SQLStatementCountValidator.*; import static org.assertj.core.api.Assertions.assertThat; -import static org.gridsuite.directory.server.DirectoryException.Type.UNKNOWN_NOTIFICATION; import static org.gridsuite.directory.server.NotificationService.HEADER_UPDATE_TYPE; import static org.gridsuite.directory.server.NotificationService.*; import static org.gridsuite.directory.server.dto.ElementAttributes.toElementAttributes; @@ -74,6 +72,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -478,13 +477,13 @@ public void testMoveElementNotFound() throws Exception { .header("userId", "Doe") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(List.of(unknownUuid)))) - .andExpect(status().isNotFound()); + .andExpect(status().isInternalServerError()); mockMvc.perform(put("/v1/elements/?targetDirectoryUuid=" + unknownUuid) .header("userId", "Doe") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(List.of(elementUuid)))) - .andExpect(status().isNotFound()); + .andExpect(status().isInternalServerError()); assertNbElementsInRepositories(2); } @@ -671,7 +670,7 @@ public void testDirectoryMoveError() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(List.of(elementUuid1))) ) - .andExpect(status().isBadRequest()); + .andExpect(status().isInternalServerError()); // test move element to one of its descendents mockMvc.perform(put("/v1/elements?targetDirectoryUuid=" + elementUuid2) @@ -1019,8 +1018,10 @@ public void testEmitDirectoryChangedNotification() { // Test unknown type notification mockMvc.perform(post(String.format("/v1/elements/%s/notification?type=bad_type", elementAttributes.getElementUuid())) .header("userId", "Doe")) - .andExpect(status().isBadRequest()) - .andExpect(content().string(new IsEqual<>(objectMapper.writeValueAsString(UNKNOWN_NOTIFICATION)))); + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.server").value("directory-server")) + .andExpect(jsonPath("$.status").value(HttpStatus.INTERNAL_SERVER_ERROR.value())) + .andExpect(jsonPath("$.path").value(String.format("/v1/elements/%s/notification", elementAttributes.getElementUuid()))); } @SneakyThrows diff --git a/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java b/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java new file mode 100644 index 00000000..722c0349 --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.directory.server; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mohamed Ben-rejeb {@literal } + */ +class PropertyServerNameProviderTest { + + @Test + void returnsProvidedName() { + PropertyServerNameProvider provider = new PropertyServerNameProvider("custom-server"); + assertThat(provider.serverName()).isEqualTo("custom-server"); + } +} diff --git a/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java new file mode 100644 index 00000000..d17f1bc6 --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.directory.server; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import org.gridsuite.directory.server.error.DirectoryBusinessErrorCode; +import org.gridsuite.directory.server.error.DirectoryException; +import org.gridsuite.directory.server.error.RestResponseEntityExceptionHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.client.HttpClientErrorException; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Mohamed Ben-rejeb {@literal } + */ +class RestResponseEntityExceptionHandlerTest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); + + private TestRestResponseEntityExceptionHandler handler; + + @BeforeEach + void setUp() { + handler = new TestRestResponseEntityExceptionHandler(); + } + + @Test + void mapsBusinessErrorToStatus() { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/dir"); + DirectoryException exception = new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NOT_FOUND, + "Directory element missing"); + + ResponseEntity response = handler.invokeHandleDomainException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(response.getBody()).isNotNull(); + assertEquals("directory.elementNotFound", response.getBody().getBusinessErrorCode()); + } + + @Test + void propagatesRemoteErrorDetails() throws JsonProcessingException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/dir/resource"); + PowsyblWsProblemDetail remote = PowsyblWsProblemDetail.builder(HttpStatus.FORBIDDEN) + .server("downstream") + .detail("Denied") + .path("/remote").build(); + + HttpClientErrorException exception = HttpClientErrorException.create( + HttpStatus.FORBIDDEN, + "coucou", + HttpHeaders.EMPTY, + OBJECT_MAPPER.writeValueAsBytes(remote), + null + ); + + ResponseEntity response = handler.invokeHandleRemoteException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + PowsyblWsProblemDetail body = response.getBody(); + assertThat(body).isNotNull(); + assertThat(body.getDetail()).isEqualTo("Denied"); + assertThat(body.getChain()).hasSize(1); + } + + @Test + void wrapsRemoteExceptionWhenPayloadInvalid() { + MockHttpServletRequest request = new MockHttpServletRequest("DELETE", "/dir/remote"); + HttpClientErrorException exception = HttpClientErrorException.create( + HttpStatus.BAD_GATEWAY, + "Bad gateway", + null, + "oops".getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8 + ); + + ResponseEntity response = handler.invokeHandleRemoteException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_GATEWAY); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getChain()).hasSize(1); + } + + @Test + void reusesRemoteStatusWhenPayloadValid() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/dir/remote"); + PowsyblWsProblemDetail remote = PowsyblWsProblemDetail.builder(HttpStatus.NOT_FOUND) + .server("downstream") + .businessErrorCode("directory.downstreamNotFound") + .detail("missing") + .path("/remote/missing") + .build(); + + byte[] payload = OBJECT_MAPPER.writeValueAsBytes(remote); + HttpClientErrorException exception = HttpClientErrorException.create(HttpStatus.NOT_FOUND, "Not found", + null, payload, StandardCharsets.UTF_8); + + ResponseEntity response = handler.invokeHandleRemoteException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(response.getBody()).isNotNull(); + assertEquals("directory.downstreamNotFound", response.getBody().getBusinessErrorCode()); + } + + private static final class TestRestResponseEntityExceptionHandler extends RestResponseEntityExceptionHandler { + + private TestRestResponseEntityExceptionHandler() { + super(() -> "directory-server"); + } + + ResponseEntity invokeHandleDomainException(DirectoryException exception, MockHttpServletRequest request) { + return super.handleDomainException(exception, request); + } + + ResponseEntity invokeHandleRemoteException(HttpClientErrorException exception, MockHttpServletRequest request) { + return super.handleRemoteException(exception, request); + } + } +}