From 26ec58917dfe5fda4aa63c2f8e4748d813d93806 Mon Sep 17 00:00:00 2001 From: Viacheslav Rostovtsev Date: Tue, 29 Sep 2026 21:23:00 +0000 Subject: [PATCH] test: exercise resumable upload URL scheme through the real TLS stack TestUploadURLScheme sets r.TLS directly on an httptest request, so it cannot see cmux hiding the *tls.Conn from net/http. The new test starts the server via CreateAllEndpoints with auto-TLS, runs a resumable upload start over HTTP/1.1, asserts the returned upload URL is https://, and finalizes the upload through it. Removing the r.TLS restore in TLSHTTPMiddleware makes it fail with an http:// URL. --- cmd/gapic-showcase/tls_test.go | 67 ++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/cmd/gapic-showcase/tls_test.go b/cmd/gapic-showcase/tls_test.go index 1e540567b..4ecb27da3 100644 --- a/cmd/gapic-showcase/tls_test.go +++ b/cmd/gapic-showcase/tls_test.go @@ -19,8 +19,10 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "io" "net" "net/http" + "net/url" "os" "path/filepath" "strings" @@ -183,6 +185,71 @@ func TestConnectWithTLS_REST(t *testing.T) { } } +// TestResumableUploadURLSchemeOverTLS checks the upload URL a resumable upload +// start hands out, through the real TLS -> cmux -> REST stack. cmux hides the +// *tls.Conn from net/http, so r.TLS is nil unless TLSHTTPMiddleware restores +// it, and an http:// URL sends every later upload request in plain text to the +// TLS port. TestUploadURLScheme cannot catch that: it sets r.TLS directly on an +// httptest request. +func TestResumableUploadURLSchemeOverTLS(t *testing.T) { + t.Parallel() + addr, certPool, cleanup := setupTLSTestServer(t) + defer cleanup() + + // A custom TLSClientConfig disables the transport's automatic HTTP/2, so the + // requests negotiate HTTP/1.1 and cmux routes them to the REST server. + hc := &http.Client{ + Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: certPool}}, + Timeout: 5 * time.Second, + } + + start, err := http.NewRequest("POST", "https://"+addr+"/resumable/upload/v1beta1/files:upload", strings.NewReader(`{"name":"sample.txt"}`)) + if err != nil { + t.Fatalf("failed to build start request: %v", err) + } + start.Header.Set("Content-Type", "application/json") + start.Header.Set("X-Goog-Upload-Protocol", "resumable") + start.Header.Set("X-Goog-Upload-Command", "start") + startResp, err := hc.Do(start) + if err != nil { + t.Fatalf("start request failed: %v", err) + } + startBody, _ := io.ReadAll(startResp.Body) + startResp.Body.Close() + if startResp.StatusCode != http.StatusOK { + t.Fatalf("start: expected 200 OK, got %d: %s", startResp.StatusCode, startBody) + } + + uploadURL := startResp.Header.Get("X-Goog-Upload-URL") + parsed, err := url.Parse(uploadURL) + if err != nil { + t.Fatalf("start: unparseable upload URL %q: %v", uploadURL, err) + } + if parsed.Scheme != "https" || parsed.Host != addr { + t.Fatalf("start: expected an https://%s upload URL, got %q", addr, uploadURL) + } + + // The URL must also be usable over TLS: finish the upload through it. + final, err := http.NewRequest("POST", uploadURL, strings.NewReader("data")) + if err != nil { + t.Fatalf("failed to build finalize request: %v", err) + } + final.Header.Set("X-Goog-Upload-Command", "upload, finalize") + final.Header.Set("X-Goog-Upload-Offset", "0") + finalResp, err := hc.Do(final) + if err != nil { + t.Fatalf("upload, finalize request failed: %v", err) + } + finalBody, _ := io.ReadAll(finalResp.Body) + finalResp.Body.Close() + if finalResp.StatusCode != http.StatusOK { + t.Fatalf("upload, finalize: expected 200 OK, got %d: %s", finalResp.StatusCode, finalBody) + } + if got, want := finalResp.Header.Get("X-Goog-Upload-Status"), "final"; got != want { + t.Fatalf("upload, finalize: expected X-Goog-Upload-Status %q, got %q", want, got) + } +} + func assertHeader(t *testing.T, md metadata.MD, key, expected string) { t.Helper() val := md.Get(key)