diff --git a/pom.xml b/pom.xml index 4f00361d..9c7b98f9 100644 --- a/pom.xml +++ b/pom.xml @@ -195,6 +195,7 @@ com.powsybl powsybl-ws-commons + 1.29.0-SNAPSHOT io.projectreactor diff --git a/src/main/java/org/gridsuite/directory/server/DirectoryBusinessErrorCode.java b/src/main/java/org/gridsuite/directory/server/DirectoryBusinessErrorCode.java new file mode 100644 index 00000000..fbe4e575 --- /dev/null +++ b/src/main/java/org/gridsuite/directory/server/DirectoryBusinessErrorCode.java @@ -0,0 +1,39 @@ +/** + * 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.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_ROOT_ALREADY_EXISTS("directory.rootAlreadyExists"), + DIRECTORY_NOT_DIRECTORY("directory.notDirectory"), + DIRECTORY_ELEMENT_NAME_CONFLICT("directory.elementNameConflict"), + DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED("directory.moveInDescendantNotAllowed"), + DIRECTORY_MOVE_SELECTION_EMPTY("directory.moveSelectionEmpty"), + DIRECTORY_ELEMENT_NOT_FOUND("directory.elementNotFound"), + DIRECTORY_DIRECTORY_NOT_FOUND_IN_PATH("directory.directoryNotFoundInPath"), + DIRECTORY_NOTIFICATION_UNKNOWN("directory.notificationUnknown"), + DIRECTORY_CANNOT_DELETE_ELEMENT("directory.cannotDeleteElement"), + DIRECTORY_REMOTE_ERROR("directory.remoteError"); + + 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/DirectoryException.java b/src/main/java/org/gridsuite/directory/server/DirectoryException.java index a5f585b1..43c431aa 100644 --- a/src/main/java/org/gridsuite/directory/server/DirectoryException.java +++ b/src/main/java/org/gridsuite/directory/server/DirectoryException.java @@ -6,51 +6,63 @@ */ package org.gridsuite.directory.server; +import com.powsybl.ws.commons.error.AbstractPowsyblWsException; +import com.powsybl.ws.commons.error.BusinessErrorCode; +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; import lombok.NonNull; import java.util.Objects; +import java.util.Optional; import java.util.UUID; /** * @author Abdelsalem Hedhili + * @author Mohamed Ben-rejeb {@literal } */ -public class DirectoryException extends RuntimeException { +public class DirectoryException extends AbstractPowsyblWsException { - private final Type type; + private final DirectoryBusinessErrorCode errorCode; + private final PowsyblWsProblemDetail remoteError; - public DirectoryException(Type type) { - super(Objects.requireNonNull(type.name())); - this.type = type; + public DirectoryException(DirectoryBusinessErrorCode errorCode, String message) { + this(errorCode, message, null); } - public DirectoryException(Type type, String message) { - super(message); - this.type = type; + public DirectoryException(DirectoryBusinessErrorCode errorCode, String message, PowsyblWsProblemDetail remoteError) { + super(Objects.requireNonNull(message, "message must not be null")); + this.errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null"); + this.remoteError = remoteError; } public static DirectoryException createNotificationUnknown(@NonNull String action) { - return new DirectoryException(Type.UNKNOWN_NOTIFICATION, String.format("The notification type '%s' is unknown", action)); + return new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_NOTIFICATION_UNKNOWN, + 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)); + 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(Type.NAME_ALREADY_EXISTS, String.format("Element with the same name '%s' already exists in the directory !", name)); + return new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NAME_CONFLICT, + String.format("Element with the same name '%s' already exists in the directory !", name)); } - Type getType() { - return type; + public static DirectoryException of(DirectoryBusinessErrorCode errorCode, String message, Object... args) { + return new DirectoryException(errorCode, args.length == 0 ? message : String.format(message, args)); } - public enum Type { - NOT_ALLOWED, - NOT_FOUND, - NOT_DIRECTORY, - IS_DIRECTORY, - UNKNOWN_NOTIFICATION, - NAME_ALREADY_EXISTS, - MOVE_IN_DESCENDANT_NOT_ALLOWED, + public Optional getErrorCode() { + return Optional.of(errorCode); + } + + @Override + public Optional getBusinessErrorCode() { + return Optional.ofNullable(errorCode); + } + + public Optional getRemoteError() { + return Optional.ofNullable(remoteError); } } diff --git a/src/main/java/org/gridsuite/directory/server/DirectoryService.java b/src/main/java/org/gridsuite/directory/server/DirectoryService.java index ba21acea..61ff1f01 100644 --- a/src/main/java/org/gridsuite/directory/server/DirectoryService.java +++ b/src/main/java/org/gridsuite/directory/server/DirectoryService.java @@ -24,8 +24,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.gridsuite.directory.server.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 +94,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 +111,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 +133,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_ROOT_ALREADY_EXISTS, "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 +153,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 +193,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 +203,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 +227,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; @@ -269,10 +270,10 @@ public List getDirectoryElements(UUID directoryUuid, List descendents = repositoryService.findAllDescendants(directoryUuid).stream().toList(); return descendents - .stream() - .filter(e -> types.isEmpty() || types.contains(e.getType())) - .map(ElementAttributes::toElementAttributes) - .toList(); + .stream() + .filter(e -> types.isEmpty() || types.contains(e.getType())) + .map(ElementAttributes::toElementAttributes) + .toList(); } else { return getAllDirectoryElementsStream(directoryUuid, types).toList(); } @@ -280,16 +281,16 @@ public List getDirectoryElements(UUID directoryUuid, List getOnlyElementsStream(UUID directoryUuid, List types) { return getAllDirectoryElementsStream(directoryUuid, types) - .filter(elementAttributes -> !elementAttributes.getType().equals(DIRECTORY)); + .filter(elementAttributes -> !elementAttributes.getType().equals(DIRECTORY)); } private Stream getAllDirectoryElementsStream(UUID directoryUuid, List types) { List directoryElements = repositoryService.findAllByParentId(directoryUuid); Map subdirectoriesCountsMap = getSubDirectoriesCountsMap(types, directoryElements); return directoryElements - .stream() - .filter(e -> e.getType().equals(DIRECTORY) || types.isEmpty() || types.contains(e.getType())) - .map(e -> toElementAttributes(e, subdirectoriesCountsMap.getOrDefault(e.getId(), 0L))); + .stream() + .filter(e -> e.getType().equals(DIRECTORY) || types.isEmpty() || types.contains(e.getType())) + .map(e -> toElementAttributes(e, subdirectoriesCountsMap.getOrDefault(e.getId(), 0L))); } public List getRootDirectories(List types, String userId) { @@ -301,8 +302,8 @@ public List getRootDirectories(List types, String use } Map subdirectoriesCountsMap = getSubDirectoriesCountsMap(types, directoryElements); 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) { @@ -313,8 +314,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())) { - throw new DirectoryException(NOT_ALLOWED); + directoryHasElementOfNameAndType(directoryElement.getParentId(), newElementAttributes.getElementName(), directoryElement.getType())) { + 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)); @@ -332,7 +335,7 @@ 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); + throw DirectoryException.of(DIRECTORY_MOVE_SELECTION_EMPTY, "Cannot move elements: no elements provided"); } validateNewDirectory(newDirectoryUuid); @@ -371,7 +374,9 @@ private void moveElementDirectory(DirectoryElementEntity element, UUID newDirect private void validateElementForMove(DirectoryElementEntity element, UUID newDirectoryUuid, Set descendentsUuids) { 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())) { @@ -386,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); } } @@ -434,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 @@ -470,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(); } @@ -502,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())) - ).toList(); + hasReadPermissions(userId, List.of(directoryElementEntity.getId())) + ).toList(); } if (strictMode && elementEntities.size() != ids.stream().distinct().count()) { - throw new DirectoryException(NOT_FOUND); + throw DirectoryException.of(DIRECTORY_ELEMENT_NOT_FOUND, "Some requested elements were not found"); } Map subElementsCount = getSubDirectoriesCounts(elementEntities.stream().map(DirectoryElementEntity::getId).toList(), types); 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) { @@ -545,7 +552,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)) { @@ -569,7 +576,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_DIRECTORY_NOT_FOUND_IN_PATH, "Directory '%s' not found in path", s); } else { parentDirectoryUuid = currentDirectoryUuid; } @@ -602,13 +609,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 ); } @@ -626,13 +633,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 ); } @@ -640,15 +647,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); @@ -659,14 +666,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) { @@ -690,9 +697,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; } @@ -707,8 +714,8 @@ private PermissionCheckResult checkWritePermission(String userId, List ele public boolean hasReadPermissions(String userId, List elementUuids) { List elements = directoryElementRepository.findAllByIdIn(elementUuids); return elements.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) ); } @@ -726,22 +733,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) { @@ -788,7 +795,7 @@ private void addPermissionForGroup(UUID elementUuid, String groupId, PermissionT public void validatePermissionsGetAccess(UUID directoryUuid, String userId) { if (!roleService.isUserExploreAdmin() && !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); } } @@ -797,7 +804,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 */ @@ -812,8 +819,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()); } /** @@ -822,15 +829,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); } @@ -839,20 +846,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()); } /** @@ -865,10 +872,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; }; } @@ -878,10 +885,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); } /** @@ -889,18 +896,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 (!roleService.isUserExploreAdmin() && !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); } } @@ -1032,7 +1039,7 @@ private void applyPermissionConfiguration(UUID directoryUuid, PermissionConfigur } else { // Apply group permissions config.groupPermissions().forEach((groupId, permissionType) -> - addPermissionForGroup(directoryUuid, groupId, permissionType) + addPermissionForGroup(directoryUuid, groupId, permissionType) ); } @@ -1043,7 +1050,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..cc21b055 --- /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("${server.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 index f10d7329..19994b19 100644 --- a/src/main/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandler.java +++ b/src/main/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandler.java @@ -6,46 +6,64 @@ */ package org.gridsuite.directory.server; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.powsybl.ws.commons.error.AbstractBaseRestExceptionHandler; +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import com.powsybl.ws.commons.error.ServerNameProvider; 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.*; +import java.util.Optional; /** * @author Abdelsalem Hedhili + * @author Mohamed Ben-rejeb {@literal } */ @ControllerAdvice -public class RestResponseEntityExceptionHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(RestResponseEntityExceptionHandler.class); - - @ExceptionHandler(value = {DirectoryException.class}) - protected ResponseEntity handleException(RuntimeException exception) { - if (LOGGER.isErrorEnabled()) { - LOGGER.error(exception.getMessage(), exception); - } - DirectoryException directoryException = (DirectoryException) exception; - switch (directoryException.getType()) { - case NOT_ALLOWED: - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(NOT_ALLOWED); - case IS_DIRECTORY: - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(IS_DIRECTORY); - case NOT_DIRECTORY: - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(NOT_DIRECTORY); - case NOT_FOUND: - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(NOT_FOUND); - case UNKNOWN_NOTIFICATION: - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(UNKNOWN_NOTIFICATION); - case NAME_ALREADY_EXISTS: - return ResponseEntity.status(HttpStatus.CONFLICT).body(directoryException.getMessage()); - case MOVE_IN_DESCENDANT_NOT_ALLOWED: - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(MOVE_IN_DESCENDANT_NOT_ALLOWED); - default: - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } +public class RestResponseEntityExceptionHandler + extends AbstractBaseRestExceptionHandler { + + public RestResponseEntityExceptionHandler(ServerNameProvider serverNameProvider) { + super(serverNameProvider); + } + + @Override + protected Optional getRemoteError(DirectoryException ex) { + return ex.getRemoteError(); + } + + @Override + protected Optional getBusinessCode(DirectoryException ex) { + return ex.getErrorCode(); + } + + @Override + protected HttpStatus mapStatus(DirectoryBusinessErrorCode errorCode) { + return switch (errorCode) { + case DIRECTORY_ELEMENT_NOT_FOUND, DIRECTORY_DIRECTORY_NOT_FOUND_IN_PATH -> HttpStatus.NOT_FOUND; + case DIRECTORY_ELEMENT_NAME_CONFLICT -> HttpStatus.CONFLICT; + case DIRECTORY_NOTIFICATION_UNKNOWN -> HttpStatus.BAD_REQUEST; + case DIRECTORY_REMOTE_ERROR -> HttpStatus.INTERNAL_SERVER_ERROR; + case DIRECTORY_PERMISSION_DENIED, + DIRECTORY_ELEMENT_NAME_BLANK, + DIRECTORY_ROOT_ALREADY_EXISTS, + DIRECTORY_NOT_DIRECTORY, + DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED, + DIRECTORY_MOVE_SELECTION_EMPTY, + DIRECTORY_CANNOT_DELETE_ELEMENT -> HttpStatus.FORBIDDEN; + }; + } + + @Override + protected DirectoryBusinessErrorCode defaultRemoteErrorCode() { + return DirectoryBusinessErrorCode.DIRECTORY_REMOTE_ERROR; + } + + @Override + protected DirectoryException wrapRemote(PowsyblWsProblemDetail remoteError) { + return new DirectoryException( + DirectoryBusinessErrorCode.DIRECTORY_REMOTE_ERROR, + remoteError.getDetail(), + remoteError + ); } } diff --git a/src/main/java/org/gridsuite/directory/server/services/UserAdminService.java b/src/main/java/org/gridsuite/directory/server/services/UserAdminService.java index 9c669272..5d40064f 100644 --- a/src/main/java/org/gridsuite/directory/server/services/UserAdminService.java +++ b/src/main/java/org/gridsuite/directory/server/services/UserAdminService.java @@ -11,7 +11,6 @@ import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -45,11 +44,7 @@ public UserAdminService(RestTemplate restTemplate, RemoteServicesProperties remo @RegisterReflectionForBinding(UserGroupDTO.class) public List getUserGroups(String sub) { String path = UriComponentsBuilder.fromPath(DELIMITER + USER_ADMIN_API_VERSION + GET_USER_GROUPS_URI) - .buildAndExpand(sub).toUriString(); - try { - return List.of(Objects.requireNonNull(restTemplate.getForEntity(userAdminServerBaseUri + path, UserGroupDTO[].class).getBody())); - } catch (HttpStatusCodeException e) { - return List.of(); - } + .buildAndExpand(sub).toUriString(); + return List.of(Objects.requireNonNull(restTemplate.getForEntity(userAdminServerBaseUri + path, UserGroupDTO[].class).getBody())); } } 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..63b4ed5f --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java @@ -0,0 +1,15 @@ +package org.gridsuite.directory.server; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +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..60bde1e5 --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java @@ -0,0 +1,50 @@ +package org.gridsuite.directory.server; + +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import java.time.Instant; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class DirectoryExceptionTest { + + @Test + void staticFactoriesProduceExpectedMessages() { + DirectoryException notification = DirectoryException.createNotificationUnknown("ARCHIVE"); + assertThat(notification.getMessage()).contains("ARCHIVE"); + assertThat(notification.getErrorCode()).contains(DirectoryBusinessErrorCode.DIRECTORY_NOTIFICATION_UNKNOWN); + + DirectoryException notFound = DirectoryException.createElementNotFound("Folder", UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); + assertThat(notFound.getMessage()).contains("Folder"); + assertThat(notFound.getErrorCode()).contains(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NOT_FOUND); + + DirectoryException conflict = DirectoryException.createElementNameAlreadyExists("report"); + assertThat(conflict.getMessage()).contains("report"); + assertThat(conflict.getErrorCode()).contains(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.getErrorCode()).contains(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NAME_BLANK); + } + + @Test + void remoteErrorIsExposedWhenProvided() { + PowsyblWsProblemDetail remote = PowsyblWsProblemDetail.builder(HttpStatus.BAD_GATEWAY) + .server("remote") + .detail("Gateway failure") + .timestamp(Instant.parse("2025-08-01T00:00:00Z")) + .path("/remote") + .build(); + + DirectoryException exception = new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_REMOTE_ERROR, + "wrapped", + remote); + + assertThat(exception.getRemoteError()).contains(remote); + assertThat(exception.getBusinessErrorCode()).contains(DirectoryBusinessErrorCode.DIRECTORY_REMOTE_ERROR); + } +} diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java index f151d7e3..4dbbd57d 100644 --- a/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java +++ b/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java @@ -28,6 +28,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.DirectoryBusinessErrorCode.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -118,7 +119,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.getErrorCode().get()); 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 +138,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.getErrorCode().get()); assertEquals(DirectoryException.createElementNameAlreadyExists(elementAttributes.getElementName()).getMessage(), directoryException.getMessage()); inOrder.verify(directoryService, calls(MAX_RETRY)).getDuplicateNameCandidate(root2Uuid, elementAttributes.getElementName(), elementAttributes.getType(), "User1"); } @@ -203,7 +204,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.getErrorCode().get()); } @Test @@ -231,6 +232,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.getErrorCode().get()); } } diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryTest.java index e8f87130..3f1bbaf3 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,7 @@ 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.DirectoryBusinessErrorCode.DIRECTORY_NOTIFICATION_UNKNOWN; 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 +73,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; /** @@ -1020,7 +1020,11 @@ public void testEmitDirectoryChangedNotification() { 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(jsonPath("$.server").value("directory-server")) + .andExpect(jsonPath("$.businessErrorCode").value(DIRECTORY_NOTIFICATION_UNKNOWN.value())) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.detail").value("The notification type 'bad_type' is unknown")) + .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..0f53b2c0 --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java @@ -0,0 +1,14 @@ +package org.gridsuite.directory.server; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +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..d414b7e5 --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java @@ -0,0 +1,122 @@ +package org.gridsuite.directory.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +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 java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +class RestResponseEntityExceptionHandlerTest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); + + private TestRestResponseEntityExceptionHandler handler; + + @BeforeEach + void setUp() { + handler = new TestRestResponseEntityExceptionHandler(); + } + + @Test + void mapsDomainExceptionsToConfiguredStatus() { + 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(); + assertThat(response.getBody().getBusinessErrorCode()) + .isEqualTo(new PowsyblWsProblemDetail.BusinessErrorCode("directory.elementNotFound")); + } + + @Test + void enrichesResponseWithRemoteDetails() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/dir/resource"); + PowsyblWsProblemDetail remote = PowsyblWsProblemDetail.builder(HttpStatus.FORBIDDEN) + .server("downstream") + .detail("Denied") + .timestamp(Instant.parse("2025-09-10T12:00:00Z")) + .path("/remote").build(); + DirectoryException exception = new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_PERMISSION_DENIED, + "Wrapped", remote); + + ResponseEntity response = handler.invokeHandleDomainException(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); + assertThat(body.getChain().getFirst().fromServer()).isEqualTo(PowsyblWsProblemDetail.ServerName.of("directory-server")); + } + + @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.INTERNAL_SERVER_ERROR); + PowsyblWsProblemDetail body = response.getBody(); + assertThat(body).isNotNull(); + assertThat(body.getBusinessErrorCode()) + .isEqualTo(new PowsyblWsProblemDetail.BusinessErrorCode("directory.remoteError")); + assertThat(body.getDetail()).contains("remote server"); + } + + @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") + .timestamp(Instant.parse("2025-09-15T08:30:00Z")) + .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(); + assertThat(response.getBody().getBusinessErrorCode()) + .isEqualTo(new PowsyblWsProblemDetail.BusinessErrorCode("directory.downstreamNotFound")); + } + + 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); + } + } +}