diff --git a/metadata-ingestion/tests/integration/lookml/vv-lineage-and-liquid-templates/employee_salary_rating.view.lkml b/metadata-ingestion/tests/integration/lookml/vv-lineage-and-liquid-templates/employee_salary_rating.view.lkml new file mode 100644 index 00000000000000..3a00099e7998e9 --- /dev/null +++ b/metadata-ingestion/tests/integration/lookml/vv-lineage-and-liquid-templates/employee_salary_rating.view.lkml @@ -0,0 +1,50 @@ +view: employee_salary_rating { + derived_table: { + sql: SELECT + employee_id, + employee_name, + {% if dw_eff_dt_date._is_selected or finance_dw_eff_dt_date._is_selected %} + prod_core.data.r_metric_summary_v2 + {% elsif dw_eff_dt_week._is_selected or finance_dw_eff_dt_week._is_selected %} + prod_core.data.r_metric_summary_v3 + {% else %} + 'default_table' as source + {% endif %}, + employee_income + FROM source_table + WHERE + {% condition source_region %} source_table.region {% endcondition %} AND + {% if rating_window._is_filtered %} + {% condition rating_window %} DATE (rating_created) {% endcondition %} + {% endif %} + ;; + } + + filter: rating_window { + description: "Date window in which to look for rating" + default_value: "90 days ago for 90 days" + datatype: date + type: date + } + + dimension: id { + type: number + sql: ${TABLE}.employee_id;; + } + + dimension: name { + type: string + sql: ${TABLE}.employee_name;; + } + + dimension: source { + type: string + sql: ${TABLE}.source ;; + } + + dimension: income { + type: number + sql: ${TABLE}.employee_income ;; + } + +} \ No newline at end of file diff --git a/metadata-ingestion/tests/integration/lookml/vv-lineage-and-liquid-templates/parent_view.view.lkml b/metadata-ingestion/tests/integration/lookml/vv-lineage-and-liquid-templates/parent_view.view.lkml new file mode 100644 index 00000000000000..c2f18924351c29 --- /dev/null +++ b/metadata-ingestion/tests/integration/lookml/vv-lineage-and-liquid-templates/parent_view.view.lkml @@ -0,0 +1,18 @@ +view: parent_view { + sql_table_name: `dataset.table` ;; + + dimension: id { + primary_key: yes + type: string + sql: ${TABLE}.id ;; + } + + dimension: parent_dimension_1 { + type: string + sql: ${TABLE}.parent_dimension_1 ;; + } + + measure: parent_count { + type: count + } +} \ No newline at end of file diff --git a/metadata-ingestion/tests/integration/lookml/vv-lineage-and-liquid-templates/rent_as_employee_income_source.view.lkml b/metadata-ingestion/tests/integration/lookml/vv-lineage-and-liquid-templates/rent_as_employee_income_source.view.lkml new file mode 100644 index 00000000000000..40b6e3642f3b34 --- /dev/null +++ b/metadata-ingestion/tests/integration/lookml/vv-lineage-and-liquid-templates/rent_as_employee_income_source.view.lkml @@ -0,0 +1,27 @@ +view: rent_as_employee_income_source { + sql_table_name: ( + SELECT id, + name, + source + FROM ${employee_income_source.SQL_TABLE_NAME} + WHERE source = "RENT" + ORDER BY source desc + LIMIT 10 + );; + + + dimension: id { + type: number + sql: ${TABLE}.id ;; + } + + dimension: name { + type: string + sql: ${TABLE}.name ;; + } + + dimension: source { + type: string + sql: ${TABLE}.source ;; + } +} \ No newline at end of file diff --git a/metadata-io/metadata-io-api/src/test/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImplTest.java b/metadata-io/metadata-io-api/src/test/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImplTest.java new file mode 100644 index 00000000000000..31dd868b4cb4a3 --- /dev/null +++ b/metadata-io/metadata-io-api/src/test/java/com/linkedin/metadata/entity/ebean/batch/AspectsBatchImplTest.java @@ -0,0 +1,356 @@ +package com.linkedin.metadata.entity.ebean.batch; + +import static com.linkedin.metadata.Constants.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import com.google.common.collect.ImmutableList; +import com.linkedin.common.FabricType; +import com.linkedin.common.Status; +import com.linkedin.common.urn.DataPlatformUrn; +import com.linkedin.common.urn.DatasetUrn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.data.ByteString; +import com.linkedin.data.schema.annotation.PathSpecBasedSchemaAnnotationVisitor; +import com.linkedin.dataset.DatasetProperties; +import com.linkedin.events.metadata.ChangeType; +import com.linkedin.metadata.aspect.AspectRetriever; +import com.linkedin.metadata.aspect.GraphRetriever; +import com.linkedin.metadata.aspect.batch.MCPItem; +import com.linkedin.metadata.aspect.patch.GenericJsonPatch; +import com.linkedin.metadata.aspect.patch.PatchOperationType; +import com.linkedin.metadata.aspect.patch.builder.DatasetPropertiesPatchBuilder; +import com.linkedin.metadata.aspect.plugins.config.AspectPluginConfig; +import com.linkedin.metadata.aspect.plugins.hooks.MutationHook; +import com.linkedin.metadata.entity.SearchRetriever; +import com.linkedin.metadata.models.registry.ConfigEntityRegistry; +import com.linkedin.metadata.models.registry.EntityRegistry; +import com.linkedin.metadata.models.registry.EntityRegistryException; +import com.linkedin.metadata.models.registry.MergedEntityRegistry; +import com.linkedin.metadata.models.registry.SnapshotEntityRegistry; +import com.linkedin.metadata.snapshot.Snapshot; +import com.linkedin.metadata.utils.AuditStampUtils; +import com.linkedin.metadata.utils.GenericRecordUtils; +import com.linkedin.mxe.GenericAspect; +import com.linkedin.mxe.MetadataChangeProposal; +import com.linkedin.mxe.SystemMetadata; +import com.linkedin.structured.StructuredProperties; +import com.linkedin.structured.StructuredPropertyValueAssignmentArray; +import com.linkedin.util.Pair; +import io.datahubproject.metadata.context.RetrieverContext; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +public class AspectsBatchImplTest { + private EntityRegistry testRegistry; + private AspectRetriever mockAspectRetriever; + private RetrieverContext retrieverContext; + + @BeforeTest + public void beforeTest() throws EntityRegistryException { + PathSpecBasedSchemaAnnotationVisitor.class + .getClassLoader() + .setClassAssertionStatus(PathSpecBasedSchemaAnnotationVisitor.class.getName(), false); + + EntityRegistry snapshotEntityRegistry = new SnapshotEntityRegistry(); + EntityRegistry configEntityRegistry = + new ConfigEntityRegistry( + Snapshot.class.getClassLoader().getResourceAsStream("AspectsBatchImplTest.yaml")); + this.testRegistry = + new MergedEntityRegistry(snapshotEntityRegistry).apply(configEntityRegistry); + } + + @BeforeMethod + public void setup() { + this.mockAspectRetriever = mock(AspectRetriever.class); + when(this.mockAspectRetriever.getEntityRegistry()).thenReturn(testRegistry); + this.retrieverContext = + RetrieverContext.builder() + .searchRetriever(mock(SearchRetriever.class)) + .aspectRetriever(mockAspectRetriever) + .graphRetriever(mock(GraphRetriever.class)) + .build(); + } + + @Test + public void toUpsertBatchItemsChangeItemTest() { + List testItems = + List.of( + ChangeItemImpl.builder() + .urn( + UrnUtils.getUrn( + "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD)")) + .changeType(ChangeType.UPSERT) + .aspectName(STATUS_ASPECT_NAME) + .entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME)) + .aspectSpec( + testRegistry + .getEntitySpec(DATASET_ENTITY_NAME) + .getAspectSpec(STATUS_ASPECT_NAME)) + .auditStamp(AuditStampUtils.createDefaultAuditStamp()) + .recordTemplate(new Status().setRemoved(true)) + .build(mockAspectRetriever), + ChangeItemImpl.builder() + .urn( + UrnUtils.getUrn( + "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_deleted,PROD)")) + .changeType(ChangeType.UPSERT) + .aspectName(STATUS_ASPECT_NAME) + .entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME)) + .aspectSpec( + testRegistry + .getEntitySpec(DATASET_ENTITY_NAME) + .getAspectSpec(STATUS_ASPECT_NAME)) + .auditStamp(AuditStampUtils.createDefaultAuditStamp()) + .recordTemplate(new Status().setRemoved(false)) + .build(mockAspectRetriever)); + + AspectsBatchImpl testBatch = + AspectsBatchImpl.builder().items(testItems).retrieverContext(retrieverContext).build(); + + assertEquals( + testBatch.toUpsertBatchItems(Map.of()), + Pair.of(Map.of(), testItems), + "Expected noop, pass through with no additional MCPs or changes"); + } + + @Test + public void toUpsertBatchItemsPatchItemTest() { + GenericJsonPatch.PatchOp testPatchOp = new GenericJsonPatch.PatchOp(); + testPatchOp.setOp(PatchOperationType.REMOVE.getValue()); + testPatchOp.setPath( + String.format( + "/properties/%s", "urn:li:structuredProperty:io.acryl.privacy.retentionTime")); + + List testItems = + List.of( + PatchItemImpl.builder() + .urn( + UrnUtils.getUrn( + "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD)")) + .entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME)) + .aspectName(STRUCTURED_PROPERTIES_ASPECT_NAME) + .aspectSpec( + testRegistry + .getEntitySpec(DATASET_ENTITY_NAME) + .getAspectSpec(STRUCTURED_PROPERTIES_ASPECT_NAME)) + .patch( + GenericJsonPatch.builder() + .arrayPrimaryKeys(Map.of("properties", List.of("propertyUrn"))) + .patch(List.of(testPatchOp)) + .build() + .getJsonPatch()) + .auditStamp(AuditStampUtils.createDefaultAuditStamp()) + .build(retrieverContext.getAspectRetriever().getEntityRegistry()), + PatchItemImpl.builder() + .urn( + UrnUtils.getUrn( + "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_deleted,PROD)")) + .entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME)) + .aspectName(STRUCTURED_PROPERTIES_ASPECT_NAME) + .aspectSpec( + testRegistry + .getEntitySpec(DATASET_ENTITY_NAME) + .getAspectSpec(STRUCTURED_PROPERTIES_ASPECT_NAME)) + .patch( + GenericJsonPatch.builder() + .arrayPrimaryKeys(Map.of("properties", List.of("propertyUrn"))) + .patch(List.of(testPatchOp)) + .build() + .getJsonPatch()) + .auditStamp(AuditStampUtils.createDefaultAuditStamp()) + .build(retrieverContext.getAspectRetriever().getEntityRegistry())); + + AspectsBatchImpl testBatch = + AspectsBatchImpl.builder().items(testItems).retrieverContext(retrieverContext).build(); + + assertEquals( + testBatch.toUpsertBatchItems(Map.of()), + Pair.of( + Map.of(), + List.of( + ChangeItemImpl.builder() + .urn( + UrnUtils.getUrn( + "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD)")) + .changeType(ChangeType.UPSERT) + .aspectName(STRUCTURED_PROPERTIES_ASPECT_NAME) + .entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME)) + .aspectSpec( + testRegistry + .getEntitySpec(DATASET_ENTITY_NAME) + .getAspectSpec(STRUCTURED_PROPERTIES_ASPECT_NAME)) + .auditStamp(testItems.get(0).getAuditStamp()) + .recordTemplate( + new StructuredProperties() + .setProperties(new StructuredPropertyValueAssignmentArray())) + .systemMetadata(testItems.get(0).getSystemMetadata()) + .build(mockAspectRetriever), + ChangeItemImpl.builder() + .urn( + UrnUtils.getUrn( + "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_deleted,PROD)")) + .changeType(ChangeType.UPSERT) + .aspectName(STRUCTURED_PROPERTIES_ASPECT_NAME) + .entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME)) + .aspectSpec( + testRegistry + .getEntitySpec(DATASET_ENTITY_NAME) + .getAspectSpec(STRUCTURED_PROPERTIES_ASPECT_NAME)) + .auditStamp(testItems.get(1).getAuditStamp()) + .recordTemplate( + new StructuredProperties() + .setProperties(new StructuredPropertyValueAssignmentArray())) + .systemMetadata(testItems.get(1).getSystemMetadata()) + .build(mockAspectRetriever))), + "Expected patch items converted to upsert change items"); + } + + @Test + public void toUpsertBatchItemsProposedItemTest() { + List testItems = + List.of( + ProposedItem.builder() + .entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME)) + .metadataChangeProposal( + new MetadataChangeProposal() + .setEntityUrn( + UrnUtils.getUrn( + "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD)")) + .setAspectName("my-custom-aspect") + .setEntityType(DATASET_ENTITY_NAME) + .setChangeType(ChangeType.UPSERT) + .setAspect( + new GenericAspect() + .setContentType("application/json") + .setValue( + ByteString.copyString( + "{\"foo\":\"bar\"}", StandardCharsets.UTF_8))) + .setSystemMetadata(new SystemMetadata())) + .auditStamp(AuditStampUtils.createDefaultAuditStamp()) + .build(), + ProposedItem.builder() + .entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME)) + .metadataChangeProposal( + new MetadataChangeProposal() + .setEntityUrn( + UrnUtils.getUrn( + "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_deleted,PROD)")) + .setAspectName("my-custom-aspect") + .setEntityType(DATASET_ENTITY_NAME) + .setChangeType(ChangeType.UPSERT) + .setAspect( + new GenericAspect() + .setContentType("application/json") + .setValue( + ByteString.copyString( + "{\"foo\":\"bar\"}", StandardCharsets.UTF_8))) + .setSystemMetadata(new SystemMetadata())) + .auditStamp(AuditStampUtils.createDefaultAuditStamp()) + .build()); + + AspectsBatchImpl testBatch = + AspectsBatchImpl.builder().items(testItems).retrieverContext(retrieverContext).build(); + + assertEquals( + testBatch.toUpsertBatchItems(Map.of()), + Pair.of( + Map.of(), + List.of( + ChangeItemImpl.builder() + .urn( + UrnUtils.getUrn( + "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_created,PROD)")) + .changeType(ChangeType.UPSERT) + .aspectName(STATUS_ASPECT_NAME) + .entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME)) + .aspectSpec( + testRegistry + .getEntitySpec(DATASET_ENTITY_NAME) + .getAspectSpec(STATUS_ASPECT_NAME)) + .auditStamp(AuditStampUtils.createDefaultAuditStamp()) + .systemMetadata(testItems.get(0).getSystemMetadata()) + .recordTemplate(new Status().setRemoved(false)) + .build(mockAspectRetriever), + ChangeItemImpl.builder() + .urn( + UrnUtils.getUrn( + "urn:li:dataset:(urn:li:dataPlatform:hive,fct_users_deleted,PROD)")) + .changeType(ChangeType.UPSERT) + .aspectName(STATUS_ASPECT_NAME) + .entitySpec(testRegistry.getEntitySpec(DATASET_ENTITY_NAME)) + .aspectSpec( + testRegistry + .getEntitySpec(DATASET_ENTITY_NAME) + .getAspectSpec(STATUS_ASPECT_NAME)) + .auditStamp(AuditStampUtils.createDefaultAuditStamp()) + .systemMetadata(testItems.get(1).getSystemMetadata()) + .recordTemplate(new Status().setRemoved(false)) + .build(mockAspectRetriever))), + "Mutation to status aspect"); + } + + @Test + public void singleInvalidDoesntBreakBatch() { + MetadataChangeProposal proposal1 = + new DatasetPropertiesPatchBuilder() + .urn(new DatasetUrn(new DataPlatformUrn("platform"), "name", FabricType.PROD)) + .setDescription("something") + .setName("name") + .addCustomProperty("prop1", "propVal1") + .addCustomProperty("prop2", "propVal2") + .build(); + MetadataChangeProposal proposal2 = + new MetadataChangeProposal() + .setEntityType(DATASET_ENTITY_NAME) + .setAspectName(DATASET_PROPERTIES_ASPECT_NAME) + .setAspect(GenericRecordUtils.serializeAspect(new DatasetProperties())) + .setChangeType(ChangeType.UPSERT); + + AspectsBatchImpl testBatch = + AspectsBatchImpl.builder() + .mcps( + ImmutableList.of(proposal1, proposal2), + AuditStampUtils.createDefaultAuditStamp(), + retrieverContext) + .retrieverContext(retrieverContext) + .build(); + + assertEquals( + testBatch.toUpsertBatchItems(Map.of()).getSecond().size(), + 1, + "Expected 1 valid mcp to be passed through."); + } + + /** Converts unsupported to status aspect */ + @Getter + @Setter + @Accessors(chain = true) + public static class TestMutator extends MutationHook { + private AspectPluginConfig config; + + @Override + protected Stream proposalMutation( + @Nonnull Collection mcpItems, + @Nonnull com.linkedin.metadata.aspect.RetrieverContext retrieverContext) { + return mcpItems.stream() + .peek( + item -> + item.getMetadataChangeProposal() + .setAspectName(STATUS_ASPECT_NAME) + .setAspect( + GenericRecordUtils.serializeAspect(new Status().setRemoved(false)))); + } + } +} diff --git a/metadata-service/openapi-servlet/src/test/java/io/datahubproject/openapi/v3/controller/EntityControllerTest.java b/metadata-service/openapi-servlet/src/test/java/io/datahubproject/openapi/v3/controller/EntityControllerTest.java new file mode 100644 index 00000000000000..60425fc7e756ed --- /dev/null +++ b/metadata-service/openapi-servlet/src/test/java/io/datahubproject/openapi/v3/controller/EntityControllerTest.java @@ -0,0 +1,215 @@ +package io.datahubproject.openapi.v3.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.testng.Assert.assertNotNull; + +import com.datahub.authentication.Actor; +import com.datahub.authentication.ActorType; +import com.datahub.authentication.Authentication; +import com.datahub.authentication.AuthenticationContext; +import com.datahub.authorization.AuthorizationResult; +import com.datahub.authorization.AuthorizerChain; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.linkedin.common.Status; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.entity.Aspect; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.metadata.entity.EntityService; +import com.linkedin.metadata.entity.EntityServiceImpl; +import com.linkedin.metadata.graph.elastic.ElasticSearchGraphService; +import com.linkedin.metadata.models.registry.EntityRegistry; +import com.linkedin.metadata.query.filter.Filter; +import com.linkedin.metadata.query.filter.SortOrder; +import com.linkedin.metadata.search.ScrollResult; +import com.linkedin.metadata.search.SearchEntity; +import com.linkedin.metadata.search.SearchEntityArray; +import com.linkedin.metadata.search.SearchService; +import com.linkedin.metadata.utils.SearchUtil; +import io.datahubproject.metadata.context.OperationContext; +import io.datahubproject.openapi.config.SpringWebConfig; +import io.datahubproject.test.metadata.context.TestOperationContexts; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; +import org.springframework.http.MediaType; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.testng.annotations.Test; + +@SpringBootTest(classes = {SpringWebConfig.class}) +@ComponentScan(basePackages = {"io.datahubproject.openapi.v3.controller"}) +@Import({SpringWebConfig.class, EntityControllerTest.EntityControllerTestConfig.class}) +@AutoConfigureWebMvc +@AutoConfigureMockMvc +public class EntityControllerTest extends AbstractTestNGSpringContextTests { + @Autowired private EntityController entityController; + @Autowired private MockMvc mockMvc; + @Autowired private SearchService mockSearchService; + @Autowired private EntityService mockEntityService; + + @Test + public void initTest() { + assertNotNull(entityController); + } + + @Test + public void testSearchOrderPreserved() throws Exception { + List TEST_URNS = + List.of( + UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:testPlatform,1,PROD)"), + UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:testPlatform,2,PROD)"), + UrnUtils.getUrn("urn:li:dataset:(urn:li:dataPlatform:testPlatform,3,PROD)")); + + // Mock scroll ascending/descending results + ScrollResult expectedResultAscending = + new ScrollResult() + .setEntities( + new SearchEntityArray( + List.of( + new SearchEntity().setEntity(TEST_URNS.get(0)), + new SearchEntity().setEntity(TEST_URNS.get(1)), + new SearchEntity().setEntity(TEST_URNS.get(2))))); + when(mockSearchService.scrollAcrossEntities( + any(OperationContext.class), + eq(List.of("dataset")), + anyString(), + nullable(Filter.class), + eq(Collections.singletonList(SearchUtil.sortBy("urn", SortOrder.valueOf("ASCENDING")))), + nullable(String.class), + nullable(String.class), + anyInt())) + .thenReturn(expectedResultAscending); + ScrollResult expectedResultDescending = + new ScrollResult() + .setEntities( + new SearchEntityArray( + List.of( + new SearchEntity().setEntity(TEST_URNS.get(2)), + new SearchEntity().setEntity(TEST_URNS.get(1)), + new SearchEntity().setEntity(TEST_URNS.get(0))))); + when(mockSearchService.scrollAcrossEntities( + any(OperationContext.class), + eq(List.of("dataset")), + anyString(), + nullable(Filter.class), + eq( + Collections.singletonList( + SearchUtil.sortBy("urn", SortOrder.valueOf("DESCENDING")))), + nullable(String.class), + nullable(String.class), + anyInt())) + .thenReturn(expectedResultDescending); + // Mock entity aspect + when(mockEntityService.getEnvelopedVersionedAspects( + any(OperationContext.class), anyMap(), eq(false))) + .thenReturn( + Map.of( + TEST_URNS.get(0), + List.of( + new EnvelopedAspect() + .setName("status") + .setValue(new Aspect(new Status().data()))), + TEST_URNS.get(1), + List.of( + new EnvelopedAspect() + .setName("status") + .setValue(new Aspect(new Status().data()))), + TEST_URNS.get(2), + List.of( + new EnvelopedAspect() + .setName("status") + .setValue(new Aspect(new Status().data()))))); + + // test ASCENDING + mockMvc + .perform( + MockMvcRequestBuilders.get("/v3/entity/dataset") + .param("sortOrder", "ASCENDING") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().is2xxSuccessful()) + .andExpect( + MockMvcResultMatchers.jsonPath("$.entities[0].urn").value(TEST_URNS.get(0).toString())) + .andExpect( + MockMvcResultMatchers.jsonPath("$.entities[1].urn").value(TEST_URNS.get(1).toString())) + .andExpect( + MockMvcResultMatchers.jsonPath("$.entities[2].urn").value(TEST_URNS.get(2).toString())); + + // test DESCENDING + mockMvc + .perform( + MockMvcRequestBuilders.get("/v3/entity/dataset") + .accept(MediaType.APPLICATION_JSON) + .param("sortOrder", "DESCENDING")) + .andExpect(status().is2xxSuccessful()) + .andExpect( + MockMvcResultMatchers.jsonPath("$.entities[0].urn").value(TEST_URNS.get(2).toString())) + .andExpect( + MockMvcResultMatchers.jsonPath("$.entities[1].urn").value(TEST_URNS.get(1).toString())) + .andExpect( + MockMvcResultMatchers.jsonPath("$.entities[2].urn").value(TEST_URNS.get(0).toString())); + } + + @TestConfiguration + public static class EntityControllerTestConfig { + @MockBean public EntityServiceImpl entityService; + @MockBean public SearchService searchService; + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + @Bean(name = "systemOperationContext") + public OperationContext systemOperationContext() { + return TestOperationContexts.systemContextNoSearchAuthorization(); + } + + @Bean("entityRegistry") + @Primary + public EntityRegistry entityRegistry( + @Qualifier("systemOperationContext") final OperationContext testOperationContext) { + return testOperationContext.getEntityRegistry(); + } + + @Bean("graphService") + @Primary + public ElasticSearchGraphService graphService() { + return mock(ElasticSearchGraphService.class); + } + + @Bean + public AuthorizerChain authorizerChain() { + AuthorizerChain authorizerChain = mock(AuthorizerChain.class); + + Authentication authentication = mock(Authentication.class); + when(authentication.getActor()).thenReturn(new Actor(ActorType.USER, "datahub")); + when(authorizerChain.authorize(any())) + .thenReturn(new AuthorizationResult(null, AuthorizationResult.Type.ALLOW, "")); + AuthenticationContext.setAuthentication(authentication); + + return authorizerChain; + } + } +}