Skip to content

Commit 6cf8bcb

Browse files
authored
Merge pull request #1147 from DiogoRibeiro7/coverage-improvement-plan
Coverage improvement plan
2 parents 6356227 + 2cb03a4 commit 6cf8bcb

10 files changed

Lines changed: 1385 additions & 30 deletions

tests/cli/test_cli_auth.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import io
22
import json
3+
import tempfile
34
import unittest
5+
from pathlib import Path
46
from unittest.mock import MagicMock, patch
57

8+
import requests
9+
610
from codecarbon.cli import auth
711
from codecarbon.cli.auth import _CallbackHandler
812

@@ -100,6 +104,31 @@ def test_validate_access_token_valid(
100104
):
101105
self.assertTrue(auth._validate_access_token("token"))
102106

107+
@patch("codecarbon.cli.auth._discover_endpoints", return_value={"jwks_uri": "jwks"})
108+
@patch(
109+
"codecarbon.cli.auth.requests.get",
110+
side_effect=requests.RequestException("offline"),
111+
)
112+
def test_validate_access_token_network_error_returns_true(
113+
self, mock_get, mock_discover
114+
):
115+
self.assertTrue(auth._validate_access_token("token"))
116+
117+
@patch("codecarbon.cli.auth._discover_endpoints", return_value={"jwks_uri": "jwks"})
118+
@patch("codecarbon.cli.auth.requests.get")
119+
@patch("codecarbon.cli.auth.JsonWebKey.import_key_set")
120+
@patch(
121+
"codecarbon.cli.auth.jose_jwt.decode",
122+
side_effect=Exception("invalid"),
123+
)
124+
def test_validate_access_token_invalid_returns_false(
125+
self, mock_decode, mock_import_key_set, mock_get, mock_discover
126+
):
127+
mock_get.return_value.json.return_value = {"keys": []}
128+
mock_get.return_value.raise_for_status.return_value = None
129+
130+
self.assertFalse(auth._validate_access_token("token"))
131+
103132
@patch("codecarbon.cli.auth.requests.post")
104133
@patch("codecarbon.cli.auth._discover_endpoints")
105134
def test_refresh_tokens(self, mock_discover, mock_post):
@@ -120,6 +149,18 @@ def test_get_access_token_valid(self, mock_validate, mock_load):
120149
mock_validate.return_value = True
121150
self.assertEqual(auth.get_access_token(), "a")
122151

