blob: d067eb8112ad9fde387f44900182a9ae4fc1ab5a [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import io
import json
import urllib.error
from unittest.mock import patch
import pytest
from oauth_draft.credentials import (
Credentials,
locate_credentials,
refresh_access_token,
)
def write_creds(path, **overrides):
data = {
"client_id": "id-123",
"client_secret": "secret-456",
"refresh_token": "refresh-789",
"from_address": "you@example.com",
}
data.update(overrides)
path.write_text(json.dumps(data))
return path
def test_load_full_record(tmp_path):
p = write_creds(tmp_path / "c.json")
creds = Credentials.load(p)
assert creds.client_id == "id-123"
assert creds.client_secret == "secret-456"
assert creds.refresh_token == "refresh-789"
assert creds.from_address == "you@example.com"
def test_load_missing_required_field(tmp_path):
p = tmp_path / "c.json"
p.write_text(json.dumps({"client_id": "x", "client_secret": "y"}))
with pytest.raises(SystemExit) as excinfo:
Credentials.load(p)
assert "refresh_token" in str(excinfo.value)
assert "from_address" in str(excinfo.value)
def test_load_without_from_address_when_not_required(tmp_path):
p = tmp_path / "c.json"
p.write_text(json.dumps({"client_id": "x", "client_secret": "y", "refresh_token": "z"}))
creds = Credentials.load(p, require_from_address=False)
assert creds.from_address is None
def test_load_empty_value_treated_as_missing(tmp_path):
p = write_creds(tmp_path / "c.json", refresh_token="")
with pytest.raises(SystemExit) as excinfo:
Credentials.load(p)
assert "refresh_token" in str(excinfo.value)
def test_locate_explicit_wins(tmp_path, monkeypatch):
explicit = tmp_path / "explicit.json"
explicit.write_text("{}")
other = tmp_path / "other.json"
other.write_text("{}")
monkeypatch.setenv("GMAIL_OAUTH_CREDENTIALS", str(other))
assert locate_credentials(str(explicit)) == explicit
def test_locate_falls_back_to_env(tmp_path, monkeypatch):
env_path = tmp_path / "env.json"
env_path.write_text("{}")
monkeypatch.setenv("GMAIL_OAUTH_CREDENTIALS", str(env_path))
assert locate_credentials(None) == env_path
def test_locate_raises_when_nothing_exists(tmp_path, monkeypatch):
monkeypatch.delenv("GMAIL_OAUTH_CREDENTIALS", raising=False)
monkeypatch.setattr(
"oauth_draft.credentials.DEFAULT_CREDENTIALS_PATH",
tmp_path / "does-not-exist.json",
)
with pytest.raises(SystemExit) as excinfo:
locate_credentials(None)
assert "No Gmail OAuth credentials found" in str(excinfo.value)
# --- refresh_access_token --------------------------------------------------
CREDS = Credentials(
client_id="cid",
client_secret="secret",
refresh_token="refresh",
from_address="me@example.com",
)
class _FakeResponse:
"""Minimal context-manager stand-in for urllib.request.urlopen()."""
def __init__(self, payload: bytes):
self._payload = payload
def __enter__(self):
return self
def __exit__(self, *exc):
return False
def read(self):
return self._payload
def test_refresh_access_token_returns_access_token():
with patch("oauth_draft.credentials.urllib.request.urlopen") as mock_open:
mock_open.return_value = _FakeResponse(b'{"access_token": "abc-123"}')
token = refresh_access_token(CREDS)
assert token == "abc-123"
# The POST body must include all four OAuth refresh-flow params.
request = mock_open.call_args.args[0]
assert request.method == "POST"
assert request.full_url == "https://oauth2.googleapis.com/token"
body = request.data.decode()
assert "client_id=cid" in body
assert "client_secret=secret" in body
assert "refresh_token=refresh" in body
assert "grant_type=refresh_token" in body
def test_refresh_access_token_raises_on_http_error():
err = urllib.error.HTTPError(
url="https://oauth2.googleapis.com/token",
code=400,
msg="Bad Request",
hdrs=None, # type: ignore[arg-type]
fp=io.BytesIO(b'{"error": "invalid_grant"}'),
)
with patch("oauth_draft.credentials.urllib.request.urlopen", side_effect=err):
with pytest.raises(SystemExit) as excinfo:
refresh_access_token(CREDS)
assert "OAuth token refresh failed (400)" in str(excinfo.value)
assert "invalid_grant" in str(excinfo.value)
def test_refresh_access_token_raises_when_no_token_in_payload():
with patch("oauth_draft.credentials.urllib.request.urlopen") as mock_open:
mock_open.return_value = _FakeResponse(b'{"expires_in": 3600}')
with pytest.raises(SystemExit) as excinfo:
refresh_access_token(CREDS)
assert "no access_token" in str(excinfo.value)