Skip to content

fix(auth): refresh OAuth2 tokens in long-running watch/subscribe loops#407

Open
meme8383 wants to merge 5 commits intogoogleworkspace:mainfrom
meme8383:fix/392-refresh-long-running-tokens
Open

fix(auth): refresh OAuth2 tokens in long-running watch/subscribe loops#407
meme8383 wants to merge 5 commits intogoogleworkspace:mainfrom
meme8383:fix/392-refresh-long-running-tokens

Conversation

@meme8383
Copy link

Summary

  • Introduce AccessTokenProvider trait and ScopedTokenProvider in auth.rs so long-running helpers can request fresh tokens before each API call instead of holding a single token string that expires after ~1 hour
  • Refactor gmail +watch (watch_pull_loop) and events +subscribe (pull_loop) to accept a &dyn AccessTokenProvider and refresh tokens per-request
  • Refresh tokens before cleanup phase (original tokens may have expired after hours of polling)
  • Extract PUBSUB_API_BASE / GMAIL_API_BASE URL constants to enable test injection

Test plan

  • cargo test — 553 tests pass (3 new)
  • cargo clippy -- -D warnings — clean
  • New: test_scoped_token_provider_uses_get_token — verifies ScopedTokenProvider delegates to get_token
  • New: test_pull_loop_refreshes_pubsub_token_between_requests — fake TCP server + FakeTokenProvider verify each Pub/Sub request uses a distinct fresh token
  • New: test_watch_pull_loop_refreshes_tokens_for_each_request — same pattern for Gmail watch, verifying all 4 HTTP calls (pull, history, message fetch, ack) each get a fresh token

Closes #392

🤖 Generated with Claude Code

@meme8383 meme8383 requested a review from jpoehnelt as a code owner March 11, 2026 15:08
@changeset-bot
Copy link

changeset-bot bot commented Mar 11, 2026

🦋 Changeset detected

Latest commit: f40d037

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@googleworkspace/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@google-cla
Copy link

google-cla bot commented Mar 11, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@googleworkspace-bot googleworkspace-bot added area: auth area: core Core CLI parsing, commands, error handling, utilities labels Mar 11, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the reliability of long-running operations, specifically Gmail watch and Workspace Events subscribe, by implementing a robust mechanism for refreshing OAuth2 access tokens. It introduces a new AccessTokenProvider abstraction, allowing these helpers to acquire fresh tokens on demand for each API request, thereby preventing authentication failures due to token expiration. The changes also improve the testability of API interactions by externalizing base URLs.

Highlights

  • OAuth2 Token Refreshing: Introduced an AccessTokenProvider trait and a ScopedTokenProvider struct to enable long-running helpers to request fresh OAuth2 tokens before each API call, preventing issues with expired tokens.
  • Refactored Long-Running Loops: The gmail +watch (watch_pull_loop) and events +subscribe (pull_loop) functions were refactored to accept and utilize the new AccessTokenProvider, ensuring tokens are refreshed dynamically.
  • Cleanup Phase Token Refresh: Implemented token refreshing before the cleanup phase in long-running helpers, addressing potential failures if original tokens expired during extended polling.
  • API Base URL Extraction: Extracted PUBSUB_API_BASE and GMAIL_API_BASE URL constants to improve testability and maintainability of API endpoints.
Changelog
  • .changeset/refresh-long-running-helper-tokens.md
    • Added a new changeset entry documenting the token refresh feature for long-running helpers.
  • src/auth.rs
    • Defined the AccessTokenProvider trait for fetching access tokens.
    • Implemented ScopedTokenProvider to provide tokens for a fixed set of scopes.
    • Added a new test test_scoped_token_provider_uses_get_token to verify ScopedTokenProvider functionality.
  • src/helpers/events/subscribe.rs
    • Added PUBSUB_API_BASE constant for the Pub/Sub API endpoint.
    • Updated handle_subscribe to initialize and use ScopedTokenProvider for Pub/Sub operations.
    • Modified pull_loop to accept &dyn AccessTokenProvider and refresh tokens before pull and acknowledge requests.
    • Ensured cleanup operations use a freshly acquired token.
    • Added a new test test_pull_loop_refreshes_pubsub_token_between_requests to validate token refreshing in the pull loop.
  • src/helpers/gmail/watch.rs
    • Added PUBSUB_API_BASE and GMAIL_API_BASE constants for API endpoints.
    • Introduced WatchRuntime struct to encapsulate client, token providers, and configuration.
    • Refactored handle_watch to initialize ScopedTokenProvider for both Gmail and Pub/Sub.
    • Modified watch_pull_loop to accept &WatchRuntime and refresh tokens for Pub/Sub pull, Gmail history, Gmail message fetch, and Pub/Sub acknowledge requests.
    • Ensured cleanup operations use a freshly acquired token.
    • Added a new test test_watch_pull_loop_refreshes_tokens_for_each_request to verify token refreshing across all API calls in the watch loop.
