refactor(googleapi): expose authenticated HTTP client
Co-authored-by: Ben Lewis <johnbenjaminlewis@gmail.com>
This commit is contained in:
parent
7ee069d88d
commit
8f6791f9f9
@ -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.
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user