diff --git a/java-bigtable/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java b/java-bigtable/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java index ddc9916a3a5f..7a89c5d94140 100644 --- a/java-bigtable/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java +++ b/java-bigtable/test-proxy/src/main/java/com/google/cloud/bigtable/testproxy/CbtTestProxy.java @@ -48,6 +48,7 @@ import com.google.cloud.bigtable.data.v2.models.sql.SqlType; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStubSettings; import com.google.cloud.bigtable.testproxy.CloudBigtableV2TestProxyGrpc.CloudBigtableV2TestProxyImplBase; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.protobuf.ByteString; import com.google.protobuf.util.Durations; @@ -395,6 +396,9 @@ public void readRow(ReadRowRequest request, StreamObserver responseOb resultBuilder.setStatus(com.google.rpc.Status.getDefaultInstance()).build()); } else { logger.info(String.format("readRow() did not find row: %s", request.getRowKey())); + responseObserver.onNext( + // Note that the default instance == OK + RowResult.newBuilder().setStatus(com.google.rpc.Status.getDefaultInstance()).build()); } } catch (ApiException e) { responseObserver.onNext(RowResult.newBuilder().setStatus(convertStatus(e)).build()); @@ -640,6 +644,9 @@ public void readModifyWriteRow( logger.info( String.format( "readModifyWriteRow() did not find row: %s", request.getRequest().getRowKey())); + responseObserver.onNext( + // Note that the default instance == OK + RowResult.newBuilder().setStatus(com.google.rpc.Status.getDefaultInstance()).build()); } } catch (ApiException e) { responseObserver.onNext(RowResult.newBuilder().setStatus(convertStatus(e)).build()); @@ -815,7 +822,7 @@ private CredentialsProvider getCredentialsProvider(String accessToken) { OAuth2Credentials.create(new AccessToken(accessToken, null))); } - private final ConcurrentHashMap idClientMap; + @VisibleForTesting final ConcurrentHashMap idClientMap; private static final Pattern tablePattern = Pattern.compile("projects/([^/]+)/instances/([^/]+)/tables/([^/]+)"); diff --git a/java-bigtable/test-proxy/src/test/java/com/google/cloud/bigtable/testproxy/CbtTestProxyTest.java b/java-bigtable/test-proxy/src/test/java/com/google/cloud/bigtable/testproxy/CbtTestProxyTest.java new file mode 100644 index 000000000000..25cc29e1d252 --- /dev/null +++ b/java-bigtable/test-proxy/src/test/java/com/google/cloud/bigtable/testproxy/CbtTestProxyTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.bigtable.testproxy; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.bigtable.v2.ReadModifyWriteRule; +import com.google.cloud.bigtable.data.v2.BigtableDataClient; +import com.google.cloud.bigtable.data.v2.BigtableDataSettings; +import com.google.cloud.bigtable.data.v2.models.Filters.Filter; +import com.google.cloud.bigtable.data.v2.models.ReadModifyWriteRow; +import com.google.cloud.bigtable.data.v2.models.Row; +import com.google.cloud.bigtable.data.v2.models.RowCell; +import com.google.cloud.bigtable.testproxy.CbtTestProxy.CbtClient; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.ByteString; +import com.google.rpc.Code; +import io.grpc.stub.StreamObserver; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +@RunWith(JUnit4.class) +public class CbtTestProxyTest { + + private static final String CLIENT_ID = "test-client"; + private static final String TABLE_NAME = + "projects/my-project/instances/my-instance/tables/my-table"; + private static final String TABLE_ID = "my-table"; + + private BigtableDataClient mockDataClient; + private CbtTestProxy proxy; + + @Before + public void setUp() { + mockDataClient = mock(BigtableDataClient.class); + BigtableDataSettings settings = + BigtableDataSettings.newBuilderForEmulator(8080) + .setProjectId("my-project") + .setInstanceId("my-instance") + .build(); + proxy = CbtTestProxy.create(); + proxy.idClientMap.put(CLIENT_ID, CbtClient.create(settings, mockDataClient)); + } + + @Test + public void testReadRowNotFoundEmitsEmptyRowResultWithOkStatus() { + when(mockDataClient.readRow(eq(TABLE_ID), eq("missing-row"), any(Filter.class))) + .thenReturn(null); + + @SuppressWarnings("unchecked") + StreamObserver responseObserver = mock(StreamObserver.class); + ReadRowRequest request = + ReadRowRequest.newBuilder() + .setClientId(CLIENT_ID) + .setTableName(TABLE_NAME) + .setRowKey("missing-row") + .build(); + + proxy.readRow(request, responseObserver); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(RowResult.class); + verify(responseObserver).onNext(resultCaptor.capture()); + verify(responseObserver).onCompleted(); + + RowResult result = resultCaptor.getValue(); + assertThat(result.getStatus().getCode()).isEqualTo(Code.OK.getNumber()); + assertThat(result.hasRow()).isFalse(); + } + + @Test + public void testReadRowFoundEmitsPopulatedRowResultWithOkStatus() { + Row row = + Row.create( + ByteString.copyFromUtf8("row-1"), + ImmutableList.of( + RowCell.create( + "cf", + ByteString.copyFromUtf8("col"), + 1000L, + ImmutableList.of(), + ByteString.copyFromUtf8("val")))); + when(mockDataClient.readRow(eq(TABLE_ID), eq("row-1"), any(Filter.class))).thenReturn(row); + + @SuppressWarnings("unchecked") + StreamObserver responseObserver = mock(StreamObserver.class); + ReadRowRequest request = + ReadRowRequest.newBuilder() + .setClientId(CLIENT_ID) + .setTableName(TABLE_NAME) + .setRowKey("row-1") + .build(); + + proxy.readRow(request, responseObserver); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(RowResult.class); + verify(responseObserver).onNext(resultCaptor.capture()); + verify(responseObserver).onCompleted(); + + RowResult result = resultCaptor.getValue(); + assertThat(result.getStatus().getCode()).isEqualTo(Code.OK.getNumber()); + assertThat(result.hasRow()).isTrue(); + assertThat(result.getRow().getKey()).isEqualTo(ByteString.copyFromUtf8("row-1")); + } + + @Test + public void testReadModifyWriteRowNullEmitsEmptyRowResultWithOkStatus() { + when(mockDataClient.readModifyWriteRow(any(ReadModifyWriteRow.class))).thenReturn(null); + + @SuppressWarnings("unchecked") + StreamObserver responseObserver = mock(StreamObserver.class); + ReadModifyWriteRowRequest request = + ReadModifyWriteRowRequest.newBuilder() + .setClientId(CLIENT_ID) + .setRequest( + com.google.bigtable.v2.ReadModifyWriteRowRequest.newBuilder() + .setTableName(TABLE_NAME) + .setRowKey(ByteString.copyFromUtf8("rmw-row")) + .addRules( + ReadModifyWriteRule.newBuilder() + .setFamilyName("cf") + .setColumnQualifier(ByteString.copyFromUtf8("col")) + .setAppendValue(ByteString.copyFromUtf8("v")))) + .build(); + + proxy.readModifyWriteRow(request, responseObserver); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(RowResult.class); + verify(responseObserver).onNext(resultCaptor.capture()); + verify(responseObserver).onCompleted(); + + RowResult result = resultCaptor.getValue(); + assertThat(result.getStatus().getCode()).isEqualTo(Code.OK.getNumber()); + assertThat(result.hasRow()).isFalse(); + } +}