diff --git a/NEWS.md b/NEWS.md
index 25de8deac..de1999089 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,3 +1,6 @@
+## 2.0.4 (17-08-2026)
+- Update profile settings to allow for multiple settings per profile [MODLD-1040](https://folio-org.atlassian.net/browse/MODLD-1040)
+
## 2.0.3 (02-06-2026)
- Exclude LIGHT_RESOURCE from reindexing [MODLD-1071](https://folio-org.atlassian.net/browse/MODLD-1071)
- Fix Instance loses association with Work when Work profile is changed [MODLD-1072](https://folio-org.atlassian.net/browse/MODLD-1072)
diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index 545f2c754..53d2ed4c8 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -171,13 +171,43 @@
},
{
"methods": [ "GET" ],
- "pathPattern": "/linked-data/profile/settings/{id}",
- "permissionsRequired": [ "linked-data.profiles.settings.get" ]
+ "pathPattern": "/linked-data/profile/{profileId}/settings",
+ "permissionsRequired": [ "linked-data.profiles.settings.list.get" ]
},
{
"methods": [ "POST" ],
- "pathPattern": "/linked-data/profile/settings/{id}",
+ "pathPattern": "/linked-data/profile/{profileId}/settings",
"permissionsRequired": [ "linked-data.profiles.settings.post" ]
+ },
+ {
+ "methods": [ "GET" ],
+ "pathPattern": "/linked-data/profile/{profileId}/settings/{id}",
+ "permissionsRequired": [ "linked-data.profiles.settings.get" ]
+ },
+ {
+ "methods": [ "PUT" ],
+ "pathPattern": "/linked-data/profile/{profileId}/settings/{id}",
+ "permissionsRequired": [ "linked-data.profiles.settings.put" ]
+ },
+ {
+ "methods": [ "DELETE" ],
+ "pathPattern": "/linked-data/profile/{profileId}/settings/{id}",
+ "permissionsRequired": [ "linked-data.profiles.settings.delete" ]
+ },
+ {
+ "methods": [ "GET" ],
+ "pathPattern": "/linked-data/profile/{profileId}/preferred",
+ "permissionsRequired": [ "linked-data.profiles.settings.preferred.get" ]
+ },
+ {
+ "methods": [ "POST" ],
+ "pathPattern": "/linked-data/profile/{profileId}/preferred",
+ "permissionsRequired": [ "linked-data.profiles.settings.preferred.post" ]
+ },
+ {
+ "methods": [ "DELETE" ],
+ "pathPattern": "/linked-data/profile/{profileId}/preferred",
+ "permissionsRequired": [ "linked-data.profiles.settings.preferred.delete" ]
}
]
},
@@ -404,15 +434,45 @@
"displayName": "Linked Data: Delete the preferred profile for a resource type for the current user",
"description": "Delete the preferred profile for a resource type for the current user"
},
+ {
+ "permissionName": "linked-data.profiles.settings.list.get",
+ "displayName": "Linked Data: List all of the workspace profile settings for the profile for the current user",
+ "description": "List all of the workspace profile settings for the profile for the current user"
+ },
{
"permissionName": "linked-data.profiles.settings.get",
- "displayName": "Linked Data: Get the workspace profile settings for the profile for the current user",
- "description": "Get the workspace profile settings for the profile for the current user"
+ "displayName": "Linked Data: Get workspace profile settings for the profile for the current user",
+ "description": "Get workspace profile settings for the profile for the current user"
},
{
"permissionName": "linked-data.profiles.settings.post",
- "displayName": "Linked Data: Create or update the profile settings for the profile for the current user",
- "description": "Create or update the profile settings for the profile for the current user"
+ "displayName": "Linked Data: Create profile settings for the profile for the current user",
+ "description": "Create profile settings for the profile for the current user"
+ },
+ {
+ "permissionName": "linked-data.profiles.settings.put",
+ "displayName": "Linked Data: Update workspace profile settings for the profile for the current user",
+ "description": "Update workspace profile settings for the profile for the current user"
+ },
+ {
+ "permissionName": "linked-data.profiles.settings.delete",
+ "displayName": "Linked Data: Delete profile settings for the profile for the current user",
+ "description": "Delete profile settings for the profile for the current user"
+ },
+ {
+ "permissionName": "linked-data.profiles.settings.preferred.get",
+ "displayName": "Linked Data: Get preferred workspace profile settings for the profile for the current user",
+ "description": "Get preferred workspace profile settings for the profile for the current user"
+ },
+ {
+ "permissionName": "linked-data.profiles.settings.preferred.post",
+ "displayName": "Linked Data: Create or update preferred profile settings for the profile for the current user",
+ "description": "Create or updated preferred profile settings for the profile for the current user"
+ },
+ {
+ "permissionName": "linked-data.profiles.settings.preferred.delete",
+ "displayName": "Linked Data: Delete preferred profile settings for the profile for the current user",
+ "description": "Delete preferred profile settings for the profile for the current user"
},
{
"permissionName": "linked-data.resources.rdf.get",
diff --git a/pom.xml b/pom.xml
index 2e777570c..7fffb6f7c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
mod-linked-data
org.folio
mod-linked-data
- 2.0.3
+ 2.0.4
jar
@@ -686,6 +686,6 @@
https://github.com/folio-org/${project.artifactId}
scm:git:git://github.com/folio-org/${project.artifactId}.git
scm:git:git@github.com:folio-org/${project.artifactId}.git
- v2.0.3
+ v2.0.4
diff --git a/src/main/java/org/folio/linked/data/controller/ProfileController.java b/src/main/java/org/folio/linked/data/controller/ProfileController.java
index dd2a8beed..e5297d779 100644
--- a/src/main/java/org/folio/linked/data/controller/ProfileController.java
+++ b/src/main/java/org/folio/linked/data/controller/ProfileController.java
@@ -1,13 +1,21 @@
package org.folio.linked.data.controller;
+import static org.springframework.http.HttpStatus.CREATED;
+
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.folio.linked.data.domain.dto.CustomProfileSettingsMetadata;
import org.folio.linked.data.domain.dto.CustomProfileSettingsRequestDto;
import org.folio.linked.data.domain.dto.CustomProfileSettingsResponseDto;
import org.folio.linked.data.domain.dto.PreferredProfileRequest;
+import org.folio.linked.data.domain.dto.PreferredProfileSettingsRequest;
import org.folio.linked.data.domain.dto.ProfileMetadata;
+import org.folio.linked.data.model.CreateProfileSettingsRequest;
import org.folio.linked.data.rest.resource.ProfileApi;
import org.folio.linked.data.service.profile.PreferredProfileService;
+import org.folio.linked.data.service.profile.PreferredProfileSettingsService;
import org.folio.linked.data.service.profile.ProfileService;
import org.folio.linked.data.service.profile.ProfileSettingsService;
import org.springframework.http.ResponseEntity;
@@ -20,6 +28,8 @@ public class ProfileController implements ProfileApi {
private final ProfileService profileService;
private final PreferredProfileService preferredProfileService;
private final ProfileSettingsService profileSettingsService;
+ private final PreferredProfileSettingsService preferredProfileSettingsService;
+ private final jakarta.validation.Validator validator;
@Override
public ResponseEntity getProfileById(Integer profileId) {
@@ -49,15 +59,75 @@ public ResponseEntity deletePreferredProfile(String resourceTypeUri) {
}
@Override
- public ResponseEntity getProfileSettings(Integer profileId) {
- return ResponseEntity.ok(profileSettingsService.getProfileSettings(profileId));
+ public ResponseEntity> getAllProfileSettings(Integer profileId) {
+ return ResponseEntity.ok(profileSettingsService.getAllProfileSettings(profileId));
+ }
+
+ @Override
+ public ResponseEntity getProfileSettings(
+ Integer profileId,
+ Integer profileSettingsId
+ ) {
+ return ResponseEntity.ok(profileSettingsService.getProfileSettings(profileId, profileSettingsId));
+ }
+
+ @Override
+ public ResponseEntity createProfileSettings(
+ Integer profileId,
+ @Valid CustomProfileSettingsRequestDto profileSettingsRequest) {
+ var wrappedRequest = new CreateProfileSettingsRequest(profileId, profileSettingsRequest);
+ validateCreateProfileSettingsRequest(wrappedRequest);
+
+ return ResponseEntity
+ .status(CREATED)
+ .body(profileSettingsService.createProfileSettings(profileId, profileSettingsRequest));
}
@Override
public ResponseEntity setProfileSettings(
Integer profileId,
+ Integer profileSettingsId,
CustomProfileSettingsRequestDto profileSettingsRequest) {
- profileSettingsService.setProfileSettings(profileId, profileSettingsRequest);
+ profileSettingsService.setProfileSettings(profileId, profileSettingsId, profileSettingsRequest);
+ return ResponseEntity.noContent().build();
+ }
+
+ @Override
+ public ResponseEntity deleteProfileSettings(
+ Integer profileId,
+ Integer profileSettingsId
+ ) {
+ profileSettingsService.deleteProfileSettings(profileId, profileSettingsId);
+ return ResponseEntity.noContent().build();
+ }
+
+ private void validateCreateProfileSettingsRequest(CreateProfileSettingsRequest request) {
+ var violations = validator.validate(request);
+ if (!violations.isEmpty()) {
+ throw new ConstraintViolationException(violations);
+ }
+ }
+
+ @Override
+ public ResponseEntity> getPreferredProfileSettings(Integer profileId) {
+ return ResponseEntity.ok(preferredProfileSettingsService.getPreferredProfileSettings(profileId));
+ }
+
+ @Override
+ public ResponseEntity setPreferredProfileSettings(
+ Integer profileId,
+ PreferredProfileSettingsRequest preferredProfileSettings
+ ) {
+ preferredProfileSettingsService.setPreferredProfileSettings(
+ profileId,
+ preferredProfileSettings.getProfileSettingsId()
+ );
+ return ResponseEntity.noContent().build();
+ }
+
+ @Override
+ public ResponseEntity deletePreferredProfileSettings(Integer profileId) {
+ preferredProfileSettingsService.deletePreferredProfileSettings(profileId);
return ResponseEntity.noContent().build();
}
}
diff --git a/src/main/java/org/folio/linked/data/model/CreateProfileSettingsRequest.java b/src/main/java/org/folio/linked/data/model/CreateProfileSettingsRequest.java
new file mode 100644
index 000000000..3e2599e51
--- /dev/null
+++ b/src/main/java/org/folio/linked/data/model/CreateProfileSettingsRequest.java
@@ -0,0 +1,20 @@
+package org.folio.linked.data.model;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.folio.linked.data.domain.dto.CustomProfileSettingsRequestDto;
+import org.folio.linked.data.validation.ProfileSettingsNameUniqueConstraint;
+
+@ProfileSettingsNameUniqueConstraint
+@AllArgsConstructor
+@Data
+public class CreateProfileSettingsRequest {
+ @NotNull
+ private Integer profileId;
+
+ @NotNull
+ @Valid
+ private CustomProfileSettingsRequestDto customProfileSettingsRequestDto;
+}
diff --git a/src/main/java/org/folio/linked/data/model/entity/PreferredProfileSettings.java b/src/main/java/org/folio/linked/data/model/entity/PreferredProfileSettings.java
new file mode 100644
index 000000000..1200ed161
--- /dev/null
+++ b/src/main/java/org/folio/linked/data/model/entity/PreferredProfileSettings.java
@@ -0,0 +1,30 @@
+package org.folio.linked.data.model.entity;
+
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.Entity;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.MapsId;
+import jakarta.persistence.Table;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.folio.linked.data.model.entity.pk.PreferredProfileSettingsPk;
+
+@Data
+@Entity
+@Table(name = "preferred_profile_settings")
+@Accessors(chain = true)
+public class PreferredProfileSettings {
+
+ @EmbeddedId
+ private PreferredProfileSettingsPk id;
+
+ @MapsId("profileId")
+ @ManyToOne
+ @JoinColumn(name = "profile_id", nullable = false)
+ private Profile profile;
+
+ @ManyToOne
+ @JoinColumn(name = "profile_settings_id", nullable = false)
+ private ProfileSettings profileSettings;
+}
diff --git a/src/main/java/org/folio/linked/data/model/entity/ProfileSettings.java b/src/main/java/org/folio/linked/data/model/entity/ProfileSettings.java
index c818e79a0..36549028c 100644
--- a/src/main/java/org/folio/linked/data/model/entity/ProfileSettings.java
+++ b/src/main/java/org/folio/linked/data/model/entity/ProfileSettings.java
@@ -1,32 +1,54 @@
package org.folio.linked.data.model.entity;
import jakarta.persistence.Column;
-import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
-import jakarta.persistence.MapsId;
+import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import java.util.UUID;
import lombok.Data;
import lombok.experimental.Accessors;
-import org.folio.linked.data.model.entity.pk.ProfileSettingsPk;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
@Data
@Entity
-@Table(name = "profile_settings")
+@Table(
+ name = "profile_settings",
+ uniqueConstraints = {
+ @UniqueConstraint(
+ name = "unique_profile_settings_name",
+ columnNames = {"user_id", "profile_id", "name"}
+ )
+ }
+)
@Accessors(chain = true)
public class ProfileSettings {
+ @Id
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "profile_settings_id_seq_gen")
+ @SequenceGenerator(
+ name = "profile_settings_id_seq_gen",
+ sequenceName = "profile_settings_id_seq",
+ initialValue = 1,
+ allocationSize = 1
+ )
+ private Integer id;
- @EmbeddedId
- private ProfileSettingsPk id;
+ @Column(name = "user_id", nullable = false)
+ private UUID userId;
- @MapsId("profileId")
@ManyToOne
@JoinColumn(name = "profile_id", nullable = false)
private Profile profile;
+ @Column(name = "name", nullable = false)
+ private String name;
+
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "settings", columnDefinition = "jsonb", nullable = false)
private String settings;
diff --git a/src/main/java/org/folio/linked/data/model/entity/pk/ProfileSettingsPk.java b/src/main/java/org/folio/linked/data/model/entity/pk/PreferredProfileSettingsPk.java
similarity index 87%
rename from src/main/java/org/folio/linked/data/model/entity/pk/ProfileSettingsPk.java
rename to src/main/java/org/folio/linked/data/model/entity/pk/PreferredProfileSettingsPk.java
index 034b23829..a20af4df7 100644
--- a/src/main/java/org/folio/linked/data/model/entity/pk/ProfileSettingsPk.java
+++ b/src/main/java/org/folio/linked/data/model/entity/pk/PreferredProfileSettingsPk.java
@@ -12,7 +12,7 @@
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
-public class ProfileSettingsPk implements Serializable {
+public class PreferredProfileSettingsPk implements Serializable {
@Column(name = "user_id", nullable = false)
private UUID userId;
diff --git a/src/main/java/org/folio/linked/data/repo/PreferredProfileSettingsRepository.java b/src/main/java/org/folio/linked/data/repo/PreferredProfileSettingsRepository.java
new file mode 100644
index 000000000..7bb726b86
--- /dev/null
+++ b/src/main/java/org/folio/linked/data/repo/PreferredProfileSettingsRepository.java
@@ -0,0 +1,12 @@
+package org.folio.linked.data.repo;
+
+import java.util.List;
+import java.util.UUID;
+import org.folio.linked.data.model.entity.PreferredProfileSettings;
+import org.folio.linked.data.model.entity.pk.PreferredProfileSettingsPk;
+import org.springframework.data.repository.CrudRepository;
+
+public interface PreferredProfileSettingsRepository
+ extends CrudRepository {
+ List findByIdUserIdAndIdProfileId(UUID userId, Integer profileId);
+}
diff --git a/src/main/java/org/folio/linked/data/repo/ProfileSettingsRepository.java b/src/main/java/org/folio/linked/data/repo/ProfileSettingsRepository.java
index 53ef12443..571f753fb 100644
--- a/src/main/java/org/folio/linked/data/repo/ProfileSettingsRepository.java
+++ b/src/main/java/org/folio/linked/data/repo/ProfileSettingsRepository.java
@@ -1,11 +1,17 @@
package org.folio.linked.data.repo;
+import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.folio.linked.data.model.entity.ProfileSettings;
-import org.folio.linked.data.model.entity.pk.ProfileSettingsPk;
import org.springframework.data.repository.CrudRepository;
-public interface ProfileSettingsRepository extends CrudRepository {
- Optional getByIdUserIdAndIdProfileId(UUID userId, Integer profileId);
+public interface ProfileSettingsRepository extends CrudRepository {
+ List findByUserIdAndProfileId(UUID userId, Integer profileId);
+
+ Optional findByIdAndUserId(Integer id, UUID userId);
+
+ Boolean existsByUserIdAndProfileIdAndName(UUID userId, Integer profileId, String name);
+
+ void deleteByIdAndProfileIdAndUserId(Integer id, Integer profileId, UUID userId);
}
diff --git a/src/main/java/org/folio/linked/data/service/profile/PreferredProfileSettingsService.java b/src/main/java/org/folio/linked/data/service/profile/PreferredProfileSettingsService.java
new file mode 100644
index 000000000..416f3d1a9
--- /dev/null
+++ b/src/main/java/org/folio/linked/data/service/profile/PreferredProfileSettingsService.java
@@ -0,0 +1,12 @@
+package org.folio.linked.data.service.profile;
+
+import java.util.List;
+import org.folio.linked.data.domain.dto.CustomProfileSettingsMetadata;
+
+public interface PreferredProfileSettingsService {
+ void setPreferredProfileSettings(Integer profileId, Integer profileSettingsId);
+
+ List getPreferredProfileSettings(Integer profileId);
+
+ void deletePreferredProfileSettings(Integer profileId);
+}
diff --git a/src/main/java/org/folio/linked/data/service/profile/PreferredProfileSettingsServiceImpl.java b/src/main/java/org/folio/linked/data/service/profile/PreferredProfileSettingsServiceImpl.java
new file mode 100644
index 000000000..4effdeda4
--- /dev/null
+++ b/src/main/java/org/folio/linked/data/service/profile/PreferredProfileSettingsServiceImpl.java
@@ -0,0 +1,68 @@
+package org.folio.linked.data.service.profile;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.folio.linked.data.domain.dto.CustomProfileSettingsMetadata;
+import org.folio.linked.data.exception.RequestProcessingExceptionBuilder;
+import org.folio.linked.data.model.entity.PreferredProfileSettings;
+import org.folio.linked.data.model.entity.pk.PreferredProfileSettingsPk;
+import org.folio.linked.data.repo.PreferredProfileSettingsRepository;
+import org.folio.linked.data.repo.ProfileRepository;
+import org.folio.linked.data.repo.ProfileSettingsRepository;
+import org.folio.spring.FolioExecutionContext;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+@Transactional
+public class PreferredProfileSettingsServiceImpl implements PreferredProfileSettingsService {
+
+ private final ProfileRepository profileRepository;
+ private final ProfileSettingsRepository profileSettingsRepository;
+ private final PreferredProfileSettingsRepository preferredProfileSettingsRepository;
+ private final RequestProcessingExceptionBuilder exceptionBuilder;
+ private final FolioExecutionContext executionContext;
+
+ @Override
+ public void setPreferredProfileSettings(Integer profileId, Integer profileSettingsId) {
+ var profile = profileRepository.findById(profileId)
+ .orElseThrow(() -> exceptionBuilder.notFoundLdResourceByIdException("Profile", String.valueOf(profileId)));
+ var profileSettings = profileSettingsRepository.findById(profileSettingsId)
+ .orElseThrow(() -> exceptionBuilder.notFoundLdResourceByIdException(
+ "ProfileSettings",
+ String.valueOf(profileSettingsId)
+ ));
+ var id = new PreferredProfileSettingsPk(executionContext.getUserId(), profileId);
+ var preferredProfileSettings = preferredProfileSettingsRepository.findById(id)
+ .map(pps -> pps.setProfileSettings(profileSettings))
+ .orElse(new PreferredProfileSettings().setId(id).setProfile(profile).setProfileSettings(profileSettings));
+ preferredProfileSettingsRepository.save(preferredProfileSettings);
+ }
+
+ @Override
+ public void deletePreferredProfileSettings(Integer profileId) {
+ var idToDelete = new PreferredProfileSettingsPk(executionContext.getUserId(), profileId);
+ preferredProfileSettingsRepository.deleteById(idToDelete);
+ }
+
+ @Override
+ public List getPreferredProfileSettings(Integer profileId) {
+ var userId = executionContext.getUserId();
+ var preferredProfileSettings = getPreferredProfileSettings(userId, profileId).stream().toList();
+
+ return preferredProfileSettings
+ .stream()
+ .map(PreferredProfileSettings::getProfileSettings)
+ .map(p -> new CustomProfileSettingsMetadata(p.getId(), p.getProfile().getId(), p.getName()))
+ .toList();
+ }
+
+ private Optional getPreferredProfileSettings(UUID userId, Integer profileId) {
+ return preferredProfileSettingsRepository.findById(new PreferredProfileSettingsPk(userId, profileId));
+ }
+}
diff --git a/src/main/java/org/folio/linked/data/service/profile/ProfileSettingsService.java b/src/main/java/org/folio/linked/data/service/profile/ProfileSettingsService.java
index 4d59ee7ae..68d9ea0fc 100644
--- a/src/main/java/org/folio/linked/data/service/profile/ProfileSettingsService.java
+++ b/src/main/java/org/folio/linked/data/service/profile/ProfileSettingsService.java
@@ -1,10 +1,27 @@
package org.folio.linked.data.service.profile;
+import java.util.List;
+import org.folio.linked.data.domain.dto.CustomProfileSettingsMetadata;
import org.folio.linked.data.domain.dto.CustomProfileSettingsRequestDto;
import org.folio.linked.data.domain.dto.CustomProfileSettingsResponseDto;
public interface ProfileSettingsService {
- CustomProfileSettingsResponseDto getProfileSettings(Integer profileId);
+ List getAllProfileSettings(Integer profileId);
- void setProfileSettings(Integer profileId, CustomProfileSettingsRequestDto profileSettingsRequest);
+ CustomProfileSettingsResponseDto getProfileSettings(Integer profileId, Integer profileSettingsId);
+
+ CustomProfileSettingsMetadata createProfileSettings(
+ Integer profileId,
+ CustomProfileSettingsRequestDto profileSettingsRequest
+ );
+
+ void setProfileSettings(
+ Integer profileId,
+ Integer profileSettingsId,
+ CustomProfileSettingsRequestDto profileSettingsRequest
+ );
+
+ void deleteProfileSettings(Integer profileId, Integer profileSettingsId);
+
+ Boolean nameExistsForProfile(Integer profileId, CustomProfileSettingsRequestDto profileSettingsRequest);
}
diff --git a/src/main/java/org/folio/linked/data/service/profile/ProfileSettingsServiceImpl.java b/src/main/java/org/folio/linked/data/service/profile/ProfileSettingsServiceImpl.java
index 0d67d3861..8f4fb865f 100644
--- a/src/main/java/org/folio/linked/data/service/profile/ProfileSettingsServiceImpl.java
+++ b/src/main/java/org/folio/linked/data/service/profile/ProfileSettingsServiceImpl.java
@@ -2,16 +2,17 @@
import static org.folio.linked.data.util.JsonUtils.JSON_MAPPER;
+import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.folio.linked.data.domain.dto.CustomProfileSettings;
+import org.folio.linked.data.domain.dto.CustomProfileSettingsMetadata;
import org.folio.linked.data.domain.dto.CustomProfileSettingsRequestDto;
import org.folio.linked.data.domain.dto.CustomProfileSettingsResponseDto;
import org.folio.linked.data.exception.RequestProcessingExceptionBuilder;
import org.folio.linked.data.model.entity.Profile;
import org.folio.linked.data.model.entity.ProfileSettings;
-import org.folio.linked.data.model.entity.pk.ProfileSettingsPk;
import org.folio.linked.data.repo.ProfileRepository;
import org.folio.linked.data.repo.ProfileSettingsRepository;
import org.folio.spring.FolioExecutionContext;
@@ -31,55 +32,119 @@ public class ProfileSettingsServiceImpl implements ProfileSettingsService {
@Override
@Transactional(readOnly = true)
- public CustomProfileSettingsResponseDto getProfileSettings(Integer profileId) {
+ public List getAllProfileSettings(Integer profileId) {
var userId = executionContext.getUserId();
- profileRepository.findById(profileId)
- .orElseThrow(() -> exceptionBuilder.notFoundLdResourceByIdException("Profile", String.valueOf(profileId)));
- var settings = profileSettingsRepository.getByIdUserIdAndIdProfileId(userId, profileId);
+ getProfile(profileId);
+ var settings = profileSettingsRepository.findByUserIdAndProfileId(userId, profileId);
+ return settings.stream()
+ .map(profileSettings -> toMetadata(profileId, profileSettings))
+ .toList();
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public CustomProfileSettingsResponseDto getProfileSettings(Integer profileId, Integer profileSettingsId) {
+ var userId = executionContext.getUserId();
+ getProfile(profileId);
+ var settings = profileSettingsRepository.findByIdAndUserId(profileSettingsId, userId);
return settings.map(profileSettings -> toDto(profileId, userId, profileSettings))
- .orElseGet(() -> defaultToProfile(profileId));
+ .orElseGet(() -> defaultToProfile(profileId, profileSettingsId));
}
@Override
- public void setProfileSettings(Integer profileId, CustomProfileSettingsRequestDto profileSettingsRequest) {
+ public CustomProfileSettingsMetadata createProfileSettings(
+ Integer profileId,
+ CustomProfileSettingsRequestDto profileSettingsRequest
+ ) {
var userId = executionContext.getUserId();
- var profile = profileRepository.findById(profileId)
- .orElseThrow(() -> exceptionBuilder.notFoundLdResourceByIdException("Profile", String.valueOf(profileId)));
+ var profile = getProfile(profileId);
+ try {
+ var settings = toEntity(profile, null, userId, profileSettingsRequest);
+ var saved = profileSettingsRepository.save(settings);
+ return toMetadata(profileId, saved);
+ } catch (JacksonException e) {
+ throw exceptionBuilder.badRequestException("Could not process settings", String.valueOf(profileId));
+ }
+ }
+
+ @Override
+ public void setProfileSettings(
+ Integer profileId,
+ Integer profileSettingsId,
+ CustomProfileSettingsRequestDto profileSettingsRequest
+ ) {
+ var userId = executionContext.getUserId();
+ var profile = getProfile(profileId);
try {
- var settings = toEntity(profile, userId, profileSettingsRequest);
+ var settings = toEntity(profile, profileSettingsId, userId, profileSettingsRequest);
profileSettingsRepository.save(settings);
} catch (JacksonException e) {
throw exceptionBuilder.badRequestException("Could not process settings", String.valueOf(profileId));
}
}
+ @Override
+ public void deleteProfileSettings(Integer profileId, Integer profileSettingsId) {
+ var userId = executionContext.getUserId();
+ getProfile(profileId);
+ profileSettingsRepository.deleteByIdAndProfileIdAndUserId(profileSettingsId, profileId, userId);
+ }
+
+ @Override
+ public Boolean nameExistsForProfile(Integer profileId, CustomProfileSettingsRequestDto profileSettingsRequest) {
+ var userId = executionContext.getUserId();
+ return profileSettingsRepository.existsByUserIdAndProfileIdAndName(
+ userId, profileId, profileSettingsRequest.getName());
+ }
+
+ private Profile getProfile(Integer profileId) {
+ return profileRepository.findById(profileId)
+ .orElseThrow(() -> exceptionBuilder.notFoundLdResourceByIdException("Profile", String.valueOf(profileId)));
+ }
+
/**
* In any case where the custom profile settings are not available, whether because
* they haven't been set, they've been corrupted, or something else is wrong internally,
* just return inactive settings. In all cases, this should lead to the profile's default
* being used, whether for settings editing or editor rendering.
*/
- private CustomProfileSettingsResponseDto defaultToProfile(Integer profileId) {
- return new CustomProfileSettingsResponseDto(profileId, false, null);
+ private CustomProfileSettingsResponseDto defaultToProfile(Integer profileId, Integer profileSettingsId) {
+ return new CustomProfileSettingsResponseDto(false, null, "(defaults)", profileSettingsId, profileId);
+ }
+
+ private CustomProfileSettingsMetadata toMetadata(
+ Integer profileId,
+ ProfileSettings settings
+ ) {
+ return new CustomProfileSettingsMetadata(settings.getId(), profileId, settings.getName());
}
private CustomProfileSettingsResponseDto toDto(Integer profileId, UUID userId, ProfileSettings settings) {
try {
var customProfileSettings = JSON_MAPPER.readValue(settings.getSettings(), CustomProfileSettings.class);
return new CustomProfileSettingsResponseDto(
- profileId,
customProfileSettings.getActive(),
- customProfileSettings.getChildren());
+ customProfileSettings.getChildren(),
+ settings.getName(),
+ settings.getId(),
+ profileId);
} catch (JacksonException e) {
- log.error("Could not read stored profile settings (user: {}, profile: {}) - default to profile",
- userId, profileId);
- return defaultToProfile(profileId);
+ log.error("Could not read stored profile settings (user: {}, profile: {}, settings: {}) - default to profile",
+ userId, profileId, settings.getId());
+ return defaultToProfile(settings.getId(), profileId);
}
}
- private ProfileSettings toEntity(Profile profile, UUID userId, CustomProfileSettingsRequestDto requestDto) {
+ private ProfileSettings toEntity(
+ Profile profile,
+ Integer profileSettingsId,
+ UUID userId,
+ CustomProfileSettingsRequestDto requestDto
+ ) {
return new ProfileSettings()
- .setId(new ProfileSettingsPk(userId, profile.getId()))
+ .setId(profileSettingsId)
+ .setName(requestDto.getName())
+ .setUserId(userId)
.setProfile(profile)
.setSettings(JSON_MAPPER.writeValueAsString(requestDto));
}
diff --git a/src/main/java/org/folio/linked/data/validation/ProfileSettingsNameUniqueConstraint.java b/src/main/java/org/folio/linked/data/validation/ProfileSettingsNameUniqueConstraint.java
new file mode 100644
index 000000000..780c5ad83
--- /dev/null
+++ b/src/main/java/org/folio/linked/data/validation/ProfileSettingsNameUniqueConstraint.java
@@ -0,0 +1,25 @@
+package org.folio.linked.data.validation;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.folio.linked.data.validation.dto.ProfileSettingsNameUniquenessValidator;
+
+@Documented
+@SuppressWarnings("javaarchitecture:S7091")
+@Constraint(validatedBy = ProfileSettingsNameUniquenessValidator.class)
+@Target({ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ProfileSettingsNameUniqueConstraint {
+
+ String message() default "{profileSettingsNameUniqueConstraint.message}";
+
+ Class>[] groups() default {};
+
+ Class extends Payload>[] payload() default {};
+
+}
diff --git a/src/main/java/org/folio/linked/data/validation/dto/ProfileSettingsNameUniquenessValidator.java b/src/main/java/org/folio/linked/data/validation/dto/ProfileSettingsNameUniquenessValidator.java
new file mode 100644
index 000000000..7d9e5f897
--- /dev/null
+++ b/src/main/java/org/folio/linked/data/validation/dto/ProfileSettingsNameUniquenessValidator.java
@@ -0,0 +1,42 @@
+package org.folio.linked.data.validation.dto;
+
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.folio.linked.data.model.CreateProfileSettingsRequest;
+import org.folio.linked.data.service.profile.ProfileSettingsService;
+import org.folio.linked.data.validation.ProfileSettingsNameUniqueConstraint;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+@Log4j2
+@SuppressWarnings("javaarchitecture:S7091")
+public class ProfileSettingsNameUniquenessValidator
+ implements ConstraintValidator {
+
+ private final ProfileSettingsService profileSettingsService;
+
+ @Override
+ public boolean isValid(
+ CreateProfileSettingsRequest createProfileSettingsRequest, ConstraintValidatorContext context) {
+ if (createProfileSettingsRequest.getProfileId() == null
+ || createProfileSettingsRequest.getCustomProfileSettingsRequestDto() == null) {
+ return true;
+ }
+
+ var isDuplicate = profileSettingsService.nameExistsForProfile(
+ createProfileSettingsRequest.getProfileId(), createProfileSettingsRequest.getCustomProfileSettingsRequestDto());
+
+ if (isDuplicate.booleanValue()) {
+ context.disableDefaultConstraintViolation();
+ context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
+ .addPropertyNode("customProfileSettingsRequestDto")
+ .addPropertyNode("name")
+ .addConstraintViolation();
+ }
+
+ return !isDuplicate;
+ }
+}
diff --git a/src/main/resources/ValidationMessages.properties b/src/main/resources/ValidationMessages.properties
index 5b6a4a942..58b0c891a 100644
--- a/src/main/resources/ValidationMessages.properties
+++ b/src/main/resources/ValidationMessages.properties
@@ -4,3 +4,4 @@ lccnUniqueConstraint.message=lccn_not_unique
partOfSeriesTitleConstraint.message=required_series_title
resourceTypeConstraint.message=wrong_resource_type
instanceSingleWorkConstraint.message=instance_no_single_work
+profileSettingsNameUniqueConstraint.message=profile_settings_name_not_unique
diff --git a/src/main/resources/changelog/changelog-master.xml b/src/main/resources/changelog/changelog-master.xml
index e48c578b1..b69c2960a 100644
--- a/src/main/resources/changelog/changelog-master.xml
+++ b/src/main/resources/changelog/changelog-master.xml
@@ -6,4 +6,5 @@
+
diff --git a/src/main/resources/changelog/scripts/v-3.0.0/metadata/changelog.xml b/src/main/resources/changelog/scripts/v-3.0.0/metadata/changelog.xml
new file mode 100644
index 000000000..71ba6250d
--- /dev/null
+++ b/src/main/resources/changelog/scripts/v-3.0.0/metadata/changelog.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/src/main/resources/changelog/scripts/v-3.0.0/metadata/tables/create_preferred_profile_settings_table.sql b/src/main/resources/changelog/scripts/v-3.0.0/metadata/tables/create_preferred_profile_settings_table.sql
new file mode 100644
index 000000000..15a620ae2
--- /dev/null
+++ b/src/main/resources/changelog/scripts/v-3.0.0/metadata/tables/create_preferred_profile_settings_table.sql
@@ -0,0 +1,15 @@
+--liquibase formatted sql
+
+--changeset create_preferred_profile_settings_table dbms:postgresql
+
+create table if not exists preferred_profile_settings
+(
+ user_id uuid not null,
+ profile_id smallint not null,
+ profile_settings_id int not null,
+ primary key (user_id, profile_id),
+ foreign key (profile_id) references profiles (id) on delete cascade,
+ foreign key (profile_settings_id) references profile_settings (id) on delete cascade
+);
+
+--rollback drop table preferred_profile_settings;
diff --git a/src/main/resources/changelog/scripts/v-3.0.0/metadata/tables/upgrade_profile_settings_table.sql b/src/main/resources/changelog/scripts/v-3.0.0/metadata/tables/upgrade_profile_settings_table.sql
new file mode 100644
index 000000000..0e48b4457
--- /dev/null
+++ b/src/main/resources/changelog/scripts/v-3.0.0/metadata/tables/upgrade_profile_settings_table.sql
@@ -0,0 +1,45 @@
+--liquibase formatted sql
+
+--changeset upgrade_profile_settings_table dbms:postgresql
+
+-- Create a sequence for the primary ID
+create sequence profile_settings_id_seq
+ start with 1;
+
+-- Add new primary key column
+alter table profile_settings
+ add column id int not null default nextval('profile_settings_id_seq');
+
+-- Remove previous primary key
+alter table profile_settings
+ drop constraint profile_settings_pkey;
+
+-- Add primary key constraint
+alter table profile_settings
+ add constraint profile_settings_pkey primary key (id);
+
+-- Recreate old PK index, still relevant
+create index if not exists profile_settings_user_profile
+ on profile_settings(user_id, profile_id);
+
+-- Add new name column
+alter table profile_settings
+ add column name text;
+
+-- Fill in placeholder value
+update profile_settings
+ set name = '(unnamed)';
+
+-- Set name to not null, required field
+alter table profile_settings
+ alter column name set not null;
+
+-- Make name unique for each user-profile
+alter table profile_settings
+ add constraint unique_profile_settings_name unique (user_id, profile_id, name);
+
+--rollback alter table profile_settings drop column name;
+--rollback drop index profile_settings_user_profile;
+--rollback alter table profile_settings drop constraint profile_settings_id_pk;
+--rollback alter table profile_settings drop column id;
+--rollback alter table profile_settings add constraint profile_settings_pkey primary key (user_id, profile_id);
diff --git a/src/main/resources/swagger.api/mod-linked-data.yaml b/src/main/resources/swagger.api/mod-linked-data.yaml
index e0e600abf..dbb8164e9 100644
--- a/src/main/resources/swagger.api/mod-linked-data.yaml
+++ b/src/main/resources/swagger.api/mod-linked-data.yaml
@@ -399,7 +399,53 @@ paths:
'500':
$ref: '#/components/responses/internalServerErrorResponse'
- /linked-data/profile/settings/{profileId}:
+ /linked-data/profile/{profileId}/settings:
+ get:
+ operationId: getAllProfileSettings
+ tags:
+ - profile
+ description: Get the workspace profile settings metadata list for a profile for the current user
+ parameters:
+ - $ref: '#/components/parameters/profileId'
+ responses:
+ '200':
+ description: Workspace profile settings metadata list for the current user
+ content:
+ application/json:
+ schema:
+ $ref: "schema/profile/customProfileSettingsMetadataArray.json"
+ '404':
+ description: No profile found with a given profileId
+ '500':
+ $ref: '#/components/responses/internalServerErrorResponse'
+
+ post:
+ operationId: createProfileSettings
+ tags:
+ - profile
+ description: Create the workspace profile settings for a profile for the current user
+ parameters:
+ - $ref: '#/components/parameters/profileId'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "schema/profile/customProfileSettingsRequestDto.json"
+ responses:
+ '201':
+ description: Workspace profile settings created successfully
+ content:
+ application/json:
+ schema:
+ $ref: "schema/profile/customProfileSettingsMetadata.json"
+ '400':
+ $ref: '#/components/responses/badRequestResponse'
+ '404':
+ description: No profile found with a given profileId
+ '500':
+ $ref: '#/components/responses/internalServerErrorResponse'
+
+ /linked-data/profile/{profileId}/settings/{profileSettingsId}:
get:
operationId: getProfileSettings
tags:
@@ -407,6 +453,7 @@ paths:
description: Get the workspace profile settings for a profile for the current user
parameters:
- $ref: '#/components/parameters/profileId'
+ - $ref: '#/components/parameters/profileSettingsId'
responses:
'200':
description: Workspace profile settings for the current user
@@ -419,13 +466,14 @@ paths:
'500':
$ref: '#/components/responses/internalServerErrorResponse'
- post:
+ put:
operationId: setProfileSettings
tags:
- profile
- description: Create or update the workspace profile settings for a profile for the current user
+ description: Update the workspace profile settings for a profile for the current user
parameters:
- $ref: '#/components/parameters/profileId'
+ - $ref: '#/components/parameters/profileSettingsId'
requestBody:
content:
application/json:
@@ -441,6 +489,75 @@ paths:
'500':
$ref: '#/components/responses/internalServerErrorResponse'
+ delete:
+ operationId: deleteProfileSettings
+ tags:
+ - profile
+ description: Delete a saved workspace profile settings for a profile for the current user
+ parameters:
+ - $ref: '#/components/parameters/profileId'
+ - $ref: '#/components/parameters/profileSettingsId'
+ responses:
+ '204':
+ description: Saved workspace profile settings deleted successfully
+ '400':
+ $ref: '#/components/responses/badRequestResponse'
+ '500':
+ $ref: '#/components/responses/internalServerErrorResponse'
+
+ /linked-data/profile/{profileId}/preferred:
+ get:
+ operationId: getPreferredProfileSettings
+ tags:
+ - profile
+ description: Get the preferred profile settings for the profile
+ parameters:
+ - $ref: '#/components/parameters/profileId'
+ responses:
+ '200':
+ description: Preferred profile settings for the profile and current user
+ content:
+ application/json:
+ schema:
+ $ref: "schema/profile/customProfileSettingsMetadataArray.json"
+ '500':
+ $ref: '#/components/responses/internalServerErrorResponse'
+
+ post:
+ operationId: setPreferredProfileSettings
+ tags:
+ - profile
+ description: Set the preferred profile settings for the profile
+ parameters:
+ - $ref: '#/components/parameters/profileId'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "schema/profile/preferredProfileSettingsRequest.json"
+ responses:
+ '204':
+ description: Preferred profile settings created or updated successfully
+ '400':
+ $ref: '#/components/responses/badRequestResponse'
+ '500':
+ $ref: '#/components/responses/internalServerErrorResponse'
+
+ delete:
+ operationId: deletePreferredProfileSettings
+ tags:
+ - profile
+ description: Delete the preferred profile settings for the profile
+ parameters:
+ - $ref: '#/components/parameters/profileId'
+ responses:
+ '204':
+ description: Preferred profile settings deleted successfully
+ '400':
+ $ref: '#/components/responses/badRequestResponse'
+ '500':
+ $ref: '#/components/responses/internalServerErrorResponse'
+
/linked-data/resource/{id}/graph:
get:
operationId: getResourceGraphById
@@ -639,6 +756,14 @@ components:
schema:
type: integer
format: int16
+ profileSettingsId:
+ name: profileSettingsId
+ in: path
+ required: true
+ description: ID of the profile settings
+ schema:
+ type: integer
+ format: int16
profileIdQuery:
name: profileId
in: query
diff --git a/src/main/resources/swagger.api/schema/profile/customProfileSettings.json b/src/main/resources/swagger.api/schema/profile/customProfileSettings.json
index edfda40a8..208af4994 100644
--- a/src/main/resources/swagger.api/schema/profile/customProfileSettings.json
+++ b/src/main/resources/swagger.api/schema/profile/customProfileSettings.json
@@ -2,21 +2,12 @@
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Workspace custom profile settings",
"type": "object",
- "properties": {
- "active": {
- "type": "boolean",
- "description": "True if settings are intended to be applied, false if intended to be ignored"
+ "allOf": [
+ {
+ "$ref": "customProfileSettingsLabel.json"
},
- "children": {
- "type": "array",
- "description": "Profile block children",
- "items": {
- "$ref": "customProfileSettingsChild.json"
- }
+ {
+ "$ref": "customProfileSettingsValues.json"
}
- },
- "required": [
- "active",
- "children"
]
}
diff --git a/src/main/resources/swagger.api/schema/profile/customProfileSettingsLabel.json b/src/main/resources/swagger.api/schema/profile/customProfileSettingsLabel.json
new file mode 100644
index 000000000..c6c0b1f38
--- /dev/null
+++ b/src/main/resources/swagger.api/schema/profile/customProfileSettingsLabel.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Custom profile settings label",
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the workspace custom profile settings",
+ "x-field-extra-annotation": "@jakarta.validation.constraints.NotBlank"
+ }
+ },
+ "required": [
+ "name"
+ ]
+}
diff --git a/src/main/resources/swagger.api/schema/profile/customProfileSettingsMetadata.json b/src/main/resources/swagger.api/schema/profile/customProfileSettingsMetadata.json
new file mode 100644
index 000000000..583c70b4b
--- /dev/null
+++ b/src/main/resources/swagger.api/schema/profile/customProfileSettingsMetadata.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Custom profile settings metadata",
+ "type": "object",
+ "allOf": [
+ {
+ "$ref": "customProfileSettingsLabel.json"
+ },
+ {
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int16",
+ "description": "ID of the custom profile settings"
+ },
+ "profileId": {
+ "type": "integer",
+ "format": "int16",
+ "description": "ID of the profile"
+ }
+ },
+ "required": [
+ "id",
+ "profileId"
+ ]
+ }
+ ]
+}
diff --git a/src/main/resources/swagger.api/schema/profile/customProfileSettingsMetadataArray.json b/src/main/resources/swagger.api/schema/profile/customProfileSettingsMetadataArray.json
new file mode 100644
index 000000000..2e6cebda9
--- /dev/null
+++ b/src/main/resources/swagger.api/schema/profile/customProfileSettingsMetadataArray.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Array of custom profile settings metadata",
+ "type": "array",
+ "items": {
+ "$ref": "customProfileSettingsMetadata.json"
+ }
+}
diff --git a/src/main/resources/swagger.api/schema/profile/customProfileSettingsResponseDto.json b/src/main/resources/swagger.api/schema/profile/customProfileSettingsResponseDto.json
index 7db3bbd88..9082c8f13 100644
--- a/src/main/resources/swagger.api/schema/profile/customProfileSettingsResponseDto.json
+++ b/src/main/resources/swagger.api/schema/profile/customProfileSettingsResponseDto.json
@@ -1,22 +1,13 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Workspace custom profile settings response DTO",
+ "type": "object",
"allOf": [
{
- "$ref": "customProfileSettings.json"
+ "$ref": "customProfileSettingsValues.json"
},
{
- "type": "object",
- "properties": {
- "profileId": {
- "type": "integer",
- "format": "int16",
- "description": "ID of the profile"
- }
- },
- "required": [
- "profileId"
- ]
+ "$ref": "customProfileSettingsMetadata.json"
}
]
}
diff --git a/src/main/resources/swagger.api/schema/profile/customProfileSettingsValues.json b/src/main/resources/swagger.api/schema/profile/customProfileSettingsValues.json
new file mode 100644
index 000000000..edfda40a8
--- /dev/null
+++ b/src/main/resources/swagger.api/schema/profile/customProfileSettingsValues.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Workspace custom profile settings",
+ "type": "object",
+ "properties": {
+ "active": {
+ "type": "boolean",
+ "description": "True if settings are intended to be applied, false if intended to be ignored"
+ },
+ "children": {
+ "type": "array",
+ "description": "Profile block children",
+ "items": {
+ "$ref": "customProfileSettingsChild.json"
+ }
+ }
+ },
+ "required": [
+ "active",
+ "children"
+ ]
+}
diff --git a/src/main/resources/swagger.api/schema/profile/preferredProfileSettingsRequest.json b/src/main/resources/swagger.api/schema/profile/preferredProfileSettingsRequest.json
new file mode 100644
index 000000000..913a8c890
--- /dev/null
+++ b/src/main/resources/swagger.api/schema/profile/preferredProfileSettingsRequest.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Preferred profile settings request schema",
+ "type": "object",
+ "properties": {
+ "profileSettingsId": {
+ "type": "integer",
+ "format": "int16",
+ "description": "ID of the profile settings"
+ }
+ },
+ "required": [
+ "profileSettingsId"
+ ]
+}
diff --git a/src/test/java/org/folio/linked/data/e2e/endpoint/PreferredProfileSettingsIT.java b/src/test/java/org/folio/linked/data/e2e/endpoint/PreferredProfileSettingsIT.java
new file mode 100644
index 000000000..3f1ab1848
--- /dev/null
+++ b/src/test/java/org/folio/linked/data/e2e/endpoint/PreferredProfileSettingsIT.java
@@ -0,0 +1,227 @@
+package org.folio.linked.data.e2e.endpoint;
+
+import static java.util.UUID.randomUUID;
+import static org.folio.linked.data.test.TestUtil.defaultHeadersWithUserId;
+import static org.hamcrest.Matchers.equalTo;
+import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+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;
+
+import com.jayway.jsonpath.JsonPath;
+import org.folio.linked.data.e2e.base.IntegrationTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.env.Environment;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+
+@IntegrationTest
+class PreferredProfileSettingsIT {
+ private static final String PREFERRED_PROFILE_SETTINGS_URL = "/linked-data/profile/3/preferred";
+ private static final String PROFILE_SETTINGS_URL = "/linked-data/profile/3/settings";
+
+ @Autowired
+ private MockMvc mockMvc;
+ @Autowired
+ private Environment env;
+
+ @Test
+ void shouldSetPreferredProfileSettings() throws Exception {
+ // given
+ var headers = defaultHeadersWithUserId(env, randomUUID().toString());
+ headers.setContentType(APPLICATION_JSON);
+
+ var createProfileSettingsRequest = post(PROFILE_SETTINGS_URL)
+ .headers(headers)
+ .content("""
+ {
+ "name": "My settings",
+ "active": true,
+ "children": [
+ {
+ "id": "Profile:Resource:Property",
+ "visible": true,
+ "order": 1
+ }
+ ]
+ }""");
+ var createProfileSettingsResult = mockMvc.perform(createProfileSettingsRequest)
+ .andExpect(status().isCreated())
+ .andReturn();
+ var createProfileSettingsResultBody = createProfileSettingsResult.getResponse().getContentAsString();
+ var settingsId = JsonPath.read(createProfileSettingsResultBody, "$.id");
+
+ // when
+ var postRequest = post(PREFERRED_PROFILE_SETTINGS_URL)
+ .headers(headers)
+ .content("""
+ {
+ "profileSettingsId": %d
+ }""".formatted(settingsId));
+ mockMvc.perform(postRequest)
+ .andExpect(status().isNoContent());
+
+
+ // then
+ validatePreferredProfileSettings(mockMvc.perform(get(PREFERRED_PROFILE_SETTINGS_URL).headers(headers)));
+ validatePreferredProfileSettings(
+ mockMvc.perform(get(PREFERRED_PROFILE_SETTINGS_URL)
+ .headers(headers))
+ );
+ }
+
+ @Test
+ void shouldReturnEmptyPreferredProfileSettings() throws Exception {
+ // given
+ var headers = defaultHeadersWithUserId(env, randomUUID().toString());
+
+ // when
+ validateEmptyPreferredProfileSettings(mockMvc.perform(get(PREFERRED_PROFILE_SETTINGS_URL).headers(headers)));
+ }
+
+ @Test
+ void shouldDeletePreferredProfileSettings() throws Exception {
+ // given
+ var headers = defaultHeadersWithUserId(env, randomUUID().toString());
+ headers.setContentType(APPLICATION_JSON);
+
+ var createProfileSettingsRequest = post(PROFILE_SETTINGS_URL)
+ .headers(headers)
+ .content("""
+ {
+ "name": "My settings",
+ "active": true,
+ "children": [
+ {
+ "id": "Profile:Resource:Property",
+ "visible": true,
+ "order": 1
+ }
+ ]
+ }""");
+ var createProfileSettingsResult = mockMvc.perform(createProfileSettingsRequest)
+ .andExpect(status().isCreated())
+ .andReturn();
+ var createProfileSettingsResultBody = createProfileSettingsResult.getResponse().getContentAsString();
+ var settingsId = JsonPath.read(createProfileSettingsResultBody, "$.id");
+
+ var postRequest = post(PREFERRED_PROFILE_SETTINGS_URL)
+ .headers(headers)
+ .content("""
+ {
+ "profileSettingsId": %d
+ }""".formatted(settingsId));
+ mockMvc.perform(postRequest)
+ .andExpect(status().isNoContent());
+
+ validatePreferredProfileSettings(mockMvc.perform(get(PREFERRED_PROFILE_SETTINGS_URL).headers(headers)));
+
+ // when
+ mockMvc.perform(delete(PREFERRED_PROFILE_SETTINGS_URL)
+ .headers(headers))
+ .andExpect(status().isNoContent());
+
+ // then
+ validateEmptyPreferredProfileSettings(mockMvc.perform(get(PREFERRED_PROFILE_SETTINGS_URL).headers(headers)));
+ }
+
+ @Test
+ void shouldPreferredProfileSettingsDifferPerUser() throws Exception {
+ // given
+ var headersUser1 = defaultHeadersWithUserId(env, randomUUID().toString());
+ headersUser1.setContentType(APPLICATION_JSON);
+ var headersUser2 = defaultHeadersWithUserId(env, randomUUID().toString());
+ headersUser2.setContentType(APPLICATION_JSON);
+
+ var createProfileSettingsRequestUser1 = post(PROFILE_SETTINGS_URL)
+ .headers(headersUser1)
+ .content("""
+ {
+ "name": "settings for user 1",
+ "active": true,
+ "children": [
+ {
+ "id": "Profile:Resource:Property",
+ "visible": true,
+ "order": 1
+ }
+ ]
+ }""");
+ var createProfileSettingsResultUser1 = mockMvc.perform(createProfileSettingsRequestUser1)
+ .andExpect(status().isCreated())
+ .andReturn();
+ var createProfileSettingsResultBodyUser1 = createProfileSettingsResultUser1.getResponse().getContentAsString();
+ var settingsIdUser1 = JsonPath.read(createProfileSettingsResultBodyUser1, "$.id");
+
+ var createProfileSettingsRequestUser2 = post(PROFILE_SETTINGS_URL)
+ .headers(headersUser2)
+ .content("""
+ {
+ "name": "user 2 settings",
+ "active": true,
+ "children": [
+ {
+ "id": "Profile:Resource:Property",
+ "visible": true,
+ "order": 1
+ }
+ ]
+ }""");
+ var createProfileSettingsResultUser2 = mockMvc.perform(createProfileSettingsRequestUser2)
+ .andExpect(status().isCreated())
+ .andReturn();
+ var createProfileSettingsResultBodyUser2 = createProfileSettingsResultUser2.getResponse().getContentAsString();
+ var settingsIdUser2 = JsonPath.read(createProfileSettingsResultBodyUser2, "$.id");
+
+ // when: users set their own settings as preferred for profile 3
+ mockMvc.perform(post(PREFERRED_PROFILE_SETTINGS_URL)
+ .headers(headersUser1)
+ .content("""
+ {
+ "profileSettingsId": %d
+ }""".formatted(settingsIdUser1)))
+ .andExpect(status().isNoContent());
+
+ mockMvc.perform(post(PREFERRED_PROFILE_SETTINGS_URL)
+ .headers(headersUser2)
+ .content("""
+ {
+ "profileSettingsId": %d
+ }""".formatted(settingsIdUser2)))
+ .andExpect(status().isNoContent());
+
+ // then: each user sees only their own preferred profile settings
+ mockMvc.perform(get(PREFERRED_PROFILE_SETTINGS_URL).headers(headersUser1))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(APPLICATION_JSON))
+ .andExpect(jsonPath("$.length()", equalTo(1)))
+ .andExpect(jsonPath("$[0].id", equalTo(settingsIdUser1)))
+ .andExpect(jsonPath("$[0].name", equalTo("settings for user 1")));
+
+ mockMvc.perform(get(PREFERRED_PROFILE_SETTINGS_URL).headers(headersUser2))
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(APPLICATION_JSON))
+ .andExpect(jsonPath("$.length()", equalTo(1)))
+ .andExpect(jsonPath("$[0].id", equalTo(settingsIdUser2)))
+ .andExpect(jsonPath("$[0].name", equalTo("user 2 settings")));
+ }
+
+ private void validatePreferredProfileSettings(ResultActions result) throws Exception {
+ result
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(APPLICATION_JSON))
+ .andExpect(jsonPath("$[0].name", equalTo("My settings")))
+ .andExpect(jsonPath("$[0].profileId", equalTo(3)))
+ .andExpect(jsonPath("$.length()", equalTo(1)));
+ }
+
+ private void validateEmptyPreferredProfileSettings(ResultActions result) throws Exception {
+ result
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.length()", equalTo(0)));
+ }
+}
diff --git a/src/test/java/org/folio/linked/data/e2e/endpoint/ProfileSettingsIT.java b/src/test/java/org/folio/linked/data/e2e/endpoint/ProfileSettingsIT.java
index 77366a6b8..4ada2c990 100644
--- a/src/test/java/org/folio/linked/data/e2e/endpoint/ProfileSettingsIT.java
+++ b/src/test/java/org/folio/linked/data/e2e/endpoint/ProfileSettingsIT.java
@@ -1,16 +1,22 @@
package org.folio.linked.data.e2e.endpoint;
import static java.util.UUID.randomUUID;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.folio.linked.data.test.TestUtil.TEST_JSON_MAPPER;
import static org.folio.linked.data.test.TestUtil.defaultHeadersWithUserId;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.springframework.http.MediaType.APPLICATION_JSON;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
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;
+import com.jayway.jsonpath.JsonPath;
+import org.folio.linked.data.domain.dto.ErrorResponse;
import org.folio.linked.data.e2e.base.IntegrationTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -20,13 +26,29 @@
@IntegrationTest
class ProfileSettingsIT {
- private static final String PROFILE_SETTINGS_URL = "/linked-data/profile/settings/";
+ private static final String PROFILE_URL = "/linked-data/profile/";
+ private static final String SETTINGS_PATH = "/settings";
@Autowired
private MockMvc mockMvc;
@Autowired
private Environment env;
+ @Test
+ void shouldBeEmptyForProfileWithNoSettings() throws Exception {
+ // given
+ var headers = defaultHeadersWithUserId(env, randomUUID().toString());
+ headers.setContentType(APPLICATION_JSON);
+
+ // when, then
+ var getAllRequest = get(PROFILE_URL + "2" + SETTINGS_PATH)
+ .headers(headers);
+ mockMvc.perform(getAllRequest)
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(APPLICATION_JSON))
+ .andExpect(jsonPath("$").isEmpty());
+ }
+
@Test
void shouldBeNotFoundForUnknownProfile() throws Exception {
// given
@@ -34,7 +56,7 @@ void shouldBeNotFoundForUnknownProfile() throws Exception {
headers.setContentType(APPLICATION_JSON);
// when, then
- var getRequest = get(PROFILE_SETTINGS_URL + "9999999")
+ var getRequest = get(PROFILE_URL + "9999999" + SETTINGS_PATH + "/1")
.headers(headers);
mockMvc.perform(getRequest)
.andExpect(status().isNotFound());
@@ -47,7 +69,7 @@ void shouldBeInactiveSettingsWhenNotSet() throws Exception {
headers.setContentType(APPLICATION_JSON);
// when
- var getRequest = get(PROFILE_SETTINGS_URL + "2")
+ var getRequest = get(PROFILE_URL + "2" + SETTINGS_PATH + "/1")
.headers(headers);
// then
@@ -58,29 +80,33 @@ void shouldBeInactiveSettingsWhenNotSet() throws Exception {
}
@Test
- void shouldSetProfileSettings() throws Exception {
+ void shouldCreateProfileSettings() throws Exception {
// given
var headers = defaultHeadersWithUserId(env, randomUUID().toString());
headers.setContentType(APPLICATION_JSON);
- var postRequest = post(PROFILE_SETTINGS_URL + "2")
+ var postRequest = post(PROFILE_URL + "2" + SETTINGS_PATH)
.headers(headers)
.content("""
{
- "active": true,
- "children": [
- {
- "id": "Work:Monograph:Title",
- "visible": true,
- "order": 1
- }
- ]
+ "name": "My settings",
+ "active": true,
+ "children": [
+ {
+ "id": "Work:Monograph:Title",
+ "visible": true,
+ "order": 1
+ }
+ ]
}""");
- mockMvc.perform(postRequest)
- .andExpect(status().isNoContent());
+ var postResult = mockMvc.perform(postRequest)
+ .andExpect(status().isCreated())
+ .andReturn();
+ var postResultBody = postResult.getResponse().getContentAsString();
+ var settingsId = JsonPath.read(postResultBody, "$.id");
// when
- var getRequest = get(PROFILE_SETTINGS_URL + "2")
+ var getRequest = get(PROFILE_URL + "2" + SETTINGS_PATH + "/" + settingsId)
.headers(headers);
// then
@@ -90,4 +116,184 @@ void shouldSetProfileSettings() throws Exception {
.andExpect(jsonPath("$.active", is(true)))
.andExpect(jsonPath("$.children.length()", equalTo(1)));
}
+
+ @Test
+ void shouldRejectCreatingDuplicateNamedSettings() throws Exception {
+ // given
+ var headers = defaultHeadersWithUserId(env, randomUUID().toString());
+ headers.setContentType(APPLICATION_JSON);
+
+ var postRequest = post(PROFILE_URL + "2" + SETTINGS_PATH)
+ .headers(headers)
+ .content("""
+ {
+ "name": "My settings",
+ "active": true,
+ "children": [
+ {
+ "id": "Work:Monograph:Title",
+ "visible": true,
+ "order": 1
+ }
+ ]
+ }""");
+ mockMvc.perform(postRequest)
+ .andExpect(status().isCreated());
+
+ // when
+ var duplicatePostRequest = post(PROFILE_URL + "2" + SETTINGS_PATH)
+ .headers(headers)
+ .content("""
+ {
+ "name": "My settings",
+ "active": true,
+ "children": [
+ {
+ "id": "Work:Monograph:Title",
+ "visible": true,
+ "order": 1
+ },
+ {
+ "id": "Work:Monograph:OtherTitleInformation",
+ "visible": true,
+ "order": 2
+ }
+ ]
+ }
+ """);
+ var response = mockMvc.perform(duplicatePostRequest)
+ .andExpect(status().isBadRequest())
+ .andExpect(content().contentType(APPLICATION_JSON))
+ .andReturn().getResponse().getContentAsString();
+
+ var errorResponse = TEST_JSON_MAPPER.readValue(response, ErrorResponse.class);
+ assertThat(errorResponse.getErrors())
+ .extracting("code")
+ .contains("profile_settings_name_not_unique");
+ }
+
+ @Test
+ void shouldRejectBlankNamedSettings() throws Exception {
+ // given
+ var headers = defaultHeadersWithUserId(env, randomUUID().toString());
+ headers.setContentType(APPLICATION_JSON);
+
+ var postRequest = post(PROFILE_URL + "2" + SETTINGS_PATH)
+ .headers(headers)
+ .content("""
+ {
+ "name": "",
+ "active": true,
+ "children": [
+ {
+ "id": "Work:Monograph:Title",
+ "visible": true,
+ "order": 1
+ }
+ ]
+ }""");
+ var response = mockMvc.perform(postRequest)
+ .andExpect(status().isBadRequest())
+ .andExpect(content().contentType(APPLICATION_JSON))
+ .andReturn().getResponse().getContentAsString();
+
+ var errorResponse = TEST_JSON_MAPPER.readValue(response, ErrorResponse.class);
+ assertThat(errorResponse.getErrors())
+ .extracting("code")
+ .contains("must not be blank");
+ }
+
+ @Test
+ void shouldSetProfileSettings() throws Exception {
+ // given
+ var headers = defaultHeadersWithUserId(env, randomUUID().toString());
+ headers.setContentType(APPLICATION_JSON);
+
+ var postRequest = post(PROFILE_URL + "2" + SETTINGS_PATH)
+ .headers(headers)
+ .content("""
+ {
+ "name": "My settings",
+ "active": true,
+ "children": [
+ {
+ "id": "Work:Monograph:Title",
+ "visible": true,
+ "order": 1
+ }
+ ]
+ }""");
+ var postResult = mockMvc.perform(postRequest)
+ .andExpect(status().isCreated())
+ .andReturn();
+ var postResultBody = postResult.getResponse().getContentAsString();
+ var settingsId = JsonPath.read(postResultBody, "$.id");
+
+ // when
+ var putRequest = put(PROFILE_URL + "2" + SETTINGS_PATH + "/" + settingsId)
+ .headers(headers)
+ .content("""
+ {
+ "name": "My settings",
+ "active": true,
+ "children": [
+ {
+ "id": "Work:Monograph:Title",
+ "visible": true,
+ "order": 1
+ },
+ {
+ "id": "Work:Monograph:OtherTitleInformation",
+ "visible": true,
+ "order": 2
+ }
+ ]
+ }
+ """);
+ mockMvc.perform(putRequest)
+ .andExpect(status().isNoContent());
+
+ var getRequest = get(PROFILE_URL + "2" + SETTINGS_PATH + "/" + settingsId)
+ .headers(headers);
+
+ // then
+ mockMvc.perform(getRequest)
+ .andExpect(status().isOk())
+ .andExpect(content().contentType(APPLICATION_JSON))
+ .andExpect(jsonPath("$.active", is(true)))
+ .andExpect(jsonPath("$.children.length()", equalTo(2)));
+ }
+
+ @Test
+ void shouldDeleteProfileSettings() throws Exception {
+ // given
+ var headers = defaultHeadersWithUserId(env, randomUUID().toString());
+ headers.setContentType(APPLICATION_JSON);
+
+ var postRequest = post(PROFILE_URL + "2" + SETTINGS_PATH)
+ .headers(headers)
+ .content("""
+ {
+ "name": "My settings",
+ "active": true,
+ "children": [
+ {
+ "id": "Work:Monograph:Title",
+ "visible": true,
+ "order": 1
+ }
+ ]
+ }""");
+ var postResult = mockMvc.perform(postRequest)
+ .andExpect(status().isCreated())
+ .andReturn();
+ var postResultBody = postResult.getResponse().getContentAsString();
+ var settingsId = JsonPath.read(postResultBody, "$.id");
+
+ // when
+ var deleteRequest = delete(PROFILE_URL + "2" + SETTINGS_PATH + "/" + settingsId)
+ .headers(headers);
+ mockMvc.perform(deleteRequest)
+ .andExpect(status().isNoContent());
+ }
}
diff --git a/src/test/java/org/folio/linked/data/service/profile/ProfileSettingsServiceImplTest.java b/src/test/java/org/folio/linked/data/service/profile/ProfileSettingsServiceImplTest.java
index bff1b42ca..bb633fdb8 100644
--- a/src/test/java/org/folio/linked/data/service/profile/ProfileSettingsServiceImplTest.java
+++ b/src/test/java/org/folio/linked/data/service/profile/ProfileSettingsServiceImplTest.java
@@ -38,19 +38,56 @@ class ProfileSettingsServiceImplTest {
@Mock
private FolioExecutionContext executionContext;
+ @Test
+ void getAllProfileSettings_shouldReturnEmptyList_ifNoSettings() {
+ // given
+ var userId = UUID.randomUUID();
+ doReturn(userId).when(executionContext).getUserId();
+ var id = 1;
+ var profile = new Profile();
+ profile.setId(id);
+ when(profileRepository.findById(id)).thenReturn(Optional.of(profile));
+
+ // when
+ var settings = profileSettingsService.getAllProfileSettings(id);
+
+ // then
+ assertThat(settings).isEmpty();
+ }
+
+ @Test
+ void getAllProfileSettings_shouldThrowNotFound_ifNoSuchProfile() {
+ // given
+ var userId = UUID.randomUUID();
+ doReturn(userId).when(executionContext).getUserId();
+ var id = 1;
+ when(profileRepository.findById(id)).thenReturn(Optional.empty());
+ when(exceptionBuilder.notFoundLdResourceByIdException(anyString(), anyString()))
+ .thenReturn(emptyRequestProcessingException());
+
+ // when
+ var thrown = assertThrows(RequestProcessingException.class,
+ () -> profileSettingsService.getAllProfileSettings(id));
+
+ // then
+ assertThat(thrown.getClass()).isEqualTo(RequestProcessingException.class);
+ assertThat(thrown.getMessage()).isEmpty();
+ }
+
@Test
void getProfileSettings_shouldThrowNotFound_ifNoSuchProfile() {
// given
var userId = UUID.randomUUID();
doReturn(userId).when(executionContext).getUserId();
var id = 1;
+ var settingsId = 1;
when(profileRepository.findById(id)).thenReturn(Optional.empty());
when(exceptionBuilder.notFoundLdResourceByIdException(anyString(), anyString()))
.thenReturn(emptyRequestProcessingException());
// when
var thrown = assertThrows(RequestProcessingException.class,
- () -> profileSettingsService.getProfileSettings(id));
+ () -> profileSettingsService.getProfileSettings(id, settingsId));
// then
assertThat(thrown.getClass()).isEqualTo(RequestProcessingException.class);
@@ -66,33 +103,76 @@ void getProfileSettings_shouldReturnInactiveSettings_ifNoSuchSettings() {
var profile = new Profile();
profile.setId(id);
when(profileRepository.findById(id)).thenReturn(Optional.of(profile));
+ var profileSettingsId = 2;
// when
- var settings = profileSettingsService.getProfileSettings(id);
+ var settings = profileSettingsService.getProfileSettings(id, profileSettingsId);
// then
assertThat(settings.getActive()).isFalse();
assertThat(settings.getChildren()).isNull();
}
+ @Test
+ void createProfileSettings_shouldThrowNotFound_ifNoSuchProfile() {
+ // given
+ var userId = UUID.randomUUID();
+ doReturn(userId).when(executionContext).getUserId();
+ var id = 1;
+ var name = "name";
+ when(profileRepository.findById(id)).thenReturn(Optional.empty());
+ when(exceptionBuilder.notFoundLdResourceByIdException(anyString(), anyString()))
+ .thenReturn(emptyRequestProcessingException());
+ var settings = new CustomProfileSettingsRequestDto(name, false, null);
+
+ // when
+ var thrown = assertThrows(RequestProcessingException.class,
+ () -> profileSettingsService.createProfileSettings(id, settings));
+
+ // then
+ assertThat(thrown.getClass()).isEqualTo(RequestProcessingException.class);
+ assertThat(thrown.getMessage()).isEmpty();
+ }
+
@Test
void setProfileSettings_shouldThrowNotFound_ifNoSuchProfile() {
// given
var userId = UUID.randomUUID();
doReturn(userId).when(executionContext).getUserId();
var id = 1;
+ var name = "name";
when(profileRepository.findById(id)).thenReturn(Optional.empty());
when(exceptionBuilder.notFoundLdResourceByIdException(anyString(), anyString()))
.thenReturn(emptyRequestProcessingException());
- var settings = new CustomProfileSettingsRequestDto(false, null);
+ var profileSettingsId = 5;
+ var settings = new CustomProfileSettingsRequestDto(name, false, null);
// when
var thrown = assertThrows(RequestProcessingException.class,
- () -> profileSettingsService.setProfileSettings(id, settings));
+ () -> profileSettingsService.setProfileSettings(id, profileSettingsId, settings));
// then
assertThat(thrown.getClass()).isEqualTo(RequestProcessingException.class);
assertThat(thrown.getMessage()).isEmpty();
}
+ @Test
+ void deleteProfileSettings_shouldThrowNotFound_ifNoSuchProfile() {
+ // given
+ var userId = UUID.randomUUID();
+ doReturn(userId).when(executionContext).getUserId();
+ var id = 1;
+ when(profileRepository.findById(id)).thenReturn(Optional.empty());
+ when(exceptionBuilder.notFoundLdResourceByIdException(anyString(), anyString()))
+ .thenReturn(emptyRequestProcessingException());
+ var profileSettingsId = 5;
+
+ // when
+ var thrown = assertThrows(RequestProcessingException.class,
+ () -> profileSettingsService.deleteProfileSettings(id, profileSettingsId));
+
+ // then
+ assertThat(thrown.getClass()).isEqualTo(RequestProcessingException.class);
+ assertThat(thrown.getMessage()).isEmpty();
+ }
}
diff --git a/src/test/java/org/folio/linked/data/validation/dto/ProfileSettingsNameUniquenessValidatorTest.java b/src/test/java/org/folio/linked/data/validation/dto/ProfileSettingsNameUniquenessValidatorTest.java
new file mode 100644
index 000000000..9718ddbff
--- /dev/null
+++ b/src/test/java/org/folio/linked/data/validation/dto/ProfileSettingsNameUniquenessValidatorTest.java
@@ -0,0 +1,85 @@
+package org.folio.linked.data.validation.dto;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import jakarta.validation.ConstraintValidatorContext;
+import jakarta.validation.ConstraintValidatorContext.ConstraintViolationBuilder;
+import java.util.List;
+import org.folio.linked.data.domain.dto.CustomProfileSettingsRequestDto;
+import org.folio.linked.data.model.CreateProfileSettingsRequest;
+import org.folio.linked.data.service.profile.ProfileSettingsService;
+import org.folio.spring.testing.type.UnitTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@UnitTest
+@ExtendWith(MockitoExtension.class)
+class ProfileSettingsNameUniquenessValidatorTest {
+
+ private static final String SETTINGS_NAME = "My settings";
+
+ @Mock
+ private ProfileSettingsService profileSettingsService;
+
+ @Mock
+ private ConstraintValidatorContext context;
+
+ @Mock
+ private ConstraintViolationBuilder builder;
+
+ @Mock
+ private ConstraintViolationBuilder.NodeBuilderCustomizableContext nodeBuilder;
+
+ @InjectMocks
+ private ProfileSettingsNameUniquenessValidator validator;
+
+ @Test
+ void shouldReturnTrue_ifNameIsUniqueForProfile() {
+ when(profileSettingsService.nameExistsForProfile(any(), any())).thenReturn(false);
+
+ assertTrue(validator.isValid(newCreateProfileSettingsRequest(1, SETTINGS_NAME, true), context));
+ verifyNoInteractions(context);
+ }
+
+ @Test
+ void shouldReturnTrue_ifMissingProfileId() {
+ assertTrue(validator.isValid(newCreateProfileSettingsRequest(null, SETTINGS_NAME, true), context));
+ verifyNoInteractions(context);
+ }
+
+ @Test
+ void shouldReturnTrue_ifMissingDto() {
+ assertTrue(validator.isValid(newCreateProfileSettingsRequest(1, SETTINGS_NAME, false), context));
+ verifyNoInteractions(context);
+ }
+
+ @Test
+ void shouldReturnFalse_ifNameIsNotUniqueForProfile() {
+ when(profileSettingsService.nameExistsForProfile(any(), any())).thenReturn(true);
+ when(context.buildConstraintViolationWithTemplate(any())).thenReturn(builder);
+ when(builder.addPropertyNode(any())).thenReturn(nodeBuilder);
+ when(nodeBuilder.addPropertyNode(any())).thenReturn(nodeBuilder);
+
+ assertFalse(validator.isValid(newCreateProfileSettingsRequest(1, SETTINGS_NAME, true), context));
+ verify(context, times(1)).disableDefaultConstraintViolation();
+ verify(context, times(1)).buildConstraintViolationWithTemplate(any());
+ verify(nodeBuilder, times(1)).addConstraintViolation();
+ }
+
+ private CreateProfileSettingsRequest newCreateProfileSettingsRequest(Integer profileId, String name, boolean hasDto) {
+ CustomProfileSettingsRequestDto dto = null;
+ if (hasDto) {
+ dto = new CustomProfileSettingsRequestDto(name, true, List.of());
+ }
+ return new CreateProfileSettingsRequest(profileId, dto);
+ }
+}