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.
- {avios_cli-0.1.4 → avios_cli-0.1.5}/CHANGELOG.md +15 -1
- {avios_cli-0.1.4 → avios_cli-0.1.5}/PKG-INFO +9 -1
- {avios_cli-0.1.4 → avios_cli-0.1.5}/README.md +8 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/__init__.py +1 -1
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/auth.py +139 -12
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/cli.py +17 -2
- {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_auth.py +28 -30
- {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_cli.py +18 -3
- {avios_cli-0.1.4 → avios_cli-0.1.5}/.editorconfig +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/dependabot.yml +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/workflows/ci.yml +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/.github/workflows/publish.yml +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/.gitignore +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/.pre-commit-config.yaml +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/.python-version +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/CONTRIBUTING.md +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/LICENSE +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/SECURITY.md +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/docs/dashboard.svg +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/pyproject.toml +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/scripts/screenshot.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/client.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/config.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/endpoints.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/models.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/session.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/tui/__init__.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/tui/app.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/tui/art.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/tui/styles.tcss +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/src/avios/tui/widgets.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/conftest.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/fixtures/accounts.json +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/fixtures/balance.json +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/fixtures/transactions.json +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_client.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_models.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_session.py +0 -0
- {avios_cli-0.1.4 → avios_cli-0.1.5}/tests/test_tui.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
if not
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
56
|
+
def test_import_from_browser_uses_authenticated_cookies(settings: Settings) -> None:
|
|
56
57
|
session = _session(settings)
|
|
57
|
-
|
|
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
|
|
65
|
-
assert
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
98
|
-
|
|
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
|
-
|
|
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(
|
|
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) ->
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|