Skip to content

Commit

Permalink
[MODCON-127] - Implement ECS tenant soft deletion functionality (#138)
Browse files Browse the repository at this point in the history
* [MODCON-127] - Implement ECS tenant soft deletion functionality

* [MODCON-127] - Implement ECS tenant soft deletion functionality

* [MODCON-127] - Implement ECS tenant soft deletion functionality

* [MODCON-127] - Covered with Unit Tests

* [MODCON-127] - Fixed unit tests

* [MODCON-127] - Covered with Unit Tests

* [MODCON-127] - Added filter to get user methods

* [MODCON-130] - Adjust process to add ECS tenant with soft deleted functionality

* [MODCON-130] - Minor improvements

* Revert "[MODCON-130] - Adjust process to add ECS tenant with soft deleted functionality"

This reverts commit 7e9ff61.
  • Loading branch information
azizbekxm authored Dec 22, 2023
1 parent 9c00a4f commit 8acfed4
Show file tree
Hide file tree
Showing 16 changed files with 156 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.folio.spring.config.FeignClientConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

Expand All @@ -12,4 +13,7 @@ public interface UserTenantsClient {

@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
void postUserTenant(@RequestBody UserTenant userTenant);

@DeleteMapping
void deleteUserTenants();
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public Tenant convert(TenantEntity source) {
tenant.setCode(source.getCode());
tenant.setName(source.getName());
tenant.setIsCentral(source.getIsCentral());
tenant.setIsDeleted(source.getIsDeleted());
return tenant;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,19 @@ public abstract class AbstractTenantEntity extends AuditableEntity {
private String name;
private UUID consortiumId;
private Boolean isCentral;
private Boolean isDeleted;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AbstractTenantEntity that)) return false;
return Objects.equals(id, that.id) && Objects.equals(code, that.code) && Objects.equals(name, that.name) && Objects.equals(consortiumId, that.consortiumId) && Objects.equals(isCentral, that.isCentral);
return Objects.equals(id, that.id) && Objects.equals(code, that.code) &&
Objects.equals(name, that.name) && Objects.equals(consortiumId, that.consortiumId) &&
Objects.equals(isCentral, that.isCentral) && Objects.equals(isDeleted, that.isDeleted);
}

@Override
public int hashCode() {
return Objects.hash(id, code, name, consortiumId, isCentral);
return Objects.hash(id, code, name, consortiumId, isCentral, isDeleted);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
@Repository
public interface TenantRepository extends JpaRepository<TenantEntity, String> {

@Query("SELECT t FROM TenantEntity t WHERE t.consortiumId = ?1 and (t.isDeleted IS NULL OR t.isDeleted = FALSE)")
Page<TenantEntity> findByConsortiumId(UUID consortiumId, Pageable pageable);

@Query("SELECT t FROM TenantEntity t WHERE t.consortiumId = ?1 and (t.isDeleted IS NULL OR t.isDeleted = FALSE)")
List<TenantEntity> findByConsortiumId(UUID consortiumId);

@Query("SELECT t FROM TenantEntity t where t.isCentral = true")
@Query("SELECT t FROM TenantEntity t WHERE t.isCentral = true")
Optional<TenantEntity> findCentralTenant();

boolean existsByIsCentralTrue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@

@Repository
public interface UserTenantRepository extends JpaRepository<UserTenantEntity, UUID> {

@Query("SELECT ut FROM UserTenantEntity ut WHERE ut.tenant.isDeleted IS NULL OR ut.tenant.isDeleted= FALSE")
Page<UserTenantEntity> getAll(Pageable pageable);

@Query("SELECT ut FROM UserTenantEntity ut WHERE ut.userId= ?1 AND (ut.tenant.isDeleted IS NULL OR ut.tenant.isDeleted= FALSE)")
Page<UserTenantEntity> findByUserId(UUID userId, Pageable pageable);

@Query("SELECT ut FROM UserTenantEntity ut WHERE ut.username= ?1 AND ut.tenant.id= ?2")
@Query("SELECT ut FROM UserTenantEntity ut WHERE ut.userId= ?1")
Page<UserTenantEntity> findAnyByUserId(UUID userId, Pageable pageable);

@Query("SELECT ut FROM UserTenantEntity ut WHERE ut.username= ?1 AND ut.tenant.id= ?2 AND (ut.tenant.isDeleted IS NULL OR ut.tenant.isDeleted= FALSE)")
Optional<UserTenantEntity> findByUsernameAndTenantId(String username, String tenantId);

@Query("SELECT ut FROM UserTenantEntity ut WHERE ut.userId= ?1 AND ut.tenant.id= ?2")
Optional<UserTenantEntity> findByUserIdAndTenantId(UUID userId, String tenantId);

boolean existsByTenantId(String tenantId);

@Query("SELECT ut FROM UserTenantEntity ut WHERE ut.userId= ?1 AND ut.isPrimary= true")
Optional<UserTenantEntity> findByUserIdAndIsPrimaryTrue(UUID userId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ private void createPrimaryUserAffiliations(UUID consortiumId, String centralTena
var user = userList.get(idx);
try {
log.info("createPrimaryUserAffiliations:: Processing users: {} of {}", idx + 1, userList.size());
Page<UserTenantEntity> userTenantPage = userTenantRepository.findByUserId(UUID.fromString(user.getId()), PageRequest.of(0, 1));
Page<UserTenantEntity> userTenantPage = userTenantRepository.findAnyByUserId(UUID.fromString(user.getId()), PageRequest.of(0, 1));

if (userTenantPage.getTotalElements() > 0) {
log.info("createPrimaryUserAffiliations:: Primary affiliation already exists for tenant/user: {}/{}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ public class TenantServiceImpl implements TenantService {
private static final String SHADOW_ADMIN_PERMISSION_FILE_PATH = "permissions/admin-user-permissions.csv";
private static final String SHADOW_SYSTEM_USER_PERMISSION_FILE_PATH = "permissions/system-user-permissions.csv";
private static final String TENANTS_IDS_NOT_MATCHED_ERROR_MSG = "Request body tenantId and path param tenantId should be identical";
private static final String TENANT_HAS_ACTIVE_USER_ASSOCIATIONS_ERROR_MSG = "Cannot delete tenant with ID {tenantId} because it has an association with a user. "
+ "Please remove the user association before attempting to delete the tenant.";

private static final String DUMMY_USERNAME = "dummy_user";
@Value("${folio.system-user.username}")
private String systemUserUsername;
Expand Down Expand Up @@ -91,8 +90,7 @@ public TenantCollection get(UUID consortiumId, Integer offset, Integer limit) {
public TenantCollection getAll(UUID consortiumId) {
TenantCollection result = new TenantCollection();
List<Tenant> list = tenantRepository.findByConsortiumId(consortiumId)
.stream()
.map(o -> converter.convert(o, Tenant.class)).toList();
.stream().map(o -> converter.convert(o, Tenant.class)).toList();
result.setTenants(list);
result.setTotalRecords(list.size());
return result;
Expand Down Expand Up @@ -193,13 +191,24 @@ public void updateTenantSetupStatus(String tenantId, String centralTenantId, Set
@Transactional
public void delete(UUID consortiumId, String tenantId) {
consortiumService.checkConsortiumExistsOrThrow(consortiumId);
checkTenantExistsOrThrow(tenantId);
if (userTenantRepository.existsByTenantId(tenantId)) {
throw new IllegalArgumentException(TENANT_HAS_ACTIVE_USER_ASSOCIATIONS_ERROR_MSG);
var tenant = tenantRepository.findById(tenantId);

if (tenant.isEmpty()) {
throw new ResourceNotFoundException("id", tenantId);
}
if (Boolean.TRUE.equals(tenant.get().getIsCentral())) {
throw new IllegalArgumentException(String.format("central tenant '%s' cannot be deleted", tenantId));
}

var softDeletedTenant = tenant.get();
softDeletedTenant.setIsDeleted(true);
// clean publish coordinator tables first, because after tenant removal it will be ignored by cleanup service
cleanupService.clearPublicationTables();
tenantRepository.deleteById(tenantId);
tenantRepository.save(softDeletedTenant);

try (var ignored = new FolioExecutionContextSetter(contextHelper.getSystemUserFolioExecutionContext(tenantId))) {
userTenantsClient.deleteUserTenants();
}
}

private Tenant saveTenant(UUID consortiumId, Tenant tenantDto, SetupStatusEnum setupStatus) {
Expand Down Expand Up @@ -300,6 +309,7 @@ private TenantEntity toTenantEntity(UUID consortiumId, Tenant tenantDto) {
entity.setCode(tenantDto.getCode());
entity.setIsCentral(tenantDto.getIsCentral());
entity.setConsortiumId(consortiumId);
entity.setIsDeleted(tenantDto.getIsDeleted());
return entity;
}

Expand All @@ -311,6 +321,7 @@ private TenantDetailsEntity toTenantDetailsEntity(UUID consortiumId, Tenant tena
entity.setIsCentral(tenantDto.getIsCentral());
entity.setConsortiumId(consortiumId);
entity.setSetupStatus(setupStatus);
entity.setIsDeleted(tenantDto.getIsDeleted());
return entity;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ public class UserTenantServiceImpl implements UserTenantService {
public UserTenantCollection get(UUID consortiumId, Integer offset, Integer limit) {
consortiumService.checkConsortiumExistsOrThrow(consortiumId);
var result = new UserTenantCollection();
Page<UserTenantEntity> userTenantPage = userTenantRepository.findAll(PageRequest.of(offset, limit));
result.setUserTenants(userTenantPage.stream().map(o -> converter.convert(o, UserTenant.class)).toList());
Page<UserTenantEntity> userTenantPage = userTenantRepository.getAll(PageRequest.of(offset, limit));
result.setUserTenants(userTenantPage.map(o -> converter.convert(o, UserTenant.class)).getContent());
result.setTotalRecords((int) userTenantPage.getTotalElements());
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,12 @@
<column name="setup_status" type="setup_status"/>
</addColumn>
</changeSet>

<changeSet id="MOCON-127@add-is-deleted-flag" author="azizbekxm">
<addColumn tableName="tenant">
<column name="is_deleted" type="boolean" defaultValueBoolean="false">
<constraints unique="false" />
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
2 changes: 2 additions & 0 deletions src/main/resources/swagger.api/schemas/tenant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Tenant:
maxLength: 150
isCentral:
type: boolean
isDeleted:
type: boolean
additionalProperties: false
required:
- id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,20 +379,23 @@ void shouldThrowNotFoundErrorWhileUpdateTenant(String contentString) throws Exce
jsonPath("$.errors[0].code", is("NOT_FOUND_ERROR")));
}

@ParameterizedTest
@ValueSource(strings = {TENANT_REQUEST_BODY})
void shouldThrownHasActiveAffiliationExceptionWhileDeletingTenant(String contentString) throws Exception {
@Test
void shouldThrownExceptionWhenDeletingCentralTenant() throws Exception {
var headers = defaultHeaders();
when(tenantRepository.existsById(any())).thenReturn(true);
String tenantId = "diku";
var centralTenant = createTenantEntity(tenantId);
centralTenant.setIsCentral(true);

when(tenantRepository.findById(any())).thenReturn(Optional.of(centralTenant));
when(consortiumRepository.existsById(any())).thenReturn(true);
when(userTenantRepository.existsByTenantId("diku1234")).thenReturn(true);

this.mockMvc.perform(
delete("/consortia/7698e46-c3e3-11ed-afa1-0242ac120002/tenants/diku1234")
.headers(headers).content(contentString))
delete("/consortia/7698e46-c3e3-11ed-afa1-0242ac120002/tenants/diku")
.headers(headers))
.andExpectAll(
status().is4xxClientError(),
jsonPath("$.errors[0].code", is("VALIDATION_ERROR")));
jsonPath("$.errors[0].code", is("VALIDATION_ERROR")),
jsonPath("$.errors[0].message", is("central tenant 'diku' cannot be deleted")));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class UserTenantControllerTest extends BaseIT {
private static final String PERMISSION_EXCEPTION_MSG = "[403 Forbidden] during [GET] to " +
"[http://users/8c54ff1e-5954-4227-8402-9a5dd061a350] [UsersClient#getUserById(String)]: " +
"[Access for user 'ss_admin' (b82b46b6-9a6e-46f0-b986-5c643d9ba036) requires permission: users.item.get]";

@Mock
private UserTenantService userTenantService;
@InjectMocks
Expand Down Expand Up @@ -122,7 +123,7 @@ void shouldGetUserTenantList() throws Exception {
Page<UserTenantEntity> userTenantPage = new PageImpl<>(List.of(createUserTenantEntity(consortiumId)));

when(consortiumRepository.existsById(consortiumId)).thenReturn(true);
when(userTenantRepository.findAll(PageRequest.of(1, 2))).thenReturn(userTenantPage);
when(userTenantRepository.getAll(PageRequest.of(1, 2))).thenReturn(userTenantPage);

this.mockMvc.perform(
get("/consortia/7698e46-c3e3-11ed-afa1-0242ac120002/user-tenants?limit=2&offset=1")
Expand Down Expand Up @@ -223,6 +224,7 @@ private Response createForbiddenResponse(String message) {
.request(request)
.build();
}

private Response createUnknownResponse(String message) {
Request request = Request.create(Request.HttpMethod.GET, "", Map.of(), null, Charset.defaultCharset(),
new RequestTemplate());
Expand Down
Loading

0 comments on commit 8acfed4

Please sign in to comment.