diff --git a/core/src/main/java/feast/core/grpc/UIServiceImpl.java b/core/src/main/java/feast/core/grpc/UIServiceImpl.java new file mode 100644 index 0000000000..9690f83fda --- /dev/null +++ b/core/src/main/java/feast/core/grpc/UIServiceImpl.java @@ -0,0 +1,242 @@ +package feast.core.grpc; + +import static io.grpc.Status.Code.INTERNAL; +import static io.grpc.Status.Code.INVALID_ARGUMENT; + +import com.google.protobuf.Empty; +import feast.core.UIServiceGrpc.UIServiceImplBase; +import feast.core.UIServiceProto.UIServiceTypes.EntityDetail; +import feast.core.UIServiceProto.UIServiceTypes.FeatureDetail; +import feast.core.UIServiceProto.UIServiceTypes.FeatureGroupDetail; +import feast.core.UIServiceProto.UIServiceTypes.GetEntityRequest; +import feast.core.UIServiceProto.UIServiceTypes.GetEntityResponse; +import feast.core.UIServiceProto.UIServiceTypes.GetFeatureGroupRequest; +import feast.core.UIServiceProto.UIServiceTypes.GetFeatureGroupResponse; +import feast.core.UIServiceProto.UIServiceTypes.GetFeatureRequest; +import feast.core.UIServiceProto.UIServiceTypes.GetFeatureResponse; +import feast.core.UIServiceProto.UIServiceTypes.GetStorageRequest; +import feast.core.UIServiceProto.UIServiceTypes.GetStorageResponse; +import feast.core.UIServiceProto.UIServiceTypes.ListEntitiesResponse; +import feast.core.UIServiceProto.UIServiceTypes.ListFeatureGroupsResponse; +import feast.core.UIServiceProto.UIServiceTypes.ListFeaturesResponse; +import feast.core.UIServiceProto.UIServiceTypes.ListStorageResponse; +import feast.core.UIServiceProto.UIServiceTypes.StorageDetail; +import feast.core.model.EntityInfo; +import feast.core.model.FeatureGroupInfo; +import feast.core.model.FeatureInfo; +import feast.core.model.StorageInfo; +import feast.core.service.SpecService; +import io.grpc.Status; +import io.grpc.Status.Code; +import io.grpc.stub.StreamObserver; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.lognet.springboot.grpc.GRpcService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * GRPC Service exposing detailed information of feast's resources. + */ +@Slf4j +@GRpcService +public class UIServiceImpl extends UIServiceImplBase { + + private final SpecService specService; + + @Autowired + public UIServiceImpl(SpecService specService) { + this.specService = specService; + } + + @Override + public void getEntity(GetEntityRequest request, + StreamObserver responseObserver) { + String entityName = request.getId(); + + try { + List entityInfos = specService.getEntities(Collections.singletonList(entityName)); + EntityDetail entityDetail = entityInfos.get(0) + .getEntityDetail(); + + GetEntityResponse response = GetEntityResponse.newBuilder() + .setEntity(entityDetail) + .build(); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } catch (IllegalArgumentException e) { + String errMsg = "Invalid entity name: " + entityName; + log.error(errMsg, e); + onError(responseObserver, INVALID_ARGUMENT, errMsg, e); + } catch (Exception e) { + String errMsg = "Error while retrieving entity with name: " + entityName; + log.error(errMsg, e); + onError(responseObserver, INTERNAL, errMsg, e); + } + } + + @Override + public void listEntities(Empty request, StreamObserver responseObserver) { + try { + List entityInfos = specService.listEntities(); + List entityDetails = entityInfos.stream() + .map(EntityInfo::getEntityDetail) + .collect(Collectors.toList()); + ListEntitiesResponse response = ListEntitiesResponse.newBuilder() + .addAllEntities(entityDetails) + .build(); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } catch (Exception e) { + String errMsg = "Error while getting all entities"; + log.error(errMsg, e); + onError(responseObserver, INTERNAL, errMsg, e); + } + } + + @Override + public void getFeature(GetFeatureRequest request, + StreamObserver responseObserver) { + String featureId = request.getId(); + try { + List featureInfos = specService + .getFeatures(Collections.singletonList(featureId)); + FeatureDetail featureDetail = featureInfos.get(0).getFeatureDetail(); + + GetFeatureResponse resp = GetFeatureResponse.newBuilder() + .setFeature(featureDetail) + .build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); + } catch (IllegalArgumentException e) { + String errMsg = "Invalid feature ID: " + featureId; + log.error(errMsg); + onError(responseObserver, INVALID_ARGUMENT, errMsg, e); + } catch (Exception e) { + String errMsg = "Error while retrieving feature with ID: " + featureId; + log.error(errMsg, e); + onError(responseObserver, INTERNAL, errMsg, e); + } + } + + @Override + public void listFeatures(Empty request, StreamObserver responseObserver) { + try { + List featureDetails = specService.listFeatures() + .stream() + .map(FeatureInfo::getFeatureDetail) + .collect(Collectors.toList()); + + ListFeaturesResponse resp = ListFeaturesResponse.newBuilder() + .addAllFeatures(featureDetails) + .build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); + } catch (Exception e) { + String errMsg = "Error while getting all features"; + log.error(errMsg, e); + onError(responseObserver, INTERNAL, errMsg, e); + } + } + + @Override + public void getFeatureGroup(GetFeatureGroupRequest request, + StreamObserver responseObserver) { + String featureGroupId = request.getId(); + try { + List featureGroupInfos = specService + .getFeatureGroups(Collections.singletonList(featureGroupId)); + + GetFeatureGroupResponse resp = GetFeatureGroupResponse.newBuilder() + .setFeatureGroup(featureGroupInfos.get(0).getFeatureGroupDetail()) + .build(); + + responseObserver.onNext(resp); + responseObserver.onCompleted(); + } catch (IllegalArgumentException e) { + String errMsg = "Invalid feature group ID: " + featureGroupId; + log.error(errMsg); + onError(responseObserver, INVALID_ARGUMENT, errMsg, e); + } catch (Exception e) { + String errMsg = "Error while getting feature group with ID: " + featureGroupId; + log.error(errMsg, e); + onError(responseObserver, INTERNAL, errMsg, e); + } + } + + @Override + public void listFeatureGroups(Empty request, + StreamObserver responseObserver) { + try { + List featureGroupInfos = specService.listFeatureGroups(); + List featureGroupDetails = featureGroupInfos.stream() + .map(FeatureGroupInfo::getFeatureGroupDetail) + .collect(Collectors.toList()); + + ListFeatureGroupsResponse resp = ListFeatureGroupsResponse.newBuilder() + .addAllFeatureGroups(featureGroupDetails) + .build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); + } catch (Exception e) { + String errMsg = "Error while getting all feature groups"; + log.error(errMsg, e); + onError(responseObserver, INTERNAL, errMsg, e); + } + } + + @Override + public void getStorage(GetStorageRequest request, + StreamObserver responseObserver) { + String storageId = request.getId(); + try { + List storageInfos = specService.getStorage(Collections.singletonList(storageId)); + GetStorageResponse resp = GetStorageResponse.newBuilder() + .setStorage(storageInfos.get(0).getStorageDetail()) + .build(); + + responseObserver.onNext(resp); + responseObserver.onCompleted(); + } catch (IllegalArgumentException e) { + String errMsg = "Invalid storage ID: " + storageId; + log.error(errMsg, e); + onError(responseObserver, INVALID_ARGUMENT, errMsg, e); + } catch (Exception e) { + String errMsg = "Error while retrieving storage detail with ID: " + storageId; + log.error(errMsg, e); + onError(responseObserver, INTERNAL, errMsg, e); + } + } + + @Override + public void listStorage(Empty request, StreamObserver responseObserver) { + try { + List storageInfos = specService.listStorage(); + List storageDetails = storageInfos.stream() + .map(StorageInfo::getStorageDetail) + .collect(Collectors.toList()); + + ListStorageResponse resp = ListStorageResponse.newBuilder() + .addAllStorage(storageDetails) + .build(); + + responseObserver.onNext(resp); + responseObserver.onCompleted(); + } catch (Exception e) { + String errMsg = "Error while getting all storage details"; + log.error(errMsg, e); + onError(responseObserver, INTERNAL, errMsg, e); + } + } + + private void onError(StreamObserver responseObserver, Code errCode, String message, + Throwable cause) { + responseObserver.onError(Status.fromCode(errCode) + .withDescription(message) + .withCause(cause) + .asException()); + } +} diff --git a/core/src/main/java/feast/core/service/SpecService.java b/core/src/main/java/feast/core/service/SpecService.java index f4ab5c6428..ba6d0b391e 100644 --- a/core/src/main/java/feast/core/service/SpecService.java +++ b/core/src/main/java/feast/core/service/SpecService.java @@ -79,8 +79,7 @@ public SpecService( * @throws RetrievalException if any of the requested ids is not found * @throws IllegalArgumentException if the list of ids is empty */ - public List getEntities(List ids) - throws RetrievalException, IllegalArgumentException { + public List getEntities(List ids) { if (ids.size() == 0) { throw new IllegalArgumentException("ids cannot be empty"); } @@ -98,7 +97,7 @@ public List getEntities(List ids) * @return list of EntityInfos * @throws RetrievalException if retrieval fails */ - public List listEntities() throws RetrievalException { + public List listEntities() { return this.entityInfoRepository.findAll(); } @@ -110,8 +109,7 @@ public List listEntities() throws RetrievalException { * @throws RetrievalException if any of the requested ids is not found * @throws IllegalArgumentException if the list of ids is empty */ - public List getFeatures(List ids) - throws RetrievalException, IllegalArgumentException { + public List getFeatures(List ids) { if (ids.size() == 0) { throw new IllegalArgumentException("ids cannot be empty"); } @@ -129,7 +127,7 @@ public List getFeatures(List ids) * @return list of FeatureInfos * @throws RetrievalException if retrieval fails */ - public List listFeatures() throws RetrievalException { + public List listFeatures() { return this.featureInfoRepository.findAll(); } @@ -141,8 +139,7 @@ public List listFeatures() throws RetrievalException { * @throws RetrievalException if any of the requested ids is not found * @throws IllegalArgumentException if the list of ids is empty */ - public List getFeatureGroups(List ids) - throws RetrievalException, IllegalArgumentException { + public List getFeatureGroups(List ids) { if (ids.size() == 0) { throw new IllegalArgumentException("ids cannot be empty"); } @@ -161,7 +158,7 @@ public List getFeatureGroups(List ids) * @return list of FeatureGroupInfos * @throws RetrievalException if retrieval fails */ - public List listFeatureGroups() throws RetrievalException { + public List listFeatureGroups() { return this.featureGroupInfoRepository.findAll(); } @@ -173,8 +170,7 @@ public List listFeatureGroups() throws RetrievalException { * @throws RetrievalException if any of the requested ids is not found * @throws IllegalArgumentException if the list of ids is empty */ - public List getStorage(List ids) - throws RetrievalException, IllegalArgumentException { + public List getStorage(List ids) { if (ids.size() == 0) { throw new IllegalArgumentException("ids cannot be empty"); } @@ -192,7 +188,7 @@ public List getStorage(List ids) * @return list of StorageInfos * @throws RetrievalException if retrieval fails */ - public List listStorage() throws RetrievalException { + public List listStorage() { return this.storageInfoRepository.findAll(); } @@ -208,7 +204,7 @@ public List listStorage() throws RetrievalException { * @return registered FeatureInfo * @throws RegistrationException if registration fails */ - public FeatureInfo applyFeature(FeatureSpec spec) throws RegistrationException { + public FeatureInfo applyFeature(FeatureSpec spec) { try { FeatureInfo featureInfo = featureInfoRepository.findById(spec.getId()).orElse(null); Action action; @@ -258,7 +254,7 @@ public FeatureInfo applyFeature(FeatureSpec spec) throws RegistrationException { * @return registered FeatureGroupInfo * @throws RegistrationException if registration fails */ - public FeatureGroupInfo applyFeatureGroup(FeatureGroupSpec spec) throws RegistrationException { + public FeatureGroupInfo applyFeatureGroup(FeatureGroupSpec spec) { try { FeatureGroupInfo featureGroupInfo = featureGroupInfoRepository.findById(spec.getId()).orElse(null); @@ -311,7 +307,7 @@ public FeatureGroupInfo applyFeatureGroup(FeatureGroupSpec spec) throws Registra * @return registered EntityInfo * @throws RegistrationException if registration fails */ - public EntityInfo applyEntity(EntitySpec spec) throws RegistrationException { + public EntityInfo applyEntity(EntitySpec spec) { try { EntityInfo entityInfo = entityInfoRepository.findById(spec.getName()).orElse(null); Action action; @@ -342,7 +338,7 @@ public EntityInfo applyEntity(EntitySpec spec) throws RegistrationException { * @return registered StorageInfo * @throws RegistrationException if registration fails */ - public StorageInfo registerStorage(StorageSpec spec) throws RegistrationException { + public StorageInfo registerStorage(StorageSpec spec) { try { StorageInfo storageInfo = storageInfoRepository.findById(spec.getId()).orElse(null); if (storageInfo != null) { diff --git a/core/src/test/java/feast/core/grpc/UIServiceImplTest.java b/core/src/test/java/feast/core/grpc/UIServiceImplTest.java new file mode 100644 index 0000000000..cca090bec7 --- /dev/null +++ b/core/src/test/java/feast/core/grpc/UIServiceImplTest.java @@ -0,0 +1,443 @@ +package feast.core.grpc; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.protobuf.Empty; +import feast.core.UIServiceGrpc; +import feast.core.UIServiceProto.UIServiceTypes.EntityDetail; +import feast.core.UIServiceProto.UIServiceTypes.FeatureDetail; +import feast.core.UIServiceProto.UIServiceTypes.FeatureGroupDetail; +import feast.core.UIServiceProto.UIServiceTypes.GetEntityRequest; +import feast.core.UIServiceProto.UIServiceTypes.GetEntityResponse; +import feast.core.UIServiceProto.UIServiceTypes.GetFeatureGroupRequest; +import feast.core.UIServiceProto.UIServiceTypes.GetFeatureGroupResponse; +import feast.core.UIServiceProto.UIServiceTypes.GetFeatureRequest; +import feast.core.UIServiceProto.UIServiceTypes.GetFeatureResponse; +import feast.core.UIServiceProto.UIServiceTypes.GetStorageRequest; +import feast.core.UIServiceProto.UIServiceTypes.GetStorageResponse; +import feast.core.UIServiceProto.UIServiceTypes.ListEntitiesResponse; +import feast.core.UIServiceProto.UIServiceTypes.ListFeatureGroupsResponse; +import feast.core.UIServiceProto.UIServiceTypes.ListFeaturesResponse; +import feast.core.UIServiceProto.UIServiceTypes.ListStorageResponse; +import feast.core.UIServiceProto.UIServiceTypes.StorageDetail; +import feast.core.model.EntityInfo; +import feast.core.model.FeatureGroupInfo; +import feast.core.model.FeatureInfo; +import feast.core.model.StorageInfo; +import feast.core.service.SpecService; +import feast.specs.EntitySpecProto.EntitySpec; +import feast.specs.FeatureGroupSpecProto.FeatureGroupSpec; +import feast.specs.FeatureSpecProto.FeatureSpec; +import feast.specs.StorageSpecProto.StorageSpec; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessChannelBuilder; +import io.grpc.inprocess.InProcessServerBuilder; +import io.grpc.testing.GrpcCleanupRule; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mock; + +public class UIServiceImplTest { + + @Mock public SpecService specService; + + @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + + @Rule public final ExpectedException expectedException = ExpectedException.none(); + + private UIServiceGrpc.UIServiceBlockingStub client; + + @Before + public void setUp() throws Exception { + specService = mock(SpecService.class); + + UIServiceImpl service = new UIServiceImpl(specService); + + String serverName = InProcessServerBuilder.generateName(); + grpcCleanup.register( + InProcessServerBuilder.forName(serverName) + .directExecutor() + .addService(service) + .build() + .start()); + + client = + UIServiceGrpc.newBlockingStub( + grpcCleanup.register( + InProcessChannelBuilder.forName(serverName).directExecutor().build())); + } + + @Test + public void getEntity_shouldReturnCorrectEntityDetail() { + String entityName = "entity"; + GetEntityRequest req = GetEntityRequest.newBuilder().setId(entityName).build(); + + EntitySpec entitySpec = + EntitySpec.newBuilder().setName(entityName).setDescription("test entity").build(); + + EntityInfo entityInfo = new EntityInfo(entitySpec); + entityInfo.setLastUpdated(new Date()); + + when(specService.getEntities(Collections.singletonList(entityName))) + .thenReturn(Collections.singletonList(entityInfo)); + + GetEntityResponse resp = client.getEntity(req); + EntityDetail actual = resp.getEntity(); + + assertThat(actual, equalTo(entityInfo.getEntityDetail())); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void getEntity_shouldReturnClearErrorMessageForInvalidEntityName() { + String entityName = ""; + GetEntityRequest req = GetEntityRequest.newBuilder().setId(entityName).build(); + + EntitySpec entitySpec = + EntitySpec.newBuilder().setName(entityName).setDescription("test entity").build(); + + EntityInfo entityInfo = new EntityInfo(entitySpec); + entityInfo.setLastUpdated(new Date()); + + when(specService.getEntities(Collections.singletonList(entityName))) + .thenThrow(new IllegalArgumentException("invalid entity name")); + + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Invalid entity name: " + entityName); + client.getEntity(req); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void getEntity_shouldReturnClearErrorMessageForAnyFailure() { + String entityName = ""; + GetEntityRequest req = GetEntityRequest.newBuilder().setId(entityName).build(); + + EntitySpec entitySpec = + EntitySpec.newBuilder().setName(entityName).setDescription("test entity").build(); + + EntityInfo entityInfo = new EntityInfo(entitySpec); + entityInfo.setLastUpdated(new Date()); + + when(specService.getEntities(Collections.singletonList(entityName))) + .thenThrow(new RuntimeException()); + + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Error while retrieving entity with name: " + entityName); + client.getEntity(req); + } + + @Test + public void listEntities_shouldReturnAllEntities() { + EntitySpec entitySpec1 = + EntitySpec.newBuilder().setName("entity1").setDescription("test entity").build(); + EntityInfo entityInfo1 = new EntityInfo(entitySpec1); + entityInfo1.setLastUpdated(new Date()); + + EntitySpec entitySpec2 = + EntitySpec.newBuilder().setName("entity2").setDescription("test entity").build(); + EntityInfo entityInfo2 = new EntityInfo(entitySpec2); + entityInfo2.setLastUpdated(new Date()); + + List entityInfos = Arrays.asList(entityInfo1, entityInfo2); + + when(specService.listEntities()).thenReturn(entityInfos); + + ListEntitiesResponse resp = client.listEntities(Empty.getDefaultInstance()); + List actual = resp.getEntitiesList(); + + assertThat( + actual, + containsInAnyOrder(entityInfos.stream().map(EntityInfo::getEntityDetail).toArray())); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void listEntities_shouldReturnClearErrorMessageForAnyFailure() { + when(specService.listEntities()).thenThrow(new RuntimeException()); + + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Error while getting all entities"); + + client.listEntities(Empty.getDefaultInstance()); + } + + @Test + public void getFeature_shouldReturnCorrectFeatureDetail() { + String featureId = "entity.granularity.feature"; + FeatureInfo featureInfo = createFeatureInfo(featureId); + + when(specService.getFeatures(Collections.singletonList(featureId))) + .thenReturn(Collections.singletonList(featureInfo)); + + GetFeatureRequest req = GetFeatureRequest.newBuilder().setId(featureId).build(); + GetFeatureResponse resp = client.getFeature(req); + + FeatureDetail expected = featureInfo.getFeatureDetail(); + FeatureDetail actual = resp.getFeature(); + + assertThat(actual, equalTo(expected)); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void getFeature_shouldReturnInvalidArgumentForInvalidFeatureId() { + String featureId = "invalid.feature.id"; + + when(specService.getFeatures(Collections.singletonList(featureId))) + .thenThrow(new IllegalArgumentException()); + + GetFeatureRequest req = GetFeatureRequest.newBuilder().setId(featureId).build(); + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Invalid feature ID: " + featureId); + + client.getFeature(req); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void getFeature_shouldReturnErrorForAnyFailure() { + String featureId = "invalid.feature.id"; + + when(specService.getFeatures(Collections.singletonList(featureId))) + .thenThrow(new RuntimeException()); + + GetFeatureRequest req = GetFeatureRequest.newBuilder().setId(featureId).build(); + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Error while retrieving feature with ID: " + featureId); + + client.getFeature(req); + } + + @Test + public void listFeature_shouldReturnAllFeatures() { + String featureId1 = "entity.granularity.feature1"; + String featureId2 = "entity.granularity.feature2"; + + FeatureInfo featureInfo1 = createFeatureInfo(featureId1); + FeatureInfo featureInfo2 = createFeatureInfo(featureId2); + + List featureInfos = Arrays.asList(featureInfo1, featureInfo2); + + when(specService.listFeatures()).thenReturn(featureInfos); + + ListFeaturesResponse resp = client.listFeatures(Empty.getDefaultInstance()); + List actual = resp.getFeaturesList(); + + assertThat( + actual, + containsInAnyOrder(featureInfos.stream().map(FeatureInfo::getFeatureDetail).toArray())); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void listFeature_shouldReturnClearErrorMessageForAnyFailure() { + when(specService.listFeatures()).thenThrow(new RuntimeException()); + + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Error while getting all features"); + + client.listFeatures(Empty.getDefaultInstance()); + } + + @Test + public void getFeatureGroup_shouldReturnCorrectFeatureGroup() { + String featureGroupId = "featureGroup"; + + FeatureGroupInfo featureGroupInfo = createFeatureGroupInfo(featureGroupId); + + when(specService.getFeatureGroups(Collections.singletonList(featureGroupId))) + .thenReturn(Collections.singletonList(featureGroupInfo)); + + GetFeatureGroupRequest req = GetFeatureGroupRequest.newBuilder().setId(featureGroupId).build(); + + GetFeatureGroupResponse resp = client.getFeatureGroup(req); + FeatureGroupDetail actual = resp.getFeatureGroup(); + + assertThat(actual, equalTo(featureGroupInfo.getFeatureGroupDetail())); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void getFeatureGroup_shouldReturnClearErrorMessageForInvalidId() { + String invalidFeatureGroupId = "invalidId"; + + when(specService.getFeatureGroups(Collections.singletonList(invalidFeatureGroupId))) + .thenThrow(new IllegalArgumentException()); + + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Invalid feature group ID: " + invalidFeatureGroupId); + + GetFeatureGroupRequest req = + GetFeatureGroupRequest.newBuilder().setId(invalidFeatureGroupId).build(); + + client.getFeatureGroup(req); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void getFeatureGroup_shouldReturnClearErrorMessageForAnyFailure() { + String featureGroupId = "invalidId"; + + when(specService.getFeatureGroups(Collections.singletonList(featureGroupId))) + .thenThrow(new RuntimeException()); + + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Error while getting feature group with ID: " + featureGroupId); + + GetFeatureGroupRequest req = GetFeatureGroupRequest.newBuilder().setId(featureGroupId).build(); + + client.getFeatureGroup(req); + } + + @Test + public void listFeatureGroup_shouldReturnAllFeatureGroups() { + FeatureGroupInfo featureGroupInfo1 = createFeatureGroupInfo("featureGroup1"); + FeatureGroupInfo featureGroupInfo2 = createFeatureGroupInfo("featureGroup2"); + + List featureGroupInfos = Arrays.asList(featureGroupInfo1, featureGroupInfo2); + when(specService.listFeatureGroups()).thenReturn(featureGroupInfos); + + ListFeatureGroupsResponse resp = client.listFeatureGroups(Empty.getDefaultInstance()); + List actual = resp.getFeatureGroupsList(); + + assertThat( + actual, + containsInAnyOrder( + featureGroupInfos.stream().map(FeatureGroupInfo::getFeatureGroupDetail).toArray())); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void listFeatureGroup_shouldReturnClearErrorMessageForAnyFailure() { + when(specService.listFeatureGroups()).thenThrow(new RuntimeException()); + + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Error while getting all feature groups"); + client.listFeatureGroups(Empty.getDefaultInstance()); + } + + @Test + public void getStorage_shouldReturnCorrectStorageDetail() { + String storageId = "mystorage"; + StorageSpec storageSpec = StorageSpec.newBuilder().setId(storageId).build(); + StorageInfo storageInfo = new StorageInfo(storageSpec); + storageInfo.setLastUpdated(new Date()); + + when(specService.getStorage(Collections.singletonList(storageId))) + .thenReturn(Collections.singletonList(storageInfo)); + + GetStorageRequest req = GetStorageRequest.newBuilder().setId(storageId).build(); + GetStorageResponse resp = client.getStorage(req); + StorageDetail actual = resp.getStorage(); + + assertThat(actual, equalTo(storageInfo.getStorageDetail())); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void getStorage_shouldReturnErrorForInvalidStorageId() { + String storageId = "invalid"; + + when(specService.getStorage(Collections.singletonList(storageId))) + .thenThrow(new IllegalArgumentException()); + + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Invalid storage ID: " + storageId); + + GetStorageRequest req = GetStorageRequest.newBuilder().setId(storageId).build(); + client.getStorage(req); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void getStorage_shouldReturnErrorForAnyFailure() { + String storageId = "myStorage"; + + when(specService.getStorage(Collections.singletonList(storageId))) + .thenThrow(new RuntimeException()); + + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Error while retrieving storage detail with ID: " + storageId); + + GetStorageRequest req = GetStorageRequest.newBuilder().setId(storageId).build(); + client.getStorage(req); + } + + @Test + public void listStorage_shouldReturnAllStorageDetail() { + String storageId1 = "storage1"; + StorageSpec storageSpec1 = StorageSpec.newBuilder().setId(storageId1).build(); + StorageInfo storageInfo1 = new StorageInfo(storageSpec1); + storageInfo1.setLastUpdated(new Date()); + + String storageId2 = "storage2"; + StorageSpec storageSpec2 = StorageSpec.newBuilder().setId(storageId2).build(); + StorageInfo storageInfo2 = new StorageInfo(storageSpec2); + storageInfo2.setLastUpdated(new Date()); + + List storageInfos = Arrays.asList(storageInfo1, storageInfo2); + + when(specService.listStorage()).thenReturn(storageInfos); + + ListStorageResponse resp = client.listStorage(Empty.getDefaultInstance()); + List actual = resp.getStorageList(); + + assertThat( + actual, + containsInAnyOrder(storageInfos.stream().map(StorageInfo::getStorageDetail).toArray())); + } + + @Test + @SuppressWarnings("ResultOfMethodCallIgnored") + public void listStorage_shouldReturnErrorForAnyFailure() { + when(specService.listStorage()).thenThrow(new RuntimeException()); + + expectedException.expect(StatusRuntimeException.class); + expectedException.expectMessage("Error while getting all storage details"); + + client.listStorage(Empty.getDefaultInstance()); + } + + private FeatureInfo createFeatureInfo(String featureId) { + StorageSpec warehouseSpec = StorageSpec.newBuilder().setId("warehouse").build(); + StorageSpec servingSpec = StorageSpec.newBuilder().setId("serving").build(); + EntitySpec entitySpec = EntitySpec.newBuilder().setName("entity").build(); + + StorageInfo warehouseStoreInfo = new StorageInfo(warehouseSpec); + StorageInfo servingStoreInfo = new StorageInfo(servingSpec); + + EntityInfo entityInfo = new EntityInfo(entitySpec); + FeatureSpec featureSpec = FeatureSpec.newBuilder().setId(featureId).build(); + FeatureInfo featureInfo = + new FeatureInfo(featureSpec, entityInfo, servingStoreInfo, warehouseStoreInfo, null); + featureInfo.setCreated(new Date()); + featureInfo.setLastUpdated(new Date()); + return featureInfo; + } + + private FeatureGroupInfo createFeatureGroupInfo(String featureGroupId) { + StorageSpec warehouseSpec = StorageSpec.newBuilder().setId("warehouse").build(); + StorageSpec servingSpec = StorageSpec.newBuilder().setId("serving").build(); + + StorageInfo warehouseStoreInfo = new StorageInfo(warehouseSpec); + StorageInfo servingStoreInfo = new StorageInfo(servingSpec); + + FeatureGroupSpec featureGroupSpec = FeatureGroupSpec.newBuilder().setId(featureGroupId).build(); + FeatureGroupInfo featureGroupInfo = + new FeatureGroupInfo(featureGroupSpec, servingStoreInfo, warehouseStoreInfo); + featureGroupInfo.setCreated(new Date()); + featureGroupInfo.setLastUpdated(new Date()); + return featureGroupInfo; + } +}