refactor(googleapi): expose authenticated HTTP client

Co-authored-by: Ben Lewis <johnbenjaminlewis@gmail.com>
This commit is contained in:
Peter Steinberger 2026-04-28 05:08:14 +01:00
parent 7ee069d88d
commit 8f6791f9f9
No known key found for this signature in database
4 changed files with 65 additions and 10 deletions

View File

@ -18,6 +18,7 @@
- Contacts: add `contacts export` for vCard 4.0 `.vcf` exports by resource, email/name search, or all contacts, including best-effort label categories. (#519, #500) — thanks @dinakars777.
- Contacts: include birthdays in `contacts list` and `contacts search` text and JSON output. (#441)
- Auth: add `gog auth doctor` to diagnose keyring backend/password drift, unreadable file-keyring tokens, and refresh-token failures such as Workspace `invalid_rapt`. (#377, #338)
- Google API: expose a reusable authenticated HTTP client for commands that need custom HTTP policies. (#534) — thanks @johnbenjaminlewis.
### Fixed
- Backup: split Gmail checkpoint commits by row count and plaintext byte size so large messages stay below GitHub's blob limit.

View File

@ -19,16 +19,16 @@ func newDocsServiceForTest(t *testing.T, h http.HandlerFunc) (*docs.Service, fun
t.Helper()
srv := httptest.NewServer(h)
t.Cleanup(srv.Close)
docSvc, err := docs.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
srv.Close()
t.Fatalf("NewDocsService: %v", err)
}
return docSvc, srv.Close
return docSvc, func() {} // retained for call-site compat; cleanup is via t.Cleanup
}
func newDocsCmdContext(t *testing.T) context.Context {

View File

@ -50,9 +50,7 @@ func IsADCMode() bool {
return os.Getenv("GOG_AUTH_MODE") == "adc"
}
func optionsForAccountScopes(ctx context.Context, serviceLabel string, email string, scopes []string) ([]option.ClientOption, error) {
slog.Debug("creating client options with custom scopes", "serviceLabel", serviceLabel, "email", email)
func authenticatedTransport(ctx context.Context, serviceLabel string, email string, scopes []string) (http.RoundTripper, error) {
var ts oauth2.TokenSource
if IsADCMode() {
@ -73,13 +71,22 @@ func optionsForAccountScopes(ctx context.Context, serviceLabel string, email str
}
}
baseTransport := newBaseTransport()
retryTransport := NewRetryTransport(&oauth2.Transport{
return NewRetryTransport(&oauth2.Transport{
Source: ts,
Base: baseTransport,
})
Base: newBaseTransport(),
}), nil
}
func optionsForAccountScopes(ctx context.Context, serviceLabel string, email string, scopes []string) ([]option.ClientOption, error) {
slog.Debug("creating client options with custom scopes", "serviceLabel", serviceLabel, "email", email)
transport, err := authenticatedTransport(ctx, serviceLabel, email, scopes)
if err != nil {
return nil, err
}
c := &http.Client{
Transport: retryTransport,
Transport: transport,
// No Timeout set: large file downloads (Drive videos, etc.) must not
// be cut short. Server responsiveness is guarded by the transport's
// ResponseHeaderTimeout instead.
@ -90,6 +97,23 @@ func optionsForAccountScopes(ctx context.Context, serviceLabel string, email str
return []option.ClientOption{option.WithHTTPClient(c)}, nil
}
// NewHTTPClient returns a raw *http.Client authenticated for the given service
// and account. The caller may set CheckRedirect or other policies on the
// returned client.
func NewHTTPClient(ctx context.Context, service googleauth.Service, email string) (*http.Client, error) {
scopes, err := googleauth.Scopes(service)
if err != nil {
return nil, fmt.Errorf("resolve scopes: %w", err)
}
transport, err := authenticatedTransport(ctx, string(service), email, scopes)
if err != nil {
return nil, err
}
return &http.Client{Transport: transport}, nil
}
func newBaseTransport() *http.Transport {
defaultTransport, ok := http.DefaultTransport.(*http.Transport)
if !ok || defaultTransport == nil {

View File

@ -661,3 +661,33 @@ func TestOptionsForAccountScopes_NoClientTimeout(t *testing.T) {
t.Fatalf("expected ResponseHeaderTimeout to be set on transport")
}
}
func TestNewHTTPClient_NoRedirectPolicy(t *testing.T) {
origRead := readClientCredentials
origOpen := openSecretsStore
t.Cleanup(func() {
readClientCredentials = origRead
openSecretsStore = origOpen
})
readClientCredentials = func(string) (config.ClientCredentials, error) {
return config.ClientCredentials{ClientID: "id", ClientSecret: "secret"}, nil
}
openSecretsStore = func() (secrets.Store, error) {
return &stubStore{tok: secrets.Token{Email: "a@b.com", RefreshToken: "rt"}}, nil
}
client, err := NewHTTPClient(context.Background(), googleauth.ServiceDocs, "a@b.com")
if err != nil {
t.Fatalf("NewHTTPClient: %v", err)
}
if client.Transport == nil {
t.Fatal("expected Transport to be set on client")
}
if client.CheckRedirect != nil {
t.Fatal("expected no CheckRedirect on bare client")
}
}