Skip to content

Commit 9bbf17f

Browse files
committed
test(showcase): add integration tests for resumable upload
1 parent 4ca74ac commit 9bbf17f

1 file changed

Lines changed: 373 additions & 0 deletions

File tree

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
/*
2+
* Copyright 2026 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+
* https://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.showcase.v1beta1.it;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
22+
import com.google.api.client.http.javanet.NetHttpTransport;
23+
import com.google.api.gax.core.FixedCredentialsProvider;
24+
import com.google.api.gax.core.NoCredentialsProvider;
25+
import com.google.api.gax.rpc.FailedPreconditionException;
26+
import com.google.api.gax.rpc.ResumableUploadCallSettings;
27+
import com.google.api.gax.rpc.ResumableUploadFuture;
28+
import com.google.api.gax.rpc.TransportChannelProvider;
29+
import com.google.auth.Credentials;
30+
import com.google.auth.oauth2.AccessToken;
31+
import com.google.auth.oauth2.OAuth2Credentials;
32+
import com.google.showcase.v1beta1.ResumableUploadServiceClient;
33+
import com.google.showcase.v1beta1.ResumableUploadServiceSettings;
34+
import com.google.showcase.v1beta1.UploadMediaRequest;
35+
import com.google.showcase.v1beta1.UploadMediaResponse;
36+
import com.google.showcase.v1beta1.it.util.TestClientInitializer;
37+
import io.grpc.ManagedChannelBuilder;
38+
import java.io.ByteArrayInputStream;
39+
import java.io.IOException;
40+
import java.io.InputStream;
41+
import java.nio.charset.StandardCharsets;
42+
import java.nio.file.Files;
43+
import java.nio.file.Path;
44+
import java.util.Date;
45+
import java.util.concurrent.TimeUnit;
46+
import org.junit.jupiter.api.AfterAll;
47+
import org.junit.jupiter.api.BeforeAll;
48+
import org.junit.jupiter.api.Test;
49+
import org.junit.jupiter.api.io.TempDir;
50+
51+
/**
52+
* Integration tests for generated {@link ResumableUploadServiceClient} against the Showcase server.
53+
*/
54+
class ITResumableUpload {
55+
56+
private static final int SHOWCASE_CHUNK_SIZE = 256 * 1024; // 256KB
57+
private static final Credentials DUMMY_CREDENTIALS =
58+
OAuth2Credentials.create(new AccessToken("fake-token", new Date(Long.MAX_VALUE)));
59+
private static ResumableUploadServiceClient client;
60+
61+
@BeforeAll
62+
static void createClients() throws Exception {
63+
ResumableUploadServiceSettings.Builder settingsBuilder =
64+
ResumableUploadServiceSettings.newHttpJsonBuilder();
65+
settingsBuilder
66+
.setCredentialsProvider(NoCredentialsProvider.create())
67+
.setTransportChannelProvider(
68+
ResumableUploadServiceSettings.defaultHttpJsonTransportProviderBuilder()
69+
.setHttpTransport(new NetHttpTransport.Builder().doNotValidateCertificate().build())
70+
.setEndpoint(TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT)
71+
.build());
72+
settingsBuilder.uploadMediaSettings().setChunkSize(SHOWCASE_CHUNK_SIZE);
73+
client = ResumableUploadServiceClient.create(settingsBuilder.build());
74+
}
75+
76+
@AfterAll
77+
static void destroyClients() throws InterruptedException {
78+
if (client != null) {
79+
client.close();
80+
client.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
81+
}
82+
}
83+
84+
@Test
85+
void testGeneratedClient_uploadMedia_synchronousConvenienceMethod(@TempDir Path tempDir)
86+
throws Exception {
87+
Path file =
88+
createTempFile(
89+
tempDir,
90+
"it-client-sync.txt",
91+
"Hello from generated ResumableUploadServiceClient synchronous convenience method!"
92+
.getBytes(StandardCharsets.UTF_8));
93+
UploadMediaRequest request =
94+
UploadMediaRequest.newBuilder().setName("it-client-sync.txt").build();
95+
96+
try (InputStream stream = Files.newInputStream(file)) {
97+
UploadMediaResponse response = client.uploadMedia(request, stream);
98+
assertThat(response.getName()).isEqualTo("it-client-sync.txt");
99+
assertThat(response.getSize()).isEqualTo(Files.size(file));
100+
}
101+
}
102+
103+
@Test
104+
void testGeneratedClient_uploadMediaCallable_asynchronousFutureCall(@TempDir Path tempDir)
105+
throws Exception {
106+
Path file =
107+
createTempFile(
108+
tempDir,
109+
"it-client-callable.txt",
110+
"Hello from generated ResumableUploadServiceClient callable futureCall!"
111+
.getBytes(StandardCharsets.UTF_8));
112+
UploadMediaRequest request =
113+
UploadMediaRequest.newBuilder().setName("it-client-callable.txt").build();
114+
115+
try (InputStream stream = Files.newInputStream(file)) {
116+
ResumableUploadFuture<UploadMediaResponse> future =
117+
client
118+
.uploadMediaCallable()
119+
.futureCall(request, stream, (ResumableUploadCallSettings) null);
120+
121+
UploadMediaResponse response = future.get(10, TimeUnit.SECONDS);
122+
assertThat(future.isDone()).isTrue();
123+
assertThat(future.isCancelled()).isFalse();
124+
assertThat(future.getUploadSessionUrl()).isNotNull();
125+
assertThat(future.getUploadSessionUrl()).contains("/resumable/upload");
126+
assertThat(response.getName()).isEqualTo("it-client-callable.txt");
127+
assertThat(response.getSize()).isEqualTo(Files.size(file));
128+
}
129+
}
130+
131+
@Test
132+
void testGeneratedClient_multiChunkUpload(@TempDir Path tempDir) throws Exception {
133+
// 600KB payload = 2 full 256KB chunks + 1 partial 88KB chunk
134+
int totalBytes = 600 * 1024;
135+
Path file = createTempFile(tempDir, "it-client-multi-chunk.txt", totalBytes);
136+
UploadMediaRequest request =
137+
UploadMediaRequest.newBuilder().setName("it-client-multi-chunk.txt").build();
138+
139+
try (InputStream stream = Files.newInputStream(file)) {
140+
UploadMediaResponse response = client.uploadMedia(request, stream);
141+
assertThat(response.getName()).isEqualTo("it-client-multi-chunk.txt");
142+
assertThat(response.getSize()).isEqualTo(Files.size(file));
143+
}
144+
}
145+
146+
@Test
147+
void testGeneratedClient_zeroByteUpload() throws Exception {
148+
UploadMediaRequest request =
149+
UploadMediaRequest.newBuilder().setName("it-client-zero-byte.txt").build();
150+
151+
try (InputStream stream = new ByteArrayInputStream(new byte[0])) {
152+
UploadMediaResponse response = client.uploadMedia(request, stream);
153+
assertThat(response.getName()).isEqualTo("it-client-zero-byte.txt");
154+
assertThat(response.getSize()).isEqualTo(0);
155+
}
156+
}
157+
158+
@Test
159+
void testGeneratedClient_exactChunkBoundaryUpload(@TempDir Path tempDir) throws Exception {
160+
// Exactly 2 full 256KB chunks (512KB total) -> triggers 0-byte finalize request
161+
int totalBytes = 512 * 1024;
162+
Path file = createTempFile(tempDir, "it-client-exact-chunks.txt", totalBytes);
163+
UploadMediaRequest request =
164+
UploadMediaRequest.newBuilder().setName("it-client-exact-chunks.txt").build();
165+
166+
try (InputStream stream = Files.newInputStream(file)) {
167+
UploadMediaResponse response = client.uploadMedia(request, stream);
168+
assertThat(response.getName()).isEqualTo("it-client-exact-chunks.txt");
169+
assertThat(response.getSize()).isEqualTo(Files.size(file));
170+
}
171+
}
172+
173+
@Test
174+
void testGeneratedClient_grpcClientDelegation_uploadMedia(@TempDir Path tempDir)
175+
throws Exception {
176+
// Note on test harness architecture (4A & 4B):
177+
// In production Google Front End (GFE) deployments, both gRPC and HTTP/REST traffic are served
178+
// on standard port 443. However, the local Showcase server binds gRPC to port 7469 and HTTP
179+
// to port 7470.
180+
// The custom TransportChannelProvider below routes the primary transport stub to gRPC on
181+
// port 7469 while allowing the client endpoint to be configured to port 7470
182+
// (TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT) so the internal HTTP/REST resumable upload
183+
// stub can successfully communicate with Showcase's HTTP port without altering the gRPC
184+
// endpoint.
185+
//
186+
// Additionally, DUMMY_CREDENTIALS is provided to satisfy the CL-R2.2 pre-constructed channel
187+
// guard,
188+
// which requires credentials to be present when instantiating the HTTP/REST transport stub for
189+
// resumable uploads, even in this unauthenticated local test environment.
190+
TransportChannelProvider grpcTransportChannelProvider =
191+
new TransportChannelProvider() {
192+
private final TransportChannelProvider delegate =
193+
ResumableUploadServiceSettings.defaultGrpcTransportProviderBuilder()
194+
.setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
195+
.setEndpoint(TestClientInitializer.DEFAULT_GRPC_ENDPOINT)
196+
.build();
197+
198+
@Override
199+
public boolean shouldAutoClose() {
200+
return delegate.shouldAutoClose();
201+
}
202+
203+
@Override
204+
public boolean needsExecutor() {
205+
return delegate.needsExecutor();
206+
}
207+
208+
@Override
209+
public TransportChannelProvider withExecutor(java.util.concurrent.Executor executor) {
210+
return delegate.withExecutor(executor);
211+
}
212+
213+
@Override
214+
public TransportChannelProvider withExecutor(
215+
java.util.concurrent.ScheduledExecutorService executor) {
216+
return delegate.withExecutor(executor);
217+
}
218+
219+
@Override
220+
public boolean needsHeaders() {
221+
return delegate.needsHeaders();
222+
}
223+
224+
@Override
225+
public TransportChannelProvider withHeaders(java.util.Map<String, String> headers) {
226+
return delegate.withHeaders(headers);
227+
}
228+
229+
@Override
230+
public boolean needsEndpoint() {
231+
return false;
232+
}
233+
234+
@Override
235+
public TransportChannelProvider withEndpoint(String endpoint) {
236+
return this;
237+
}
238+
239+
@Override
240+
public boolean acceptsPoolSize() {
241+
return delegate.acceptsPoolSize();
242+
}
243+
244+
@Override
245+
public TransportChannelProvider withPoolSize(int size) {
246+
return delegate.withPoolSize(size);
247+
}
248+
249+
@Override
250+
public boolean needsCredentials() {
251+
return delegate.needsCredentials();
252+
}
253+
254+
@Override
255+
public TransportChannelProvider withCredentials(Credentials credentials) {
256+
return delegate.withCredentials(credentials);
257+
}
258+
259+
@Override
260+
public com.google.api.gax.rpc.TransportChannel getTransportChannel() throws IOException {
261+
return delegate.getTransportChannel();
262+
}
263+
264+
@Override
265+
public String getTransportName() {
266+
return delegate.getTransportName();
267+
}
268+
269+
@Override
270+
public String getEndpoint() {
271+
return null;
272+
}
273+
};
274+
275+
ResumableUploadServiceSettings.Builder grpcSettingsBuilder =
276+
ResumableUploadServiceSettings.newBuilder()
277+
.setCredentialsProvider(FixedCredentialsProvider.create(DUMMY_CREDENTIALS))
278+
.setTransportChannelProvider(grpcTransportChannelProvider)
279+
.setEndpoint(TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT);
280+
grpcSettingsBuilder.uploadMediaSettings().setChunkSize(SHOWCASE_CHUNK_SIZE);
281+
282+
Path file =
283+
createTempFile(
284+
tempDir,
285+
"it-grpc-delegation.txt",
286+
"Hello from generated ResumableUploadServiceClient gRPC delegation!"
287+
.getBytes(StandardCharsets.UTF_8));
288+
UploadMediaRequest request =
289+
UploadMediaRequest.newBuilder().setName("it-grpc-delegation.txt").build();
290+
291+
ResumableUploadServiceClient grpcClient =
292+
ResumableUploadServiceClient.create(grpcSettingsBuilder.build());
293+
try {
294+
// 1. Synchronous convenience method delegation
295+
try (InputStream stream = Files.newInputStream(file)) {
296+
UploadMediaResponse response = grpcClient.uploadMedia(request, stream);
297+
assertThat(response.getName()).isEqualTo("it-grpc-delegation.txt");
298+
assertThat(response.getSize()).isEqualTo(Files.size(file));
299+
}
300+
301+
// 2. Asynchronous callable futureCall delegation
302+
try (InputStream stream = Files.newInputStream(file)) {
303+
ResumableUploadFuture<UploadMediaResponse> future =
304+
grpcClient
305+
.uploadMediaCallable()
306+
.futureCall(request, stream, (ResumableUploadCallSettings) null);
307+
UploadMediaResponse response = future.get(10, TimeUnit.SECONDS);
308+
assertThat(future.isDone()).isTrue();
309+
assertThat(future.isCancelled()).isFalse();
310+
assertThat(future.getUploadSessionUrl()).isNotNull();
311+
assertThat(future.getUploadSessionUrl()).contains("/resumable/upload");
312+
assertThat(response.getName()).isEqualTo("it-grpc-delegation.txt");
313+
assertThat(response.getSize()).isEqualTo(Files.size(file));
314+
}
315+
} finally {
316+
grpcClient.close();
317+
assertThat(grpcClient.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
318+
}
319+
}
320+
321+
@Test
322+
void testChannelGuards_grpcChannelOnly_throwsFailedPreconditionException(@TempDir Path tempDir)
323+
throws Exception {
324+
ResumableUploadServiceSettings grpcSettings =
325+
ResumableUploadServiceSettings.newBuilder()
326+
.setCredentialsProvider(NoCredentialsProvider.create())
327+
.setTransportChannelProvider(
328+
ResumableUploadServiceSettings.defaultGrpcTransportProviderBuilder()
329+
.setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
330+
.build())
331+
.setEndpoint(TestClientInitializer.DEFAULT_GRPC_ENDPOINT)
332+
.build();
333+
334+
Path file =
335+
createTempFile(
336+
tempDir, "guard-test.txt", "guard test data".getBytes(StandardCharsets.UTF_8));
337+
338+
try (ResumableUploadServiceClient grpcClient =
339+
ResumableUploadServiceClient.create(grpcSettings)) {
340+
UploadMediaRequest request =
341+
UploadMediaRequest.newBuilder().setName("guard-test.txt").build();
342+
343+
try (InputStream stream1 = Files.newInputStream(file)) {
344+
// 1. Verify synchronous convenience call fails fast
345+
FailedPreconditionException syncException =
346+
assertThrows(
347+
FailedPreconditionException.class, () -> grpcClient.uploadMedia(request, stream1));
348+
assertThat(syncException.getMessage())
349+
.contains("Resumable uploads execute over HTTP/REST and require credentials");
350+
}
351+
352+
// 2. Verify callable getter fails fast
353+
FailedPreconditionException callableException =
354+
assertThrows(FailedPreconditionException.class, () -> grpcClient.uploadMediaCallable());
355+
assertThat(callableException.getMessage())
356+
.contains("Resumable uploads execute over HTTP/REST and require credentials");
357+
}
358+
}
359+
360+
private static Path createTempFile(Path dir, String fileName, byte[] data) throws IOException {
361+
Path path = dir.resolve(fileName);
362+
Files.write(path, data);
363+
return path;
364+
}
365+
366+
private static Path createTempFile(Path dir, String fileName, int size) throws IOException {
367+
byte[] data = new byte[size];
368+
for (int i = 0; i < size; i++) {
369+
data[i] = (byte) (i % 256);
370+
}
371+
return createTempFile(dir, fileName, data);
372+
}
373+
}

0 commit comments

Comments
 (0)