Skip to content

Commit

Permalink
[MODFQMMGR-333] Expose endpoint to fetch entity types with missing pe…
Browse files Browse the repository at this point in the history
…rmissions (#288)

* [MODFQMMGR-333] Expose endpoint to fetch entity types with missing permissions

* test with includeInaccessible=true

* more exhaustive tests

* formatting

* Unbox in the controller

* remove debug imports

* un-parameterize
  • Loading branch information
ncovercash committed Jun 17, 2024
1 parent 5d837f3 commit c6f8b77
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public Optional<EntityType> getEntityTypeDefinition(UUID entityTypeId) {
});
}

public List<RawEntityTypeSummary> getEntityTypeSummary(Set<UUID> entityTypeIds) {
public List<RawEntityTypeSummary> getEntityTypeSummaries(Set<UUID> entityTypeIds) {
log.info("Fetching entityTypeSummary for ids: {}", entityTypeIds);
Field<String> definitionField = field(DEFINITION_FIELD_NAME, String.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ public ResponseEntity<EntityType> getEntityType(UUID entityTypeId) {
}

@Override
public ResponseEntity<List<EntityTypeSummary>> getEntityTypeSummary(List<UUID> entityTypeIds) {
public ResponseEntity<List<EntityTypeSummary>> getEntityTypeSummary(List<UUID> entityTypeIds, Boolean includeInaccessible) {
Set<UUID> idsSet = entityTypeIds == null ? Set.of() : Set.copyOf(entityTypeIds);
// Permissions are handled in the service layer
return ResponseEntity.ok(entityTypeService.getEntityTypeSummary(idsSet));
return ResponseEntity.ok(entityTypeService.getEntityTypeSummary(idsSet, Boolean.TRUE.equals(includeInaccessible)));
}

@EntityTypePermissionsRequired
Expand Down
26 changes: 19 additions & 7 deletions src/main/java/org/folio/fqm/service/EntityTypeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,29 @@ public class EntityTypeService {
* @param entityTypeIds If provided, only the entity types having the provided Ids will be included in the results
*/
@Transactional(readOnly = true)
public List<EntityTypeSummary> getEntityTypeSummary(Set<UUID> entityTypeIds) {
public List<EntityTypeSummary> getEntityTypeSummary(Set<UUID> entityTypeIds, boolean includeInaccessible) {
Set<String> userPermissions = permissionsService.getUserPermissions();
return entityTypeRepository
.getEntityTypeSummary(entityTypeIds)
.getEntityTypeSummaries(entityTypeIds)
.stream()
.filter(entityTypeSummary -> userPermissions.containsAll(entityTypeSummary.requiredPermissions()))
.map(rawEntityTypeSummary ->
new EntityTypeSummary()
.filter(entityTypeSummary -> includeInaccessible || userPermissions.containsAll(entityTypeSummary.requiredPermissions()))
.map(rawEntityTypeSummary -> {
EntityTypeSummary result = new EntityTypeSummary()
.id(rawEntityTypeSummary.id())
.label(localizationService.getEntityTypeLabel(rawEntityTypeSummary.name()))
)
.label(localizationService.getEntityTypeLabel(rawEntityTypeSummary.name()));

if (includeInaccessible) {
return result.missingPermissions(
rawEntityTypeSummary
.requiredPermissions()
.stream()
.filter(permission -> !userPermissions.contains(permission))
.toList()
);
}

return result;
})
.sorted(Comparator.comparing(EntityTypeSummary::getLabel, String.CASE_INSENSITIVE_ORDER))
.toList();
}
Expand Down
8 changes: 8 additions & 0 deletions src/main/resources/swagger.api/mod-fqm-manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ paths:
description: Get names for a list of entity type ids.
parameters:
- $ref: '#/components/parameters/entity-type-ids'
- $ref: '#/components/parameters/include-inaccessible'
responses:
'200':
description: 'Entity type summaries'
Expand Down Expand Up @@ -79,6 +80,13 @@ components:
items:
type: string
format: UUID
include-inaccessible:
name: includeInaccessible
in: query
required: false
description: Include inaccessible entity types in the result
schema:
type: boolean
schemas:
errorResponse:
$ref: schemas/errors.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
"label" : {
"description": "Entity type label",
"type": "string"
},
"missingPermissions": {
"description": "List of missing permissions",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
Expand Down
132 changes: 88 additions & 44 deletions src/test/java/org/folio/fqm/controller/EntityTypeControllerTest.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package org.folio.fqm.controller;

import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.util.*;
import org.folio.fqm.domain.dto.EntityTypeSummary;
import org.folio.fqm.exception.EntityTypeNotFoundException;
import org.folio.fqm.exception.FieldNotFoundException;
import org.folio.fqm.resource.EntityTypeController;
import org.folio.fqm.service.EntityTypeService;
import org.folio.fqm.domain.dto.EntityTypeSummary;
import org.folio.querytool.domain.dto.ColumnValues;
import org.folio.querytool.domain.dto.EntityType;
import org.folio.querytool.domain.dto.EntityTypeColumn;
Expand All @@ -14,31 +22,25 @@
import org.folio.spring.FolioExecutionContext;
import org.folio.spring.integration.XOkapiHeaders;
import org.junit.jupiter.api.Test;

import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import java.util.*;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.hamcrest.Matchers.is;

@WebMvcTest(EntityTypeController.class)
class EntityTypeControllerTest {
private final static String GET_DEFINITION_URL = "/entity-types/{entity-type-id}";

private static final String GET_DEFINITION_URL = "/entity-types/{entity-type-id}";

@Autowired
private MockMvc mockMvc;

@MockBean
private EntityTypeService entityTypeService;

@MockBean
private FolioExecutionContext folioExecutionContext;

Expand All @@ -50,9 +52,12 @@ void shouldReturnEntityTypeDefinition() throws Exception {
EntityType mockDefinition = getEntityType(col);
when(folioExecutionContext.getTenantId()).thenReturn("tenant_01");
when(entityTypeService.getEntityTypeDefinition(id)).thenReturn(Optional.of(mockDefinition));
RequestBuilder builder = MockMvcRequestBuilders.get(GET_DEFINITION_URL, id).accept(MediaType.APPLICATION_JSON).
header(XOkapiHeaders.TENANT, "tenant_01");
mockMvc.perform(builder)
RequestBuilder builder = MockMvcRequestBuilders
.get(GET_DEFINITION_URL, id)
.accept(MediaType.APPLICATION_JSON)
.header(XOkapiHeaders.TENANT, "tenant_01");
mockMvc
.perform(builder)
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", is(derivedTableName)))
.andExpect(jsonPath("$.labelAlias", is(mockDefinition.getLabelAlias())))
Expand All @@ -67,7 +72,10 @@ void shouldReturnNotFoundErrorWhenEntityNotFound() throws Exception {
UUID id = UUID.randomUUID();
when(folioExecutionContext.getTenantId()).thenReturn("tenant_01");
when(entityTypeService.getEntityTypeDefinition(UUID.randomUUID())).thenReturn(Optional.empty());
RequestBuilder builder = MockMvcRequestBuilders.get(GET_DEFINITION_URL, id).accept(MediaType.APPLICATION_JSON).header(XOkapiHeaders.TENANT, "tenant_01");
RequestBuilder builder = MockMvcRequestBuilders
.get(GET_DEFINITION_URL, id)
.accept(MediaType.APPLICATION_JSON)
.header(XOkapiHeaders.TENANT, "tenant_01");
mockMvc.perform(builder).andExpect(status().isNotFound());
}

Expand All @@ -78,17 +86,42 @@ void shouldGetEntityTypeSummaryForValidIds() throws Exception {
Set<UUID> ids = Set.of(id1, id2);
List<EntityTypeSummary> expectedSummary = List.of(
new EntityTypeSummary().id(id1).label("label_01"),
new EntityTypeSummary().id(id2).label("label_02"));
RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/entity-types")
new EntityTypeSummary().id(id2).label("label_02")
);
RequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/entity-types")
.header(XOkapiHeaders.TENANT, "tenant_01")
.queryParam("ids", id1.toString(), id2.toString());
when(entityTypeService.getEntityTypeSummary(ids)).thenReturn(expectedSummary);
mockMvc.perform(requestBuilder)
when(entityTypeService.getEntityTypeSummary(ids, false)).thenReturn(expectedSummary);
mockMvc
.perform(requestBuilder)
.andExpect(status().isOk())
.andExpect(jsonPath("$.[0].id", is(expectedSummary.get(0).getId().toString())))
.andExpect(jsonPath("$.[0].label", is(expectedSummary.get(0).getLabel())))
.andExpect(jsonPath("$.[0].missingPermissions").doesNotExist())
.andExpect(jsonPath("$.[1].id", is(expectedSummary.get(1).getId().toString())))
.andExpect(jsonPath("$.[1].label", is(expectedSummary.get(1).getLabel())));
.andExpect(jsonPath("$.[1].label", is(expectedSummary.get(1).getLabel())))
.andExpect(jsonPath("$.[1].missingPermissions").doesNotExist());

verify(entityTypeService, times(1)).getEntityTypeSummary(ids, false);
verifyNoMoreInteractions(entityTypeService);
}

@Test
void testSummaryIncludesMissingPermissionsIfRequested() throws Exception {
RequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/entity-types")
.header(XOkapiHeaders.TENANT, "tenant_01")
.queryParam("includeInaccessible", "true");

when(entityTypeService.getEntityTypeSummary(Set.of(), true)).thenReturn(List.of());

// all we really want to check here is that the includeInaccessible parameter is correctly unboxed
// no sense making fake data to pass through to ourself; that's redundant with shouldGetEntityTypeSummaryForValidIds
mockMvc.perform(requestBuilder).andExpect(status().isOk());

verify(entityTypeService, times(1)).getEntityTypeSummary(Set.of(), true);
verifyNoMoreInteractions(entityTypeService);
}

@Test
Expand All @@ -97,13 +130,12 @@ void shouldReturnEmptyListWhenEntityTypeSummaryNotFound() throws Exception {
UUID id2 = UUID.randomUUID();
Set<UUID> ids = Set.of(id1, id2);
List<EntityTypeSummary> expectedSummary = List.of();
RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/entity-types")
RequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/entity-types")
.header(XOkapiHeaders.TENANT, "tenant_01")
.queryParam("ids", id1.toString(), id2.toString());
when(entityTypeService.getEntityTypeSummary(ids)).thenReturn(expectedSummary);
mockMvc.perform(requestBuilder)
.andExpect(status().isOk())
.andExpect(jsonPath("$", is(expectedSummary)));
when(entityTypeService.getEntityTypeSummary(ids, false)).thenReturn(expectedSummary);
mockMvc.perform(requestBuilder).andExpect(status().isOk()).andExpect(jsonPath("$", is(expectedSummary)));
}

@Test
Expand All @@ -115,11 +147,14 @@ void shouldReturnColumnValuesWithLabel() throws Exception {
new ValueWithLabel().value("value_01").label("label_01"),
new ValueWithLabel().value("value_02").label("label_02")
);
RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
RequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
.accept(MediaType.APPLICATION_JSON)
.header(XOkapiHeaders.TENANT, "tenant_01");
when(entityTypeService.getFieldValues(entityTypeId, columnName, null)).thenReturn(columnValues.content(expectedColumnValueLabel));
mockMvc.perform(requestBuilder)
when(entityTypeService.getFieldValues(entityTypeId, columnName, null))
.thenReturn(columnValues.content(expectedColumnValueLabel));
mockMvc
.perform(requestBuilder)
.andExpect(status().isOk())
.andExpect(jsonPath("$.content[0].value", is(expectedColumnValueLabel.get(0).getValue())))
.andExpect(jsonPath("$.content[0].label", is(expectedColumnValueLabel.get(0).getLabel())))
Expand All @@ -136,12 +171,15 @@ void shouldReturnColumnValuesWithLabelWithSearch() throws Exception {
new ValueWithLabel().value("value_01").label("label_01"),
new ValueWithLabel().value("value_02").label("label_02")
);
RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
RequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
.accept(MediaType.APPLICATION_JSON)
.header(XOkapiHeaders.TENANT, "tenant_01")
.queryParam("search", "label_01");
when(entityTypeService.getFieldValues(entityTypeId, columnName, "label_01")).thenReturn(columnValues.content(expectedColumnValueLabel));
mockMvc.perform(requestBuilder)
when(entityTypeService.getFieldValues(entityTypeId, columnName, "label_01"))
.thenReturn(columnValues.content(expectedColumnValueLabel));
mockMvc
.perform(requestBuilder)
.andExpect(status().isOk())
.andExpect(jsonPath("$.content[0].value", is(expectedColumnValueLabel.get(0).getValue())))
.andExpect(jsonPath("$.content[0].label", is(expectedColumnValueLabel.get(0).getLabel())));
Expand All @@ -156,11 +194,14 @@ void shouldReturnColumnValues() throws Exception {
new ValueWithLabel().value("value_01"),
new ValueWithLabel().value("value_02")
);
RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
RequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
.accept(MediaType.APPLICATION_JSON)
.header(XOkapiHeaders.TENANT, "tenant_01");
when(entityTypeService.getFieldValues(entityTypeId, columnName, null)).thenReturn(columnValues.content(expectedColumnValueLabel));
mockMvc.perform(requestBuilder)
when(entityTypeService.getFieldValues(entityTypeId, columnName, null))
.thenReturn(columnValues.content(expectedColumnValueLabel));
mockMvc
.perform(requestBuilder)
.andExpect(status().isOk())
.andExpect(jsonPath("$.content[0].value", is(expectedColumnValueLabel.get(0).getValue())))
.andExpect(jsonPath("$.content[1].value", is(expectedColumnValueLabel.get(1).getValue())));
Expand All @@ -175,12 +216,15 @@ void shouldReturnColumnValuesWithSearch() throws Exception {
new ValueWithLabel().value("value_01"),
new ValueWithLabel().value("value_02")
);
RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
RequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
.accept(MediaType.APPLICATION_JSON)
.header(XOkapiHeaders.TENANT, "tenant_01")
.queryParam("search", "value_01");
when(entityTypeService.getFieldValues(entityTypeId, columnName, "value_01")).thenReturn(columnValues.content(expectedColumnValueLabel));
mockMvc.perform(requestBuilder)
when(entityTypeService.getFieldValues(entityTypeId, columnName, "value_01"))
.thenReturn(columnValues.content(expectedColumnValueLabel));
mockMvc
.perform(requestBuilder)
.andExpect(status().isOk())
.andExpect(jsonPath("$.content[0].value", is(expectedColumnValueLabel.get(0).getValue())));
}
Expand All @@ -189,26 +233,26 @@ void shouldReturnColumnValuesWithSearch() throws Exception {
void shouldReturnErrorWhenColumnNameNotFound() throws Exception {
UUID entityTypeId = UUID.randomUUID();
String columnName = "column_name";
RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
RequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
.accept(MediaType.APPLICATION_JSON)
.header(XOkapiHeaders.TENANT, "tenant_01");
when(entityTypeService.getFieldValues(entityTypeId, columnName, null))
.thenThrow(new FieldNotFoundException("entity_type", columnName));
mockMvc.perform(requestBuilder)
.andExpect(status().isNotFound());
mockMvc.perform(requestBuilder).andExpect(status().isNotFound());
}

@Test
void shouldReturnErrorWhenEntityTypeIdNotFound() throws Exception {
UUID entityTypeId = UUID.randomUUID();
String columnName = "column_name";
RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
RequestBuilder requestBuilder = MockMvcRequestBuilders
.get("/entity-types/{id}/columns/{columnName}/values", entityTypeId, columnName)
.accept(MediaType.APPLICATION_JSON)
.header(XOkapiHeaders.TENANT, "tenant_01");
when(entityTypeService.getFieldValues(entityTypeId, columnName, null))
.thenThrow(new EntityTypeNotFoundException(entityTypeId));
mockMvc.perform(requestBuilder)
.andExpect(status().isNotFound());
mockMvc.perform(requestBuilder).andExpect(status().isNotFound());
}

private static EntityType getEntityType(EntityTypeColumn col) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ void shouldFetchAllPublicEntityTypes() {
new RawEntityTypeSummary(ENTITY_TYPE_02_ID, ENTITY_TYPE_02_LABEL, List.of())
);

List<RawEntityTypeSummary> actualSummary = repo.getEntityTypeSummary(Set.of());
List<RawEntityTypeSummary> actualSummary = repo.getEntityTypeSummaries(Set.of());
assertEquals(expectedSummary, actualSummary, "Expected Summary should equal Actual Summary");
}

Expand All @@ -59,7 +59,7 @@ void shouldFetchEntityTypesOfGivenIds() {
List<RawEntityTypeSummary> expectedSummary = List.of(
new RawEntityTypeSummary(ENTITY_TYPE_01_ID, ENTITY_TYPE_01_LABEL, List.of()));

List<RawEntityTypeSummary> actualSummary = repo.getEntityTypeSummary(ids);
List<RawEntityTypeSummary> actualSummary = repo.getEntityTypeSummaries(ids);
assertEquals(expectedSummary, actualSummary, "Expected Summary should equal Actual Summary");
}

Expand Down
Loading

0 comments on commit c6f8b77

Please sign in to comment.