Skip to content

Commit 00a6f54

Browse files
authored
Fix: Status is missing while creating FirestoreException from ApiException (#2107)
1 parent 5343e17 commit 00a6f54

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed

google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreException.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.api.core.BetaApi;
2020
import com.google.api.gax.rpc.ApiException;
21+
import com.google.api.gax.rpc.StatusCode;
2122
import com.google.cloud.grpc.BaseGrpcServiceException;
2223
import io.grpc.Status;
2324
import java.io.IOException;
@@ -43,6 +44,8 @@ private FirestoreException(String reason, ApiException exception) {
4344
exception,
4445
exception.getStatusCode().getCode().getHttpStatusCode(),
4546
exception.isRetryable());
47+
48+
this.status = FirestoreException.toStatus(exception.getStatusCode());
4649
}
4750

4851
private FirestoreException(IOException exception, boolean retryable) {
@@ -119,4 +122,22 @@ public static FirestoreException forApiException(ApiException exception, String
119122
public Status getStatus() {
120123
return status;
121124
}
125+
126+
/**
127+
* Converts a GAX {@code StatusCode} to a corresponding gRPC {@code Status} object. This method
128+
* assumes that the names of the enum values in {@code com.google.api.gax.rpc.StatusCode.Code}
129+
* directly correspond to the names of the enum values in {@code io.grpc.Status.Code}.
130+
*
131+
* <p>If the provided {@code StatusCode.Code} name does not have a direct matching {@code
132+
* io.grpc.Status.Code}, {@code Status.UNKNOWN} with a descriptive message is returned.
133+
*/
134+
private static Status toStatus(StatusCode statusCode) {
135+
try {
136+
Status.Code code = Status.Code.valueOf(statusCode.getCode().name());
137+
return code.toStatus();
138+
} catch (IllegalArgumentException e) {
139+
return Status.UNKNOWN.withDescription(
140+
"Unrecognized StatusCode.code: " + statusCode.getCode());
141+
}
142+
}
122143
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.firestore;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertFalse;
21+
import static org.junit.Assert.assertNull;
22+
import static org.junit.Assert.assertTrue;
23+
24+
import com.google.api.gax.grpc.GrpcStatusCode;
25+
import com.google.api.gax.rpc.ApiException;
26+
import com.google.api.gax.rpc.StatusCode;
27+
import io.grpc.Status;
28+
import java.io.IOException;
29+
import org.junit.Test;
30+
import org.junit.runner.RunWith;
31+
import org.mockito.junit.MockitoJUnitRunner;
32+
33+
/** Unit tests for the FirestoreException class. */
34+
@RunWith(MockitoJUnitRunner.class)
35+
public class FirestoreExceptionTest {
36+
@Test
37+
public void testConstructorWithReasonAndStatus() {
38+
String reason = "Aborted operation";
39+
Status status = Status.ABORTED;
40+
FirestoreException exception = new FirestoreException(reason, status);
41+
42+
assertEquals(reason, exception.getMessage());
43+
assertEquals(status, exception.getStatus());
44+
assertNull(exception.getCause());
45+
assertEquals(status.getCode().value(), exception.getCode());
46+
assertFalse(exception.isRetryable());
47+
}
48+
49+
@Test
50+
public void testForInvalidArgument() {
51+
String messageTemplate = "Invalid argument supplied: %s";
52+
String argument = "collectionId";
53+
String expectedMessage = String.format(messageTemplate, argument);
54+
55+
FirestoreException exception = FirestoreException.forInvalidArgument(messageTemplate, argument);
56+
57+
assertEquals(expectedMessage, exception.getMessage());
58+
assertEquals(Status.INVALID_ARGUMENT, exception.getStatus());
59+
assertNull(exception.getCause());
60+
assertEquals(Status.INVALID_ARGUMENT.getCode().value(), exception.getCode());
61+
assertFalse(exception.isRetryable());
62+
}
63+
64+
@Test
65+
public void testForServerRejection() {
66+
Status status = Status.PERMISSION_DENIED;
67+
String expectedMessage = "User is not authorized.";
68+
69+
FirestoreException exception = FirestoreException.forServerRejection(status, expectedMessage);
70+
71+
assertEquals(expectedMessage, exception.getMessage());
72+
assertEquals(status, exception.getStatus());
73+
assertNull(exception.getCause());
74+
assertEquals(status.getCode().value(), exception.getCode());
75+
assertFalse(exception.isRetryable());
76+
}
77+
78+
@Test
79+
public void testForServerRejectionWithCause() {
80+
Status status = Status.INTERNAL;
81+
String expectedMessage = "Database connection lost.";
82+
Throwable cause = new IllegalStateException("DB connection failed");
83+
84+
FirestoreException exception =
85+
FirestoreException.forServerRejection(status, cause, expectedMessage);
86+
87+
assertEquals(expectedMessage, exception.getMessage());
88+
assertEquals(status, exception.getStatus());
89+
assertEquals(cause, exception.getCause());
90+
assertEquals(status.getCode().value(), exception.getCode());
91+
assertFalse(exception.isRetryable());
92+
}
93+
94+
@Test
95+
public void testForIOException() {
96+
IOException ioException = new IOException("Simulated network read error");
97+
// The 'retryable' argument is passed, but BaseGrpcServiceException determines actual
98+
// retryability for IOExceptions.
99+
boolean retryable = true;
100+
101+
FirestoreException exception = FirestoreException.forIOException(ioException, retryable);
102+
103+
assertEquals(ioException.getMessage(), exception.getMessage());
104+
assertEquals(ioException, exception.getCause());
105+
// BaseGrpcServiceException classifies generic IOExceptions as non-retryable.
106+
assertFalse(exception.isRetryable());
107+
assertNull(exception.getStatus());
108+
// BaseGrpcServiceException extracts Code from HttpResponseException, or set it to
109+
// UNKNOWN_CODE, which is 0.
110+
assertEquals(0, exception.getCode());
111+
}
112+
113+
@Test
114+
public void testForApiException() {
115+
String apiExceptionMessage = "Generic API error details";
116+
boolean apiExceptionRetryable = true;
117+
final StatusCode.Code apiStatusCodeCode = StatusCode.Code.DEADLINE_EXCEEDED;
118+
119+
ApiException realApiException =
120+
new ApiException(
121+
apiExceptionMessage,
122+
new RuntimeException("Underlying cause for ApiException"),
123+
GrpcStatusCode.of(Status.Code.DEADLINE_EXCEEDED),
124+
apiExceptionRetryable);
125+
126+
FirestoreException exception = FirestoreException.forApiException(realApiException);
127+
128+
assertEquals(apiExceptionMessage, exception.getMessage());
129+
assertEquals(realApiException, exception.getCause());
130+
assertEquals(Status.DEADLINE_EXCEEDED.getCode(), exception.getStatus().getCode());
131+
assertTrue(exception.isRetryable());
132+
assertEquals(apiStatusCodeCode.getHttpStatusCode(), exception.getCode());
133+
}
134+
135+
@Test
136+
public void testForApiExceptionWithCustomMessage() {
137+
String customMessage = "A specific problem occurred during API call.";
138+
boolean apiExceptionRetryable = false;
139+
final StatusCode.Code apiStatusCodeCode = StatusCode.Code.NOT_FOUND;
140+
141+
ApiException realApiException =
142+
new ApiException(
143+
"This message from ApiException will be overridden by custom message",
144+
new IllegalStateException("Original API problem"),
145+
GrpcStatusCode.of(Status.Code.NOT_FOUND),
146+
apiExceptionRetryable);
147+
148+
FirestoreException exception =
149+
FirestoreException.forApiException(realApiException, customMessage);
150+
151+
assertEquals(customMessage, exception.getMessage());
152+
assertEquals(realApiException, exception.getCause());
153+
assertEquals(Status.NOT_FOUND.getCode(), exception.getStatus().getCode());
154+
assertFalse(exception.isRetryable());
155+
assertEquals(apiStatusCodeCode.getHttpStatusCode(), exception.getCode());
156+
}
157+
158+
@Test
159+
public void testGetStatusFromDirectStatusCreation() {
160+
Status expectedStatus = Status.RESOURCE_EXHAUSTED.withDescription("Quota exceeded.");
161+
FirestoreException exception = new FirestoreException("Quota limits hit.", expectedStatus);
162+
assertEquals(expectedStatus, exception.getStatus());
163+
}
164+
}

0 commit comments

Comments
 (0)