Activity
  • All 553 existing cargo test tests passed, with 3 new tests introduced.
  • cargo clippy -- -D warnings ran clean, indicating no new warnings.
  • New tests test_scoped_token_provider_uses_get_token, test_pull_loop_refreshes_pubsub_token_between_requests, and test_watch_pull_loop_refreshes_tokens_for_each_request were added to validate the token refreshing logic.
  • This pull request addresses and closes issue Bug: gmail +watch and events +subscribe exit after ~1 hour due to expired access token #392.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Generative AI Prohibited Use Policy, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust mechanism for refreshing OAuth2 tokens in long-running processes using an AccessTokenProvider trait, improving reliability and correctly refactoring watch and subscribe helpers. However, a critical security concern has been identified: potential URL injection vulnerabilities in both the Gmail watch and Workspace Events subscribe helpers, where user-supplied subscription names and event types are used to construct API URLs without sufficient validation. Additionally, the current implementation fetches tokens excessively within loops, leading to unnecessary performance overhead. It is recommended to address the URL injection by applying strict resource name validation consistently across all helpers, and to optimize token usage by reusing tokens within a single loop iteration where they are still valid.

.bearer_auth(pubsub_token)
let pull_future = runtime
.client
.post(format!("{}/{subscription}:pull", runtime.pubsub_api_base))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The subscription parameter, which can be supplied by the user via the --subscription flag, is used to construct API URLs without any validation or sanitization. An attacker providing a malicious subscription name containing path traversal segments (e.g., ../../) or query parameters could cause the application to make requests to unintended API endpoints on the pubsub.googleapis.com host. This is inconsistent with other parts of the application (like src/helpers/events/subscribe.rs) where resource names are strictly validated using crate::validate::validate_resource_name.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pre-existing on main. Opened #408 to track.