152+
@patch("codecarbon.cli.auth._load_credentials", side_effect=OSError("missing"))
153+
def test_get_access_token_raises_when_credentials_missing(self, mock_load):
154+
with self.assertRaises(ValueError):
155+
auth.get_access_token()
156+
157+
@patch("codecarbon.cli.auth._load_credentials")
158+
def test_get_access_token_raises_when_access_token_missing(self, mock_load):
159+
mock_load.return_value = {"refresh_token": "r"}
160+
161+
with self.assertRaises(ValueError):
162+
auth.get_access_token()
163+
123164
@patch("codecarbon.cli.auth._load_credentials")
124165
@patch("codecarbon.cli.auth._validate_access_token")
125166
@patch("codecarbon.cli.auth._refresh_tokens")
@@ -133,11 +174,138 @@ def test_get_access_token_refresh(
133174
self.assertEqual(auth.get_access_token(), "b")
134175
mock_save.assert_called()
135176

177+
@patch("codecarbon.cli.auth._refresh_tokens", side_effect=Exception("expired"))
178+
@patch("codecarbon.cli.auth._validate_access_token", return_value=False)
179+
@patch(
180+
"codecarbon.cli.auth._load_credentials",
181+
return_value={"access_token": "a", "refresh_token": "r"},
182+
)
183+
def test_get_access_token_refresh_failure_deletes_credentials(
184+
self, mock_load, mock_validate, mock_refresh
185+
):
186+
original_credentials_file = auth._CREDENTIALS_FILE
187+
with tempfile.TemporaryDirectory() as tmp_dir:
188+
temp_credentials = Path(tmp_dir) / "test_credentials.json"
189+
temp_credentials.write_text("{}")
190+
try:
191+
auth._CREDENTIALS_FILE = temp_credentials
192+
with self.assertRaises(ValueError):
193+
auth.get_access_token()
194+
self.assertFalse(temp_credentials.exists())
195+
finally:
196+
auth._CREDENTIALS_FILE = original_credentials_file
197+
136198
@patch("codecarbon.cli.auth._load_credentials")
137199
def test_get_id_token(self, mock_load):
138200
mock_load.return_value = {"id_token": "i"}
139201
self.assertEqual(auth.get_id_token(), "i")
140202

203+
@patch("codecarbon.cli.auth._save_credentials")
204+
@patch("codecarbon.cli.auth.webbrowser.open")
205+
@patch("codecarbon.cli.auth.HTTPServer")
206+
@patch("codecarbon.cli.auth.OAuth2Session")
207+
@patch(
208+
"codecarbon.cli.auth._discover_endpoints",
209+
return_value={
210+
"authorization_endpoint": "https://auth.example/authorize",
211+
"token_endpoint": "https://auth.example/token",
212+
},
213+
)
214+
def test_authorize_success(
215+
self,
216+
mock_discover,
217+
mock_session_cls,
218+
mock_server_cls,
219+
mock_browser_open,
220+
mock_save_credentials,
221+
):
222+
mock_session = MagicMock()
223+
mock_session.create_authorization_url.return_value = (
224+
"https://auth.example/authorize?state=abc",
225+
"abc",
226+
)
227+
mock_session.fetch_token.return_value = {"access_token": "token"}
228+
mock_session_cls.return_value = mock_session
229+
230+
mock_server = MagicMock()
231+
mock_server.handle_request.side_effect = lambda: setattr(
232+
auth._CallbackHandler,
233+
"callback_url",
234+
"http://localhost:8090/callback?code=123",
235+
)
236+
mock_server_cls.return_value = mock_server
237+
238+
auth._CallbackHandler.callback_url = None
239+
auth._CallbackHandler.error = None
240+
241+
result = auth.authorize()
242+
243+
self.assertEqual(result, {"access_token": "token"})
244+
mock_browser_open.assert_called_once()
245+
mock_server.handle_request.assert_called_once()
246+
mock_server.server_close.assert_called_once()
247+
mock_save_credentials.assert_called_once_with({"access_token": "token"})
248+
249+
@patch("codecarbon.cli.auth.HTTPServer")
250+
@patch("codecarbon.cli.auth.OAuth2Session")
251+
@patch(
252+
"codecarbon.cli.auth._discover_endpoints",
253+
return_value={
254+
"authorization_endpoint": "https://auth.example/authorize",
255+
"token_endpoint": "https://auth.example/token",
256+
},
257+
)
258+
def test_authorize_raises_on_callback_error(
259+
self, mock_discover, mock_session_cls, mock_server_cls
260+
):
261+
mock_session = MagicMock()
262+
mock_session.create_authorization_url.return_value = (
263+
"https://auth.example/authorize?state=abc",
264+
"abc",
265+
)
266+
mock_session_cls.return_value = mock_session
267+
mock_server = MagicMock()
268+
mock_server.handle_request.side_effect = lambda: setattr(
269+
auth._CallbackHandler,
270+
"error",
271+
"access_denied",
272+
)
273+
mock_server_cls.return_value = mock_server
274+
275+
auth._CallbackHandler.callback_url = None
276+
auth._CallbackHandler.error = None
277+
278+
with self.assertRaises(ValueError):
279+
auth.authorize()
280+
mock_server.handle_request.assert_called_once()
281+
mock_server.server_close.assert_called_once()
282+
283+
@patch("codecarbon.cli.auth.HTTPServer")
284+
@patch("codecarbon.cli.auth.OAuth2Session")
285+
@patch(
286+
"codecarbon.cli.auth._discover_endpoints",
287+
return_value={
288+
"authorization_endpoint": "https://auth.example/authorize",
289+
"token_endpoint": "https://auth.example/token",
290+
},
291+
)
292+
def test_authorize_raises_when_no_callback_received(
293+
self, mock_discover, mock_session_cls, mock_server_cls
294+
):
295+
mock_session = MagicMock()
296+
mock_session.create_authorization_url.return_value = (
297+
"https://auth.example/authorize?state=abc",
298+
"abc",
299+
)
300+
mock_session_cls.return_value = mock_session
301+
mock_server_cls.return_value = MagicMock()
302+
303+
auth._CallbackHandler.callback_url = None
304+
auth._CallbackHandler.error = None
305+
306+
with self.assertRaises(ValueError):
307+
auth.authorize()
308+
141309

142310
if __name__ == "__main__":
143311
unittest.main()

tests/cli/test_cli_utils.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import configparser
2+
3+
import pytest
4+
5+
from codecarbon.cli import cli_utils
6+
7+
8+
def test_get_config_reads_codecarbon_section(tmp_path):
9+
config_path = tmp_path / ".codecarbon.config"
10+
config_path.write_text("[codecarbon]\napi_endpoint=https://example.test\n")
11+
12+
config = cli_utils.get_config(config_path)
13+
14+
assert config["api_endpoint"] == "https://example.test"
15+
16+
17+
def test_get_config_raises_when_missing(tmp_path):
18+
with pytest.raises(FileNotFoundError):
19+
cli_utils.get_config(tmp_path / ".codecarbon.config")
20+
21+
22+
def test_get_api_endpoint_appends_default_when_missing_key(tmp_path):
23+
config_path = tmp_path / ".codecarbon.config"
24+
config_path.write_text("[codecarbon]\n")
25+
26+
endpoint = cli_utils.get_api_endpoint(config_path)
27+
28+
assert endpoint == "https://api.codecarbon.io"
29+
parser = configparser.ConfigParser()
30+
parser.read(config_path)
31+
assert parser["codecarbon"]["api_endpoint"] == "https://api.codecarbon.io"
32+
33+
34+
def test_get_api_endpoint_returns_default_when_file_missing(tmp_path):
35+
endpoint = cli_utils.get_api_endpoint(tmp_path / ".codecarbon.config")
36+
37+
assert endpoint == "https://api.codecarbon.io"
38+
39+
40+
def test_get_existing_exp_id_returns_none_on_key_error(monkeypatch):
41+
def raise_key_error():
42+
raise KeyError("missing")
43+
44+
monkeypatch.setattr(cli_utils, "get_hierarchical_config", raise_key_error)
45+
46+
assert cli_utils.get_existing_exp_id() is None
47+
48+
49+
def test_get_existing_exp_id_reads_experiment_id(monkeypatch):
50+
monkeypatch.setattr(
51+
cli_utils, "get_hierarchical_config", lambda: {"experiment_id": "exp-123"}
52+
)
53+
54+
assert cli_utils.get_existing_exp_id() == "exp-123"
55+
56+
57+
def test_write_local_exp_id_creates_section(tmp_path):
58+
config_path = tmp_path / ".codecarbon.config"
59+
60+
cli_utils.write_local_exp_id("exp-456", config_path)
61+
62+
parser = configparser.ConfigParser()
63+
parser.read(config_path)
64+
assert parser["codecarbon"]["experiment_id"] == "exp-456"
65+
66+
67+
def test_overwrite_local_config_updates_existing_file(tmp_path):
68+
config_path = tmp_path / ".codecarbon.config"
69+
config_path.write_text("[codecarbon]\nexperiment_id=old\n")
70+
71+
cli_utils.overwrite_local_config("experiment_id", "new", config_path)
72+
73+
parser = configparser.ConfigParser()
74+
parser.read(config_path)
75+
assert parser["codecarbon"]["experiment_id"] == "new"
76+
77+
78+
def test_create_new_config_file_creates_parent_and_file(monkeypatch, tmp_path):
79+
target = tmp_path / "nested" / ".codecarbon.config"
80+
prompts = iter([str(target)])
81+
82+
monkeypatch.setattr(
83+
cli_utils.typer, "prompt", lambda *args, **kwargs: next(prompts)
84+
)
85+
monkeypatch.setattr(cli_utils.Confirm, "ask", lambda *args, **kwargs: True)
86+
87+
created_path = cli_utils.create_new_config_file()
88+
89+
assert created_path == target
90+
assert target.exists()
91+
assert target.read_text() == "[codecarbon]\n"
92+
93+
94+
def test_create_new_config_file_expands_home(monkeypatch, tmp_path):
95+
home = tmp_path / "home"
96+
home.mkdir()
97+
target = home / ".codecarbon.config"
98+
99+
monkeypatch.setattr(cli_utils.Path, "home", lambda: home)
100+
monkeypatch.setattr(
101+
cli_utils.typer, "prompt", lambda *args, **kwargs: "~/.codecarbon.config"
102+
)
103+
104+
created_path = cli_utils.create_new_config_file()
105+
106+
assert created_path == target
107+
assert target.exists()

0 commit comments

Comments
 (0)