Skip to content

Commit a25df69

Browse files
committed
test(showcase): add integration tests for resumable upload
1 parent b594af9 commit a25df69

1 file changed

Lines changed: 368 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)