// Delete Pub/Sub topic
if let Some(ref topic) = topic_name {
let _ = client
.delete(format!("{PUBSUB_API_BASE}/{topic}"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The topic name is constructed using a slug derived from user-supplied event-types without validation. If an attacker provides an event type containing path traversal segments (e.g., google.workspace.drive.file.v1.updated/../something), the resulting topic name will contain these segments, leading to a URL path traversal vulnerability when calling the Pub/Sub API. Although project is validated, the slug part of the resource name remains a vector for injection.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also pre-existing on main. Opened #408 to track.

Remove redundant access_token() calls for acknowledge requests (both
loops) and per-message Gmail fetches.  The token obtained at the start
of the iteration / function is still valid and is now reused for all
HTTP calls within the same pass.

Addresses review feedback from gemini-code-assist.
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an AccessTokenProvider trait and ScopedTokenProvider to enable long-running helpers, specifically for Gmail watch and Workspace Events subscribe, to refresh OAuth access tokens before each API call, preventing token expiration issues. The changes involve refactoring token acquisition in src/auth.rs, src/helpers/events/subscribe.rs, and src/helpers/gmail/watch.rs to use these new providers, along with adding new test utilities and tests. Review comments highlight several issues: potential URL path traversal vulnerabilities in both the Pub/Sub subscription and Gmail watch cleanup operations due to unvalidated user input, code duplication of the FakeTokenProvider test utility, and a re-introduced bug in the Gmail watch helper where the gmail_token might still expire during long fetch_and_output_messages operations.

.bearer_auth(pubsub_token)
let pull_future = runtime
.client
.post(format!("{}/{subscription}:pull", runtime.pubsub_api_base))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The subscription variable, which can be directly controlled by the user via the --subscription flag, is used to construct a URL path without validation. This allows for a URL path traversal vulnerability. An attacker could provide a value like ../../evil to manipulate the request URL and potentially hit unauthorized endpoints on the Pub/Sub API (e.g., https://pubsub.googleapis.com/v1/../../evil:pull resolves to https://pubsub.googleapis.com/evil:pull).

To remediate this, validate the subscription argument using crate::validate::validate_resource_name in parse_watch_args or before it is used in handle_watch.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above: This is pre-existing on main. Opened #408 to track.

"https://pubsub.googleapis.com/v1/{subscription}:pull"
))
.bearer_auth(token)
.post(format!("{pubsub_api_base}/{subscription}:pull"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The subscription parameter in pull_loop can be a resource name constructed using a 'slug' derived from user-provided event-types. Since the event types are not validated, they can contain path traversal sequences (e.g., ..) that propagate into the slug and subsequently into the subscription resource name. This allows an attacker to manipulate the Pub/Sub API request URL. For example, a malicious event type could result in a URL like https://pubsub.googleapis.com/v1/projects/{project}/topics/gws-../../evil:pull.

To remediate this, validate each event type in parse_subscribe_args or validate the resulting slug using crate::validate::validate_resource_name before using it to construct resource names.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above: This is pre-existing on main. Opened #408 to track.

Comment on lines +403 to +406
let gmail_token = gmail_token_provider
.access_token()
.await
.context("Failed to get Gmail token")?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The gmail_token is fetched only once at the beginning of fetch_and_output_messages. The subsequent loop over msg_ids (starting on line 422) fetches each message using this same token. If there are many messages to fetch, this loop could take longer than the token's lifetime (~1 hour), causing the operation to fail mid-way. This re-introduces the same class of bug this PR aims to fix.

To ensure tokens do not expire during a long-running fetch operation, consider moving this token acquisition logic inside the for msg_id in msg_ids loop, so that a fresh token is used for each message fetch.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contradicts the earlier review about N+1 Token requests. It seems unlikely that iterating over the messages will take longer than an hour, so I will revert this fix. Another solution may be to re-fetch the token on 401 errors in the loop.

…r message

- Move FakeTokenProvider to auth.rs behind #[cfg(test)] so both helper
  test modules share one definition.
- Refresh the Gmail token per message inside fetch_and_output_messages
  to guard against expiry during large batches.
- Update test expectations accordingly.
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust mechanism for refreshing OAuth2 tokens in long-running processes by using an AccessTokenProvider trait. The refactoring of the gmail +watch and events +subscribe helpers to use this provider is well-executed and addresses the critical issue of token expiration. The addition of constants for API base URLs and comprehensive tests, including mock servers, significantly improves the code's testability and reliability.

I've identified a couple of high-severity issues in the cleanup logic where a failure to refresh a token is not handled correctly, potentially misleading the user. Please see the detailed comments.

Move the success message inside the token-refresh guard and add an else
branch that warns about potential orphaned resources.
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an AccessTokenProvider trait and its implementation, ScopedTokenProvider, to enable refreshing OAuth access tokens for long-running operations. The Gmail watch and Workspace Events subscribe helpers are refactored to use these new token providers, ensuring that fresh access tokens are obtained before each Pub/Sub and Gmail API request, including during cleanup and for individual message fetches in Gmail. This prevents token expiration issues in long-running processes. New test helpers and integration tests were added to validate this token refreshing behavior.

Note: Security Review did not run due to the size of the PR.

A single history batch won't approach the token's 1-hour lifetime,
so per-message refresh is unnecessary overhead. The outer watch_pull_loop
already refreshes tokens each iteration for long-running resilience.
@googleworkspace-bot
Copy link
Collaborator

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces an AccessTokenProvider trait and ScopedTokenProvider to enable long-running helpers (Gmail watch and Workspace Events subscribe) to refresh OAuth access tokens before each API request, addressing potential token expiration. The subscribe.rs and watch.rs helpers are updated to utilize this new token refresh mechanism for Pub/Sub and Gmail API calls, including cleanup operations. New tests were added to verify this behavior. However, the review comments highlight that the newly added tests for token refreshing in both subscribe.rs and watch.rs are insufficient. They currently use single tokens for multiple requests, failing to properly validate that tokens are indeed refreshed between calls. Additionally, a potential issue was identified in watch.rs where the Gmail token for fetching individual messages is only refreshed once per fetch_and_output_messages call, which could still lead to expiration if many messages are processed. This token should be refreshed before each individual message fetch for improved robustness.

Comment on lines +842 to +877
async fn test_pull_loop_refreshes_pubsub_token_between_requests() {
let client = reqwest::Client::new();
let token_provider = FakeTokenProvider::new(["pubsub-token"]);
let (pubsub_base, requests, server) = spawn_subscribe_server().await;
let config = SubscribeConfigBuilder::default()
.subscription(Some(SubscriptionName(
"projects/test/subscriptions/demo".to_string(),
)))
.max_messages(1_u32)
.poll_interval(1_u64)
.once(true)
.build()
.unwrap();

pull_loop(
&client,
&token_provider,
"projects/test/subscriptions/demo",
config,
&pubsub_base,
)
.await
.unwrap();

server.await.unwrap();

let requests = requests.lock().await;
assert_eq!(requests.len(), 2);
assert_eq!(requests[0].0, "/v1/projects/test/subscriptions/demo:pull");
assert_eq!(requests[0].1, "authorization: Bearer pubsub-token");
assert_eq!(
requests[1].0,
"/v1/projects/test/subscriptions/demo:acknowledge"
);
assert_eq!(requests[1].1, "authorization: Bearer pubsub-token");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The test name test_pull_loop_refreshes_pubsub_token_between_requests suggests it verifies token refreshing, but it currently provides only a single token and asserts it's reused. This doesn't test the refresh logic. To properly test this, the FakeTokenProvider should be initialized with two distinct tokens, and the test should assert that the pull and acknowledge requests use different tokens, reflecting the implementation change to refresh the token for each request.

    async fn test_pull_loop_refreshes_pubsub_token_between_requests() {
        let client = reqwest::Client::new();
        let token_provider = FakeTokenProvider::new(["token-for-pull", "token-for-ack"]);
        let (pubsub_base, requests, server) = spawn_subscribe_server().await;
        let config = SubscribeConfigBuilder::default()
            .subscription(Some(SubscriptionName(
                "projects/test/subscriptions/demo".to_string(),
            )))
            .max_messages(1_u32)
            .poll_interval(1_u64)
            .once(true)
            .build()
            .unwrap();

        pull_loop(
            &client,
            &token_provider,
            "projects/test/subscriptions/demo",
            config,
            &pubsub_base,
        )
        .await
        .unwrap();

        server.await.unwrap();

        let requests = requests.lock().await;
        assert_eq!(requests.len(), 2);
        assert_eq!(requests[0].0, "/v1/projects/test/subscriptions/demo:pull");
        assert_eq!(requests[0].1, "authorization: Bearer token-for-pull");
        assert_eq!(
            requests[1].0,
            "/v1/projects/test/subscriptions/demo:acknowledge"
        );
        assert_eq!(requests[1].1, "authorization: Bearer token-for-ack");
    }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test confirms that the token is fetched per loop iteration. As per earlier review, it does not refresh the token within the same loop for the ack.

Comment on lines 424 to 434
for msg_id in msg_ids {
// Fetch full message
let msg_url = format!(
"https://gmail.googleapis.com/gmail/v1/users/me/messages/{}",
"{gmail_api_base}/users/me/messages/{}",
crate::validate::encode_path_segment(&msg_id),
);
let msg_resp = client
.get(&msg_url)
.query(&[("format", msg_format)])
.bearer_auth(gmail_token)
.bearer_auth(&gmail_token)
.send()
.await;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Gmail token is fetched once at the start of fetch_and_output_messages and then reused for all message fetch requests inside the loop. If there are many messages, this loop can take a long time, and the token could expire. To make this more robust, the token should be refreshed inside the loop before each message is fetched.

    for msg_id in msg_ids {
        let gmail_token = gmail_token_provider
            .access_token()
            .await
            .context("Failed to get Gmail token for message fetch")?;
        let msg_url = format!(
            "{gmail_api_base}/users/me/messages/{}",
            crate::validate::encode_path_segment(&msg_id),
        );
        let msg_resp = client
            .get(&msg_url)
            .query(&[("format", msg_format)])
            .bearer_auth(&gmail_token)
            .send()
            .await;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above: this contradicts the earlier review about N+1 Token requests. It seems unlikely that iterating over the messages will take longer than an hour.

Comment on lines +900 to +963
async fn test_watch_pull_loop_refreshes_tokens_for_each_request() {
let client = reqwest::Client::new();
let pubsub_provider = FakeTokenProvider::new(["pubsub-token"]);
let gmail_provider = FakeTokenProvider::new(["gmail-token"]);
let (pubsub_base, gmail_base, requests, server) = spawn_watch_server().await;
let mut last_history_id = 1;
let config = WatchConfig {
project: None,
subscription: None,
topic: None,
label_ids: None,
max_messages: 10,
poll_interval: 1,
format: "full".to_string(),
once: true,
cleanup: false,
output_dir: None,
};
let sanitize_config = crate::helpers::modelarmor::SanitizeConfig {
template: None,
mode: crate::helpers::modelarmor::SanitizeMode::Warn,
};

let runtime = WatchRuntime {
client: &client,
pubsub_token_provider: &pubsub_provider,
gmail_token_provider: &gmail_provider,
sanitize_config: &sanitize_config,
pubsub_api_base: &pubsub_base,
gmail_api_base: &gmail_base,
};

watch_pull_loop(
&runtime,
"projects/test/subscriptions/demo",
&mut last_history_id,
config,
)
.await
.unwrap();

server.await.unwrap();

let requests = requests.lock().await;
assert_eq!(requests.len(), 4);
assert_eq!(requests[0].0, "/v1/projects/test/subscriptions/demo:pull");
assert_eq!(requests[0].1, "authorization: Bearer pubsub-token");
assert_eq!(
requests[1].0,
"/gmail/v1/users/me/history?startHistoryId=1&historyTypes=messageAdded"
);
assert_eq!(requests[1].1, "authorization: Bearer gmail-token");
assert_eq!(
requests[2].0,
"/gmail/v1/users/me/messages/msg%2D1?format=full"
);
assert_eq!(requests[2].1, "authorization: Bearer gmail-token");
assert_eq!(
requests[3].0,
"/v1/projects/test/subscriptions/demo:acknowledge"
);
assert_eq!(requests[3].1, "authorization: Bearer pubsub-token");
assert_eq!(last_history_id, 2);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This test's name implies it checks for token refreshes, but it provides only a single token for Pub/Sub and a single token for Gmail, and asserts they are reused across multiple requests. This doesn't actually test the refresh mechanism. With the suggested implementation changes to refresh tokens before each request, this test would fail. It should be updated to provide distinct tokens for each of the four API calls and assert that the correct token is used for each one, ensuring the refresh logic is properly verified.

    async fn test_watch_pull_loop_refreshes_tokens_for_each_request() {
        let client = reqwest::Client::new();
        let pubsub_provider = FakeTokenProvider::new(["pubsub-token-1", "pubsub-token-2"]);
        let gmail_provider = FakeTokenProvider::new(["gmail-token-1", "gmail-token-2"]);
        let (pubsub_base, gmail_base, requests, server) = spawn_watch_server().await;
        let mut last_history_id = 1;
        let config = WatchConfig {
            project: None,
            subscription: None,
            topic: None,
            label_ids: None,
            max_messages: 10,
            poll_interval: 1,
            format: "full".to_string(),
            once: true,
            cleanup: false,
            output_dir: None,
        };
        let sanitize_config = crate::helpers::modelarmor::SanitizeConfig {
            template: None,
            mode: crate::helpers::modelarmor::SanitizeMode::Warn,
        };

        let runtime = WatchRuntime {
            client: &client,
            pubsub_token_provider: &pubsub_provider,
            gmail_token_provider: &gmail_provider,
            sanitize_config: &sanitize_config,
            pubsub_api_base: &pubsub_base,
            gmail_api_base: &gmail_base,
        };

        watch_pull_loop(
            &runtime,
            "projects/test/subscriptions/demo",
            &mut last_history_id,
            config,
        )
        .await
        .unwrap();

        server.await.unwrap();

        let requests = requests.lock().await;
        assert_eq!(requests.len(), 4);
        assert_eq!(requests[0].0, "/v1/projects/test/subscriptions/demo:pull");
        assert_eq!(requests[0].1, "authorization: Bearer pubsub-token-1");
        assert_eq!(
            requests[1].0,
            "/gmail/v1/users/me/history?startHistoryId=1&historyTypes=messageAdded"
        );
        assert_eq!(requests[1].1, "authorization: Bearer gmail-token-1");
        assert_eq!(
            requests[2].0,
            "/gmail/v1/users/me/messages/msg%2D1?format=full"
        );
        assert_eq!(requests[2].1, "authorization: Bearer gmail-token-2");
        assert_eq!(
            requests[3].0,
            "/v1/projects/test/subscriptions/demo:acknowledge"
        );
        assert_eq!(requests[3].1, "authorization: Bearer pubsub-token-2");
        assert_eq!(last_history_id, 2);
    }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test confirms that the token is fetched per loop iteration. As per earlier review, it does not refresh the token within the same loop for the ack.

@meme8383 meme8383 changed the title fix: refresh OAuth2 tokens in long-running watch/subscribe loops fix(auth): refresh OAuth2 tokens in long-running watch/subscribe loops Mar 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth area: core Core CLI parsing, commands, error handling, utilities

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: gmail +watch and events +subscribe exit after ~1 hour due to expired access token

2 participants