Skip to content

Commit 8a1a0a0

Browse files
committed
Address review feedback: move e2e tests to external test package
- Move e2e tests to token_file_call_creds_ext_test.go with package jwt_test - Split into 4 focused tests instead of one complex table-driven test - Copy helper functions (createTestJWT, writeTempTokenFile) to new file - Rename expectedAuth to wantAuth per Go style guide
1 parent 9b366bf commit 8a1a0a0

File tree

2 files changed

+271
-132
lines changed

2 files changed

+271
-132
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/*
2+
*
3+
* Copyright 2025 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package jwt_test
20+
21+
import (
22+
"context"
23+
"crypto/tls"
24+
"encoding/base64"
25+
"encoding/json"
26+
"fmt"
27+
"os"
28+
"path/filepath"
29+
"strings"
30+
"testing"
31+
"time"
32+
33+
"google.golang.org/grpc"
34+
"google.golang.org/grpc/credentials"
35+
"google.golang.org/grpc/credentials/insecure"
36+
"google.golang.org/grpc/credentials/jwt"
37+
"google.golang.org/grpc/internal/stubserver"
38+
"google.golang.org/grpc/metadata"
39+
"google.golang.org/grpc/testdata"
40+
41+
testgrpc "google.golang.org/grpc/interop/grpc_testing"
42+
testpb "google.golang.org/grpc/interop/grpc_testing"
43+
)
44+
45+
const defaultTestTimeout = 5 * time.Second
46+
47+
// TestJWTCallCredentials_InsecureTransport_AsCallOption verifies that when JWT
48+
// call credentials are passed as a per-RPC call option over an insecure
49+
// transport, the RPC fails with a meaningful error.
50+
func TestJWTCallCredentials_InsecureTransport_AsCallOption(t *testing.T) {
51+
token := createTestJWT(t, time.Now().Add(time.Hour))
52+
tokenFile := writeTempTokenFile(t, token)
53+
54+
jwtCreds, err := jwt.NewTokenFileCallCredentials(tokenFile)
55+
if err != nil {
56+
t.Fatalf("NewTokenFileCallCredentials(%q) failed: %v", tokenFile, err)
57+
}
58+
59+
ss := &stubserver.StubServer{}
60+
if err := ss.StartServer(grpc.Creds(insecure.NewCredentials())); err != nil {
61+
t.Fatalf("Failed to start server: %v", err)
62+
}
63+
defer ss.Stop()
64+
65+
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
66+
if err != nil {
67+
t.Fatalf("grpc.NewClient(%q) failed: %v", ss.Address, err)
68+
}
69+
defer cc.Close()
70+
71+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
72+
defer cancel()
73+
74+
client := testgrpc.NewTestServiceClient(cc)
75+
_, err = client.EmptyCall(ctx, &testpb.Empty{}, grpc.PerRPCCredentials(jwtCreds))
76+
77+
if err == nil || !strings.Contains(err.Error(), "cannot send secure credentials on an insecure connection") {
78+
t.Fatalf("EmptyCall() error = %v; want error containing %q", err, "cannot send secure credentials on an insecure connection")
79+
}
80+
}
81+
82+
// TestJWTCallCredentials_InsecureTransport_AsDialOption verifies that when JWT
83+
// call credentials are passed as a dial option over an insecure transport, the
84+
// client creation fails with a meaningful error.
85+
func TestJWTCallCredentials_InsecureTransport_AsDialOption(t *testing.T) {
86+
token := createTestJWT(t, time.Now().Add(time.Hour))
87+
tokenFile := writeTempTokenFile(t, token)
88+
89+
jwtCreds, err := jwt.NewTokenFileCallCredentials(tokenFile)
90+
if err != nil {
91+
t.Fatalf("NewTokenFileCallCredentials(%q) failed: %v", tokenFile, err)
92+
}
93+
94+
ss := &stubserver.StubServer{}
95+
if err := ss.StartServer(grpc.Creds(insecure.NewCredentials())); err != nil {
96+
t.Fatalf("Failed to start server: %v", err)
97+
}
98+
defer ss.Stop()
99+
100+
_, err = grpc.NewClient(ss.Address,
101+
grpc.WithTransportCredentials(insecure.NewCredentials()),
102+
grpc.WithPerRPCCredentials(jwtCreds),
103+
)
104+
105+
if err == nil || !strings.Contains(err.Error(), "the credentials require transport level security") {
106+
t.Fatalf("grpc.NewClient() error = %v; want error containing %q", err, "the credentials require transport level security")
107+
}
108+
}
109+
110+
// TestJWTCallCredentials_SecureTransport_AsDialOption verifies that JWT call
111+
// credentials work correctly when passed as a dial option over a secure TLS
112+
// transport.
113+
func TestJWTCallCredentials_SecureTransport_AsDialOption(t *testing.T) {
114+
token := createTestJWT(t, time.Now().Add(time.Hour))
115+
tokenFile := writeTempTokenFile(t, token)
116+
117+
jwtCreds, err := jwt.NewTokenFileCallCredentials(tokenFile)
118+
if err != nil {
119+
t.Fatalf("NewTokenFileCallCredentials(%q) failed: %v", tokenFile, err)
120+
}
121+
122+
wantAuth := "Bearer " + token
123+
ss := &stubserver.StubServer{
124+
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
125+
md, ok := metadata.FromIncomingContext(ctx)
126+
if !ok {
127+
return nil, fmt.Errorf("no metadata received")
128+
}
129+
authHeaders := md.Get("authorization")
130+
if len(authHeaders) != 1 || authHeaders[0] != wantAuth {
131+
return nil, fmt.Errorf("authorization header mismatch: got %v, want %q", authHeaders, wantAuth)
132+
}
133+
return &testpb.Empty{}, nil
134+
},
135+
}
136+
137+
serverCert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
138+
if err != nil {
139+
t.Fatalf("Failed to load server cert: %v", err)
140+
}
141+
if err := ss.StartServer(grpc.Creds(credentials.NewServerTLSFromCert(&serverCert))); err != nil {
142+
t.Fatalf("Failed to start server: %v", err)
143+
}
144+
defer ss.Stop()
145+
146+
clientCreds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "x.test.example.com")
147+
if err != nil {
148+
t.Fatalf("Failed to create client TLS credentials: %v", err)
149+
}
150+
151+
cc, err := grpc.NewClient(ss.Address,
152+
grpc.WithTransportCredentials(clientCreds),
153+
grpc.WithPerRPCCredentials(jwtCreds),
154+
)
155+
if err != nil {
156+
t.Fatalf("grpc.NewClient(%q) failed: %v", ss.Address, err)
157+
}
158+
defer cc.Close()
159+
160+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
161+
defer cancel()
162+
163+
client := testgrpc.NewTestServiceClient(cc)
164+
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
165+
t.Fatalf("EmptyCall() failed: %v", err)
166+
}
167+
}
168+
169+
// TestJWTCallCredentials_SecureTransport_AsCallOption verifies that JWT call
170+
// credentials work correctly when passed as a per-RPC call option over a secure
171+
// TLS transport.
172+
func TestJWTCallCredentials_SecureTransport_AsCallOption(t *testing.T) {
173+
token := createTestJWT(t, time.Now().Add(time.Hour))
174+
tokenFile := writeTempTokenFile(t, token)
175+
176+
jwtCreds, err := jwt.NewTokenFileCallCredentials(tokenFile)
177+
if err != nil {
178+
t.Fatalf("NewTokenFileCallCredentials(%q) failed: %v", tokenFile, err)
179+
}
180+
181+
wantAuth := "Bearer " + token
182+
ss := &stubserver.StubServer{
183+
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
184+
md, ok := metadata.FromIncomingContext(ctx)
185+
if !ok {
186+
return nil, fmt.Errorf("no metadata received")
187+
}
188+
authHeaders := md.Get("authorization")
189+
if len(authHeaders) != 1 || authHeaders[0] != wantAuth {
190+
return nil, fmt.Errorf("authorization header mismatch: got %v, want %q", authHeaders, wantAuth)
191+
}
192+
return &testpb.Empty{}, nil
193+
},
194+
}
195+
196+
serverCert, err := tls.LoadX509KeyPair(testdata.Path("x509/server1_cert.pem"), testdata.Path("x509/server1_key.pem"))
197+
if err != nil {
198+
t.Fatalf("Failed to load server cert: %v", err)
199+
}
200+
if err := ss.StartServer(grpc.Creds(credentials.NewServerTLSFromCert(&serverCert))); err != nil {
201+
t.Fatalf("Failed to start server: %v", err)
202+
}
203+
defer ss.Stop()
204+
205+
clientCreds, err := credentials.NewClientTLSFromFile(testdata.Path("x509/server_ca_cert.pem"), "x.test.example.com")
206+
if err != nil {
207+
t.Fatalf("Failed to create client TLS credentials: %v", err)
208+
}
209+
210+
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(clientCreds))
211+
if err != nil {
212+
t.Fatalf("grpc.NewClient(%q) failed: %v", ss.Address, err)
213+
}
214+
defer cc.Close()
215+
216+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
217+
defer cancel()
218+
219+
client := testgrpc.NewTestServiceClient(cc)
220+
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.PerRPCCredentials(jwtCreds)); err != nil {
221+
t.Fatalf("EmptyCall() failed: %v", err)
222+
}
223+
}
224+
225+
// createTestJWT creates a test JWT token with the specified expiration.
226+
func createTestJWT(t *testing.T, expiration time.Time) string {
227+
t.Helper()
228+
229+
claims := map[string]any{}
230+
if !expiration.IsZero() {
231+
claims["exp"] = expiration.Unix()
232+
}
233+
234+
header := map[string]any{
235+
"typ": "JWT",
236+
"alg": "HS256",
237+
}
238+
headerBytes, err := json.Marshal(header)
239+
if err != nil {
240+
t.Fatalf("Failed to marshal header: %v", err)
241+
}
242+
243+
claimsBytes, err := json.Marshal(claims)
244+
if err != nil {
245+
t.Fatalf("Failed to marshal claims: %v", err)
246+
}
247+
248+
headerB64 := base64.URLEncoding.EncodeToString(headerBytes)
249+
claimsB64 := base64.URLEncoding.EncodeToString(claimsBytes)
250+
251+
// Remove padding for URL-safe base64.
252+
headerB64 = strings.TrimRight(headerB64, "=")
253+
claimsB64 = strings.TrimRight(claimsB64, "=")
254+
255+
// For testing, we use a fake signature.
256+
signature := base64.URLEncoding.EncodeToString([]byte("fake_signature"))
257+
signature = strings.TrimRight(signature, "=")
258+
259+
return fmt.Sprintf("%s.%s.%s", headerB64, claimsB64, signature)
260+
}
261+
262+
// writeTempTokenFile writes the token to a temporary file and returns the path.
263+
func writeTempTokenFile(t *testing.T, token string) string {
264+
t.Helper()
265+
tempDir := t.TempDir()
266+
filePath := filepath.Join(tempDir, "token")
267+
if err := os.WriteFile(filePath, []byte(token), 0600); err != nil {
268+
t.Fatalf("Failed to write temp token file: %v", err)
269+
}
270+
return filePath
271+
}

0 commit comments

Comments
 (0)