avios-cli 0.1.4__tar.gz → 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {avios_cli-0.1.4 → avios_cli-0.1.5}/CHANGELOG.md +15 -1
  2. {avios_cli-0.1.4 → avios_cli-0.1.5}/PKG-INFO +9 -1
  3. {avios_cli-0.1.4 → avios_cli-0.1.5}/README.md +8 -0
  4. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/__init__.py +1 -1
  5. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/auth.py +139 -12
  6. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/cli.py +17 -2
  7. {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_auth.py +28 -30
  8. {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_cli.py +18 -3
  9. {avios_cli-0.1.4 → avios_cli-0.1.5}/.editorconfig +0 -0
  10. {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  11. {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  12. {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/dependabot.yml +0 -0
  14. {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/workflows/ci.yml +0 -0
  15. {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/workflows/publish.yml +0 -0
  16. {avios_cli-0.1.4 → avios_cli-0.1.5}/.gitignore +0 -0
  17. {avios_cli-0.1.4 → avios_cli-0.1.5}/.pre-commit-config.yaml +0 -0
  18. {avios_cli-0.1.4 → avios_cli-0.1.5}/.python-version +0 -0
  19. {avios_cli-0.1.4 → avios_cli-0.1.5}/CONTRIBUTING.md +0 -0
  20. {avios_cli-0.1.4 → avios_cli-0.1.5}/LICENSE +0 -0
  21. {avios_cli-0.1.4 → avios_cli-0.1.5}/SECURITY.md +0 -0
  22. {avios_cli-0.1.4 → avios_cli-0.1.5}/docs/dashboard.svg +0 -0
  23. {avios_cli-0.1.4 → avios_cli-0.1.5}/pyproject.toml +0 -0
  24. {avios_cli-0.1.4 → avios_cli-0.1.5}/scripts/screenshot.py +0 -0
  25. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/client.py +0 -0
  26. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/config.py +0 -0
  27. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/endpoints.py +0 -0
  28. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/models.py +0 -0
  29. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/session.py +0 -0
  30. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/tui/__init__.py +0 -0
  31. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/tui/app.py +0 -0
  32. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/tui/art.py +0 -0
  33. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/tui/styles.tcss +0 -0
  34. {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/tui/widgets.py +0 -0
  35. {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/conftest.py +0 -0
  36. {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/fixtures/accounts.json +0 -0
  37. {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/fixtures/balance.json +0 -0
  38. {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/fixtures/transactions.json +0 -0
  39. {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_client.py +0 -0
  40. {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_models.py +0 -0
  41. {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_session.py +0 -0
  42. {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_tui.py +0 -0
  43. {avios_cli-0.1.4 → avios_cli-0.1.5}/uv.lock +0 -0
@@ -6,6 +6,19 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.5] - 2026-07-24
10
+
11
+ ### Fixed
12
+ - **`--from-browser` only read Chrome's `Default` profile.** If you were logged
13
+ into avios.com in another profile (Profile 1, a work profile, ...), it grabbed
14
+ the wrong profile's cookies and every call failed with "Session expired". It now
15
+ **scans all Chrome profiles, verifies which one actually authenticates**
16
+ (against `/auth-gateway/user`), and uses that — and reports which profile it used.
17
+
18
+ ### Added
19
+ - `avios login --from-browser --profile "Profile 1"` to target a specific browser
20
+ profile, and a clear warning when the imported cookies aren't a logged-in session.
21
+
9
22
  ## [0.1.4] - 2026-07-24
10
23
 
11
24
  ### Fixed
@@ -74,7 +87,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
74
87
  - Reward-flight **availability** search is not yet implemented (needs a British
75
88
  Airways capture).
76
89
 
77
- [Unreleased]: https://github.com/alexechoi/avios-cli/compare/v0.1.4...HEAD
90
+ [Unreleased]: https://github.com/alexechoi/avios-cli/compare/v0.1.5...HEAD
91
+ [0.1.5]: https://github.com/alexechoi/avios-cli/compare/v0.1.4...v0.1.5
78
92
  [0.1.4]: https://github.com/alexechoi/avios-cli/compare/v0.1.3...v0.1.4
79
93
  [0.1.3]: https://github.com/alexechoi/avios-cli/compare/v0.1.2...v0.1.3
80
94
  [0.1.2]: https://github.com/alexechoi/avios-cli/compare/v0.1.1...v0.1.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avios-cli
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: A CLI and TUI for avios.com — view your Avios balance and transactions from the terminal.
5
5
  Project-URL: Homepage, https://github.com/alexechoi/avios-cli
6
6
  Project-URL: Repository, https://github.com/alexechoi/avios-cli
@@ -97,6 +97,14 @@ into avios.com with:
97
97
  uvx --from 'avios-cli[login]' avios login --from-browser
98
98
  ```
99
99
 
100
+ This scans **all your Chrome profiles** and uses whichever one is actually logged
101
+ into avios.com (it tells you which). If you have several profiles, you can target
102
+ one directly:
103
+
104
+ ```bash
105
+ uvx --from 'avios-cli[login]' avios login --from-browser --profile "Profile 1"
106
+ ```
107
+
100
108
  Plain `uvx avios-cli login` won't work — the isolated environment doesn't include
101
109
  the `login` extra.
102
110
 
@@ -62,6 +62,14 @@ into avios.com with:
62
62
  uvx --from 'avios-cli[login]' avios login --from-browser
63
63
  ```
64
64
 
65
+ This scans **all your Chrome profiles** and uses whichever one is actually logged
66
+ into avios.com (it tells you which). If you have several profiles, you can target
67
+ one directly:
68
+
69
+ ```bash
70
+ uvx --from 'avios-cli[login]' avios login --from-browser --profile "Profile 1"
71
+ ```
72
+
65
73
  Plain `uvx avios-cli login` won't work — the isolated environment doesn't include
66
74
  the `login` extra.
67
75
 
@@ -3,4 +3,4 @@
3
3
  View your Avios balance, transactions and account details from the terminal.
4
4
  """
5
5
 
6
- __version__ = "0.1.4"
6
+ __version__ = "0.1.5"
@@ -17,22 +17,62 @@ Two strategies, both requiring the optional ``login`` extra
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import contextlib
20
21
  from collections.abc import Callable, Iterable
22
+ from dataclasses import dataclass
23
+ from pathlib import Path
21
24
  from typing import Any, Protocol
22
25
 
26
+ import httpx
27
+
23
28
  from avios import endpoints
24
- from avios.session import Session
29
+ from avios.config import Settings
30
+ from avios.session import Session, cookie_header_from
25
31
 
26
32
  DASHBOARD_PATH = "/manage-avios/dashboard"
27
33
  LOGIN_TIMEOUT_MS = 300_000 # 5 minutes to complete password + captcha + MFA
28
34
  LOGIN_POLL_MS = 2_000 # how often to check whether the session is authenticated
29
35
  SUPPORTED_BROWSERS = ("chrome", "firefox", "edge", "brave", "safari", "chromium")
30
36
 
37
+ # Chromium-family user-data roots per browser (relative to home), used to discover
38
+ # per-profile cookie databases (Default, "Profile 1", ...).
39
+ _CHROMIUM_ROOTS = {
40
+ "chrome": [
41
+ "Library/Application Support/Google/Chrome",
42
+ ".config/google-chrome",
43
+ "AppData/Local/Google/Chrome/User Data",
44
+ ],
45
+ "brave": [
46
+ "Library/Application Support/BraveSoftware/Brave-Browser",
47
+ ".config/BraveSoftware/Brave-Browser",
48
+ "AppData/Local/BraveSoftware/Brave-Browser/User Data",
49
+ ],
50
+ "edge": [
51
+ "Library/Application Support/Microsoft Edge",
52
+ ".config/microsoft-edge",
53
+ "AppData/Local/Microsoft/Edge/User Data",
54
+ ],
55
+ "chromium": [
56
+ "Library/Application Support/Chromium",
57
+ ".config/chromium",
58
+ "AppData/Local/Chromium/User Data",
59
+ ],
60
+ }
61
+
31
62
 
32
63
  class LoginError(RuntimeError):
33
64
  """Login could not be completed."""
34
65
 
35
66
 
67
+ @dataclass
68
+ class ImportResult:
69
+ """Outcome of importing cookies from a browser."""
70
+
71
+ count: int
72
+ profile: str | None
73
+ authenticated: bool
74
+
75
+
36
76
  class _RawCookie(Protocol):
37
77
  name: str
38
78
  value: str
@@ -143,8 +183,7 @@ def _open_login_context(pw: Any, *, headless: bool, user_data_dir: str) -> Any:
143
183
  )
144
184
 
145
185
 
146
- def _default_loader(browser: str) -> Callable[[], Iterable[_RawCookie]]:
147
- """Return a callable that reads avios.com cookies from ``browser``."""
186
+ def _bc3() -> Any:
148
187
  try:
149
188
  import browser_cookie3 as bc3
150
189
  except ImportError as exc:
@@ -152,30 +191,118 @@ def _default_loader(browser: str) -> Callable[[], Iterable[_RawCookie]]:
152
191
  "browser-cookie3 isn't installed. Run with the 'login' extra:\n"
153
192
  " uvx --from 'avios-cli[login]' avios login --from-browser"
154
193
  ) from exc
194
+ return bc3
195
+
196
+
197
+ def _browser_fn(bc3: Any, browser: str) -> Any:
155
198
  fn = getattr(bc3, browser, None)
156
199
  if fn is None:
157
200
  raise LoginError(
158
201
  f"Unknown browser '{browser}'. Choose from: {', '.join(SUPPORTED_BROWSERS)}"
159
202
  )
160
- return lambda: fn(domain_name="avios.com")
203
+ return fn
204
+
205
+
206
+ def _profile_cookie_files(browser: str, profile: str | None) -> list[tuple[str, Path]]:
207
+ """Discover (profile_name, Cookies-db) pairs for a chromium-family browser."""
208
+ found: list[tuple[str, Path]] = []
209
+ for rel in _CHROMIUM_ROOTS.get(browser, []):
210
+ root = Path.home() / rel
211
+ if not root.exists():
212
+ continue
213
+ for prof in sorted(p for p in root.iterdir() if p.is_dir()):
214
+ if profile and prof.name != profile:
215
+ continue
216
+ for db in (prof / "Cookies", prof / "Network" / "Cookies"):
217
+ if db.exists():
218
+ found.append((prof.name, db))
219
+ break
220
+ return found
221
+
222
+
223
+ def _candidate_cookie_sets(
224
+ browser: str, profile: str | None
225
+ ) -> list[tuple[str, list[dict[str, Any]]]]:
226
+ """Return [(profile_name, avios_cookies)] to try, most specific first."""
227
+ bc3 = _bc3()
228
+ fn = _browser_fn(bc3, browser)
229
+ candidates: list[tuple[str, list[dict[str, Any]]]] = []
230
+ for name, db in _profile_cookie_files(browser, profile):
231
+ try:
232
+ raw = fn(domain_name="avios.com", cookie_file=str(db))
233
+ except Exception:
234
+ continue
235
+ candidates.append((name, _only_avios(_to_cookie_dicts(raw))))
236
+ if not profile:
237
+ # Fallback to browser_cookie3's own default lookup.
238
+ with contextlib.suppress(Exception):
239
+ raw = fn(domain_name="avios.com")
240
+ candidates.append(("default", _only_avios(_to_cookie_dicts(raw))))
241
+ return candidates
242
+
243
+
244
+ def _cookies_authenticate(settings: Settings, cookies: list[dict[str, Any]]) -> bool:
245
+ """True if these cookies yield a 200 from the auth endpoint."""
246
+ header = cookie_header_from(cookies)
247
+ if not header:
248
+ return False
249
+ try:
250
+ with httpx.Client(
251
+ base_url=settings.base_url,
252
+ timeout=15.0,
253
+ follow_redirects=False,
254
+ headers={
255
+ "accept": "application/json",
256
+ "user-agent": settings.user_agent,
257
+ "cookie": header,
258
+ },
259
+ ) as client:
260
+ return client.get(endpoints.AUTH_USER).status_code == 200
261
+ except httpx.HTTPError:
262
+ return False
161
263
 
162
264
 
163
265
  def import_from_browser(
164
266
  session: Session | None = None,
165
267
  browser: str = "chrome",
166
268
  *,
269
+ profile: str | None = None,
167
270
  loader: Callable[[], Iterable[_RawCookie]] | None = None,
168
- ) -> int:
169
- """Import the avios.com session cookie from a running browser.
271
+ authenticator: Callable[[list[dict[str, Any]]], bool] | None = None,
272
+ ) -> ImportResult:
273
+ """Import the avios.com session cookie from a browser profile.
170
274
 
171
- Returns the number of avios.com cookies imported.
275
+ Scans the browser's profiles, prefers the one whose cookies actually
276
+ authenticate, and saves them. ``loader``/``authenticator`` are test seams.
172
277
  """
173
278
  session = session or Session()
174
- load = loader or _default_loader(browser)
175
- cookies = _only_avios(_to_cookie_dicts(load()))
176
- if not cookies:
279
+ authenticate = authenticator or (lambda c: _cookies_authenticate(session.settings, c))
280
+
281
+ if loader is not None:
282
+ candidates: list[tuple[str, list[dict[str, Any]]]] = [
283
+ ("browser", _only_avios(_to_cookie_dicts(loader())))
284
+ ]
285
+ else:
286
+ candidates = _candidate_cookie_sets(browser, profile)
287
+
288
+ working: tuple[str, list[dict[str, Any]]] | None = None
289
+ fallback: tuple[str, list[dict[str, Any]]] | None = None
290
+ for name, cookies in candidates:
291
+ if not cookies:
292
+ continue
293
+ if fallback is None:
294
+ fallback = (name, cookies)
295
+ if authenticate(cookies):
296
+ working = (name, cookies)
297
+ break
298
+
299
+ chosen = working or fallback
300
+ if chosen is None:
301
+ where = f"profile '{profile}'" if profile else browser
177
302
  raise LoginError(
178
- f"No avios.com cookies found in {browser}. Log into avios.com there first."
303
+ f"No avios.com cookies found in {where}. Log into avios.com in that "
304
+ "browser first (and check the profile with --profile)."
179
305
  )
306
+ name, cookies = chosen
180
307
  session.save_cookies(cookies)
181
- return len(cookies)
308
+ return ImportResult(count=len(cookies), profile=name, authenticated=working is not None)
@@ -98,6 +98,9 @@ def login(
98
98
  False, "--from-browser", help="Import the cookie from a running browser (no popup)."
99
99
  ),
100
100
  browser: str = typer.Option("chrome", help="Browser to import from (with --from-browser)."),
101
+ profile: str | None = typer.Option(
102
+ None, help="Browser profile name for --from-browser (e.g. 'Default', 'Profile 1')."
103
+ ),
101
104
  headless: bool = typer.Option(
102
105
  False, help="Run the login browser headless (only works without captcha/MFA)."
103
106
  ),
@@ -106,7 +109,19 @@ def login(
106
109
  session = Session()
107
110
  try:
108
111
  if from_browser:
109
- count = import_from_browser(session, browser)
112
+ result = import_from_browser(session, browser, profile=profile)
113
+ where = f" from {browser} profile '{result.profile}'" if result.profile else ""
114
+ if not result.authenticated:
115
+ console.print(
116
+ f"[yellow]Imported {result.count} cookie(s){where}, but they're not a "
117
+ "logged-in avios session.[/]"
118
+ )
119
+ console.print(
120
+ "Log into avios.com in that profile, pick another with "
121
+ "[bold]--profile[/], or use [bold]avios login[/] (browser)."
122
+ )
123
+ raise typer.Exit(2)
124
+ console.print(f"[green]Logged in{where}.[/] Saved {result.count} cookie(s).")
110
125
  else:
111
126
  console.print(
112
127
  "Opening a browser — log in normally (password, captcha, SMS code). "
@@ -114,11 +129,11 @@ def login(
114
129
  "captures your session automatically."
115
130
  )
116
131
  count = login_via_browser(session, headless=headless)
132
+ console.print(f"[green]Logged in.[/] Saved {count} avios cookie(s).")
117
133
  except LoginError as exc:
118
134
  console.print(f"[red]{escape(str(exc))}[/]")
119
135
  raise typer.Exit(1) from exc
120
136
 
121
- console.print(f"[green]Logged in.[/] Saved {count} avios cookie(s).")
122
137
  try:
123
138
  balance = AviosClient(session).get_balance()
124
139
  console.print(
@@ -14,8 +14,9 @@ import types
14
14
  import pytest
15
15
 
16
16
  from avios.auth import (
17
+ ImportResult,
17
18
  LoginError,
18
- _default_loader,
19
+ _browser_fn,
19
20
  _only_avios,
20
21
  _open_login_context,
21
22
  _to_cookie_dicts,
@@ -52,52 +53,49 @@ def test_only_avios_filters_foreign_domains() -> None:
52
53
  assert _only_avios(cookies) == [cookies[0]]
53
54
 
54
55
 
55
- def test_import_from_browser_saves_only_avios(settings: Settings) -> None:
56
+ def test_import_from_browser_uses_authenticated_cookies(settings: Settings) -> None:
56
57
  session = _session(settings)
57
- count = import_from_browser(
58
+ result = import_from_browser(
58
59
  session,
59
60
  loader=lambda: [
60
61
  FakeCookie("appSession", "abc", "www.avios.com"),
61
62
  FakeCookie("_ga", "junk", ".google.com"),
62
63
  ],
64
+ authenticator=lambda cookies: True,
63
65
  )
64
- assert count == 1
65
- assert session.is_authenticated()
66
+ assert isinstance(result, ImportResult)
67
+ assert result.count == 1 # only the avios cookie
68
+ assert result.authenticated is True
66
69
  assert session.load_cookies() == [
67
70
  {"name": "appSession", "value": "abc", "domain": "www.avios.com"}
68
71
  ]
69
72
 
70
73
 
71
- def test_import_from_browser_no_cookies_raises(settings: Settings) -> None:
72
- with pytest.raises(LoginError, match="No avios.com cookies"):
73
- import_from_browser(_session(settings), loader=lambda: [FakeCookie("_ga", "j", ".x.com")])
74
-
75
-
76
- def _fake_bc3(monkeypatch: pytest.MonkeyPatch, **funcs: object) -> None:
77
- module = types.ModuleType("browser_cookie3")
78
- for name, fn in funcs.items():
79
- setattr(module, name, fn)
80
- monkeypatch.setitem(sys.modules, "browser_cookie3", module)
81
-
82
-
83
- def test_default_loader_passes_domain(monkeypatch: pytest.MonkeyPatch) -> None:
84
- seen: dict[str, str] = {}
74
+ def test_import_from_browser_reports_unauthenticated(settings: Settings) -> None:
75
+ session = _session(settings)
76
+ result = import_from_browser(
77
+ session,
78
+ loader=lambda: [FakeCookie("appSession", "stale", "www.avios.com")],
79
+ authenticator=lambda cookies: False, # cookies exist but don't authenticate
80
+ )
81
+ assert result.count == 1
82
+ assert result.authenticated is False # saved as best-effort, flagged as not working
85
83
 
86
- def chrome(**kwargs: str) -> list[FakeCookie]:
87
- seen.update(kwargs)
88
- return [FakeCookie("appSession", "x", "www.avios.com")]
89
84
 
90
- _fake_bc3(monkeypatch, chrome=chrome)
91
- loader = _default_loader("chrome")
92
- cookies = list(loader())
93
- assert seen["domain_name"] == "avios.com"
94
- assert cookies[0].name == "appSession"
85
+ def test_import_from_browser_no_cookies_raises(settings: Settings) -> None:
86
+ with pytest.raises(LoginError, match="No avios.com cookies"):
87
+ import_from_browser(
88
+ _session(settings),
89
+ loader=lambda: [FakeCookie("_ga", "j", ".x.com")],
90
+ authenticator=lambda cookies: True,
91
+ )
95
92
 
96
93
 
97
- def test_default_loader_unknown_browser(monkeypatch: pytest.MonkeyPatch) -> None:
98
- _fake_bc3(monkeypatch, chrome=lambda **k: [])
94
+ def test_browser_fn_unknown_browser() -> None:
95
+ bc3 = types.ModuleType("browser_cookie3")
96
+ bc3.chrome = lambda **k: [] # type: ignore[attr-defined]
99
97
  with pytest.raises(LoginError, match="Unknown browser"):
100
- _default_loader("nope")
98
+ _browser_fn(bc3, "nope")
101
99
 
102
100
 
103
101
  def test_login_via_browser_without_playwright(
@@ -13,7 +13,7 @@ import pytest
13
13
  from typer.testing import CliRunner
14
14
 
15
15
  from avios import __version__
16
- from avios.auth import LoginError
16
+ from avios.auth import ImportResult, LoginError
17
17
  from avios.cli import app
18
18
  from avios.models import Account, Balance, Overview, Profile, Transaction
19
19
  from avios.session import NotAuthenticated, SessionExpired
@@ -134,16 +134,31 @@ def test_session_expired(monkeypatch: pytest.MonkeyPatch) -> None:
134
134
 
135
135
  # -- login / logout ----------------------------------------------------------
136
136
  def test_login_from_browser(monkeypatch: pytest.MonkeyPatch) -> None:
137
- monkeypatch.setattr("avios.cli.import_from_browser", lambda session, browser: 2)
137
+ monkeypatch.setattr(
138
+ "avios.cli.import_from_browser",
139
+ lambda session, browser, profile=None: ImportResult(2, "Default", True),
140
+ )
138
141
  monkeypatch.setattr("avios.cli.AviosClient", FakeClient)
139
142
  result = runner.invoke(app, ["login", "--from-browser"])
140
143
  assert result.exit_code == 0
141
144
  assert "Logged in" in result.stdout
145
+ assert "Default" in result.stdout
142
146
  assert "Balance" in result.stdout
143
147
 
144
148
 
149
+ def test_login_from_browser_unauthenticated(monkeypatch: pytest.MonkeyPatch) -> None:
150
+ monkeypatch.setattr(
151
+ "avios.cli.import_from_browser",
152
+ lambda session, browser, profile=None: ImportResult(20, "Default", False),
153
+ )
154
+ result = runner.invoke(app, ["login", "--from-browser"])
155
+ assert result.exit_code == 2
156
+ normalized = " ".join(result.stdout.split()) # Rich wraps lines
157
+ assert "not a logged-in avios session" in normalized
158
+
159
+
145
160
  def test_login_error(monkeypatch: pytest.MonkeyPatch) -> None:
146
- def boom(session: Any, browser: str) -> int:
161
+ def boom(session: Any, browser: str, profile: str | None = None) -> ImportResult:
147
162
  raise LoginError("nope")
148
163
 
149
164
  monkeypatch.setattr("avios.cli.import_from_browser", boom)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes