diff --git a/outlook_web/services/graph.py b/outlook_web/services/graph.py index b1e03fe9..4bcfb543 100644 --- a/outlook_web/services/graph.py +++ b/outlook_web/services/graph.py @@ -11,6 +11,7 @@ TOKEN_URL_TEMPLATE = "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token" TOKEN_URL_GRAPH = TOKEN_URL_TEMPLATE.format(tenant="common") DEFAULT_GRAPH_SCOPE = "https://graph.microsoft.com/.default" +DEFAULT_REFRESH_SCOPE = f"{DEFAULT_GRAPH_SCOPE} offline_access" GRAPH_MAIL_READ_SCOPES = ("Mail.Read", "Mail.ReadWrite") # Graph API 返回 401 时表示账号授权失效(与 token endpoint 失败不同) @@ -266,7 +267,7 @@ def test_refresh_token_with_rotation( proxy_url: str = None, *, tenant: str = "common", - scope: str = DEFAULT_GRAPH_SCOPE, + scope: str = DEFAULT_REFRESH_SCOPE, max_retries: int = 3, ) -> tuple[bool, str | None, str | None]: """测试 refresh token 是否有效;如服务端返回新的 refresh_token,则一并返回(用于滚动更新)。 @@ -274,7 +275,7 @@ def test_refresh_token_with_rotation( import time proxies = build_proxies(proxy_url) - resolved_scope = (scope or DEFAULT_GRAPH_SCOPE).strip() or DEFAULT_GRAPH_SCOPE + resolved_scope = (scope or DEFAULT_REFRESH_SCOPE).strip() or DEFAULT_REFRESH_SCOPE url = build_token_url(tenant) data = { "client_id": client_id, diff --git a/tests/test_graph_permission_precheck.py b/tests/test_graph_permission_precheck.py index e567667c..f634785a 100644 --- a/tests/test_graph_permission_precheck.py +++ b/tests/test_graph_permission_precheck.py @@ -102,6 +102,20 @@ def test_token_refresh_failure_returns_auth_expired(self, mock_post): self.assertFalse(result.get("success")) self.assertIsNone(result.get("scope")) + @patch("outlook_web.services.graph.requests.post") + def test_refresh_token_request_includes_offline_access(self, mock_post): + """刷新请求必须申请可滚动更新的 refresh token。""" + from outlook_web.services.graph import test_refresh_token_with_rotation + + mock_post.return_value = _make_graph_token_response("at-123", refresh_token="rt-new") + + success, error_message, new_refresh_token = test_refresh_token_with_rotation("cid", "rt-old") + + self.assertTrue(success) + self.assertIsNone(error_message) + self.assertEqual(new_refresh_token, "rt-new") + self.assertIn("offline_access", mock_post.call_args.kwargs["data"]["scope"].split()) + # ------------------------------------------------------------------ # get_emails_graph 跳过无权限调用 # ------------------------------------------------------------------