Skip to content
This repository was archived by the owner on Apr 30, 2026. It is now read-only.

Commit 1df22a1

Browse files
a-dealclaude
andcommitted
Token store Step 4: remove legacy file migration
Remove _LEGACY_BASE_DIR, _migrate_from_files, _legacy_token_dir from TokenStore. save_token no longer returns a legacy path. has_token and load_token read SQLite directly (garth-cache fallback remains for Garmin). _garmin_token_dir in tools.py drops legacy filesystem fallback. 4 tests added, 1 legacy test removed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b9c893e commit 1df22a1

4 files changed

Lines changed: 50 additions & 83 deletions

File tree

engine/gateway/token_store.py

Lines changed: 11 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
logger = logging.getLogger("health-engine.token_store")
2323

24-
_LEGACY_BASE_DIR = Path(os.path.expanduser("~/.config/health-engine/tokens"))
2524
_KEY_PATH = Path(os.path.expanduser("~/.config/health-engine/token.key"))
2625
# Garth needs a real directory to load/dump tokens. We use a stable per-user
2726
# path so garth's token refresh can persist across calls within a process.
@@ -69,13 +68,12 @@ def _now_iso() -> str:
6968
class TokenStore:
7069
"""Manage wearable auth tokens per service and user.
7170
72-
Reads/writes tokens in SQLite (wearable_token table).
73-
Falls back to legacy file paths for migration.
71+
Primary storage: SQLite (wearable_token table).
72+
Garmin tokens fall back to garth-cache directory for import.
7473
"""
7574

76-
def __init__(self, base_dir: str | Path | None = None):
77-
# Legacy base_dir kept for backward compat during migration
78-
self._legacy_dir = Path(base_dir) if base_dir else _LEGACY_BASE_DIR
75+
def __init__(self, base_dir=None):
76+
# base_dir is accepted but ignored (legacy compat for callers).
7977
self._fernet = _get_fernet()
8078

8179
def _encrypt(self, data: bytes) -> bytes:
@@ -139,45 +137,15 @@ def _db_list_tokens(self, user_id: str, service: str) -> list[str]:
139137
).fetchall()
140138
return [r["token_name"] for r in rows]
141139

142-
# --- Legacy file migration ---
143-
144-
def _legacy_token_dir(self, service: str, user_id: str) -> Path:
145-
return self._legacy_dir / service / user_id
146-
147-
def _migrate_from_files(self, user_id: str, service: str):
148-
"""One-time migration: copy file tokens into SQLite, then leave files as backup."""
149-
legacy_dir = self._legacy_token_dir(service, user_id)
150-
if not legacy_dir.exists() or legacy_dir.is_symlink():
151-
return
152-
if self._db_has_tokens(user_id, service):
153-
return # Already migrated
154-
155-
migrated = 0
156-
for fpath in legacy_dir.iterdir():
157-
if not fpath.is_file():
158-
continue
159-
raw = fpath.read_bytes()
160-
# Decrypt if file was Fernet-encrypted, then re-encrypt for DB
161-
if self._fernet and raw.startswith(b"gAAAAA"):
162-
raw = self._fernet.decrypt(raw)
163-
self._db_save_token(user_id, service, fpath.name, raw)
164-
migrated += 1
165-
166-
if migrated:
167-
logger.info("Migrated %d token files for %s/%s from disk to SQLite", migrated, service, user_id)
168-
169140
# --- Public API ---
170141

171-
def save_token(self, service: str, user_id: str, data: dict) -> Path:
172-
"""Save token data to SQLite. Returns a (legacy) directory path for backward compat."""
142+
def save_token(self, service: str, user_id: str, data: dict):
143+
"""Save token data to SQLite."""
173144
raw = json.dumps(data, indent=2).encode()
174145
self._db_save_token(user_id, service, "token.json", raw)
175-
# Return legacy path for any callers that expect it
176-
return self._legacy_token_dir(service, user_id)
177146

178147
def load_token(self, service: str, user_id: str) -> dict | None:
179-
"""Load token data from SQLite. Falls back to files + migrates."""
180-
self._migrate_from_files(user_id, service)
148+
"""Load token data from SQLite."""
181149
raw = self._db_load_token(user_id, service, "token.json")
182150
if raw is None:
183151
return None
@@ -186,11 +154,9 @@ def load_token(self, service: str, user_id: str) -> dict | None:
186154
def has_token(self, service: str, user_id: str) -> bool:
187155
"""Check if tokens exist for a service/user combo.
188156
189-
Checks SQLite first (after legacy migration). For Garmin, falls back
190-
to garth-cache directory: if tokens exist there but not in SQLite,
191-
imports them and returns True.
157+
For Garmin, falls back to garth-cache directory: if tokens exist
158+
there but not in SQLite, imports them and returns True.
192159
"""
193-
self._migrate_from_files(user_id, service)
194160
if self._db_has_tokens(user_id, service):
195161
return True
196162
# Fallback: garth-cache may have tokens from direct garth usage
@@ -220,14 +186,12 @@ def garmin_token_dir(self, user_id: str = "default") -> Path:
220186
221187
Garmin tokens are stored as garth dumps (oauth1_token.json,
222188
oauth2_token.json). This method:
223-
1. Migrates legacy file tokens to SQLite if needed
224-
2. Writes SQLite tokens to a cache directory for garth to read
225-
3. Returns the cache directory path
189+
1. Writes SQLite tokens to a cache directory for garth to read
190+
2. Returns the cache directory path
226191
227192
After garth modifies tokens (e.g. refresh), call sync_garmin_tokens()
228193
to write changes back to SQLite.
229194
"""
230-
self._migrate_from_files(user_id, "garmin")
231195

232196
cache_dir = _GARTH_CACHE_DIR / user_id
233197
cache_dir.mkdir(parents=True, exist_ok=True)

mcp_server/tools.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -147,24 +147,13 @@ def _get_token_store():
147147
def _garmin_token_dir(user_id: str | None = None) -> str | None:
148148
"""Resolve per-user Garmin token directory. Returns None if no tokens exist.
149149
150-
Uses SQLite-backed TokenStore (with automatic migration from legacy files).
151-
NEVER falls back to another user's tokens.
150+
Uses SQLite-backed TokenStore. NEVER falls back to another user's tokens.
152151
"""
153152
ts = _get_token_store()
154153
uid = user_id if user_id and user_id != "default" else "default"
155154

156-
if uid != "default":
157-
if ts.has_token("garmin", uid):
158-
return str(ts.garmin_token_dir(uid))
159-
return None
160-
161-
# Legacy CLI path (no user_id context)
162-
if ts.has_token("garmin", "default"):
163-
return str(ts.garmin_token_dir("default"))
164-
# Check old-style legacy path as absolute fallback for CLI
165-
legacy = Path(os.path.expanduser("~/.config/health-engine/garmin-tokens"))
166-
if legacy.exists() and any(legacy.iterdir()):
167-
return str(legacy)
155+
if ts.has_token("garmin", uid):
156+
return str(ts.garmin_token_dir(uid))
168157
return None
169158

170159

tests/test_garmin.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,13 +466,12 @@ def test_db(self, tmp_path):
466466
def store(self, tmp_path, test_db, monkeypatch):
467467
garth_cache = tmp_path / "garth-cache"
468468
monkeypatch.setattr("engine.gateway.token_store._GARTH_CACHE_DIR", garth_cache)
469-
monkeypatch.setattr("engine.gateway.token_store._LEGACY_BASE_DIR", tmp_path / "legacy")
470469
monkeypatch.setattr(
471470
"engine.gateway.token_store._get_db",
472471
lambda: __import__("engine.gateway.db", fromlist=["get_db"]).get_db(test_db),
473472
)
474473
from engine.gateway.token_store import TokenStore
475-
return TokenStore(base_dir=tmp_path / "legacy")
474+
return TokenStore()
476475

477476
def test_init_accepts_token_store(self, store):
478477
"""GarminClient.__init__ accepts token_store and user_id params."""

tests/test_token_store.py

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,19 @@ def test_db(tmp_path, monkeypatch):
3535
@pytest.fixture
3636
def store_encrypted(token_dir, key_path, test_db, monkeypatch):
3737
"""TokenStore with Fernet encryption enabled."""
38-
monkeypatch.setattr("engine.gateway.token_store._LEGACY_BASE_DIR", token_dir)
3938
monkeypatch.setattr("engine.gateway.token_store._KEY_PATH", key_path)
4039
monkeypatch.delenv("HE_TOKEN_KEY", raising=False)
4140
from engine.gateway.token_store import TokenStore
42-
return TokenStore(base_dir=token_dir)
41+
return TokenStore()
4342

4443

4544
@pytest.fixture
4645
def store_no_crypto(token_dir, test_db, monkeypatch):
4746
"""TokenStore with cryptography unavailable."""
48-
monkeypatch.setattr("engine.gateway.token_store._LEGACY_BASE_DIR", token_dir)
49-
5047
import engine.gateway.token_store as mod
5148
monkeypatch.setattr(mod, "_get_fernet", lambda: None)
5249
from engine.gateway.token_store import TokenStore
53-
store = TokenStore(base_dir=token_dir)
50+
store = TokenStore()
5451
store._fernet = None
5552
return store
5653

@@ -84,17 +81,6 @@ def test_encrypted_blob_is_not_plaintext(store_encrypted, test_db):
8481
assert raw.startswith(b"gAAAAA") # Fernet prefix
8582

8683

87-
def test_legacy_file_migration(store_encrypted, token_dir):
88-
"""Plaintext JSON files from before SQLite migration can be loaded via migration."""
89-
td = token_dir / "google-calendar" / "legacy"
90-
td.mkdir(parents=True)
91-
data = {"access_token": "old_token", "refresh_token": "old_refresh"}
92-
(td / "token.json").write_text(json.dumps(data))
93-
94-
loaded = store_encrypted.load_token("google-calendar", "legacy")
95-
assert loaded == data
96-
97-
9884
def test_no_crypto_roundtrip(store_no_crypto):
9985
"""Without cryptography, tokens still save and load via SQLite."""
10086
data = {"access_token": "plain", "refresh_token": "text"}
@@ -114,9 +100,8 @@ def test_load_missing_returns_none(store_encrypted):
114100
assert store_encrypted.load_token("nonexistent", "nobody") is None
115101

116102

117-
def test_key_auto_generated(key_path, token_dir, monkeypatch):
103+
def test_key_auto_generated(key_path, monkeypatch):
118104
"""Key file is auto-generated on first use."""
119-
monkeypatch.setattr("engine.gateway.token_store._LEGACY_BASE_DIR", token_dir)
120105
monkeypatch.setattr("engine.gateway.token_store._KEY_PATH", key_path)
121106
monkeypatch.delenv("HE_TOKEN_KEY", raising=False)
122107

@@ -129,12 +114,11 @@ def test_key_auto_generated(key_path, token_dir, monkeypatch):
129114
assert mode == "600"
130115

131116

132-
def test_env_var_key(token_dir, monkeypatch):
117+
def test_env_var_key(monkeypatch):
133118
"""HE_TOKEN_KEY env var is used when set."""
134119
from cryptography.fernet import Fernet
135120
key = Fernet.generate_key()
136121
monkeypatch.setenv("HE_TOKEN_KEY", key.decode())
137-
monkeypatch.setattr("engine.gateway.token_store._LEGACY_BASE_DIR", token_dir)
138122

139123
from engine.gateway.token_store import _get_fernet
140124
f = _get_fernet()
@@ -191,3 +175,34 @@ def test_has_token_garth_cache_non_garmin(store_encrypted, garth_cache_dir):
191175

192176
# oura should NOT use garth-cache fallback
193177
assert not store_encrypted.has_token("oura", "andrew")
178+
179+
180+
# --- Step 4: Legacy removal verification ---
181+
182+
183+
def test_no_legacy_migration_attribute(store_encrypted):
184+
"""TokenStore should not have _migrate_from_files."""
185+
assert not hasattr(store_encrypted, "_migrate_from_files")
186+
187+
188+
def test_no_legacy_base_dir_constant():
189+
"""_LEGACY_BASE_DIR should not exist in token_store module."""
190+
import engine.gateway.token_store as mod
191+
assert not hasattr(mod, "_LEGACY_BASE_DIR")
192+
193+
194+
def test_has_token_only_checks_sqlite_and_garth_cache(store_encrypted, token_dir):
195+
"""has_token should NOT check legacy file paths."""
196+
# Create a token file at the old legacy location
197+
legacy = token_dir / "oura" / "andrew"
198+
legacy.mkdir(parents=True)
199+
(legacy / "token.json").write_text('{"token": "legacy"}')
200+
201+
# Should not find it since legacy migration is removed
202+
assert not store_encrypted.has_token("oura", "andrew")
203+
204+
205+
def test_save_token_returns_none(store_encrypted):
206+
"""save_token should not return a legacy directory path."""
207+
result = store_encrypted.save_token("svc", "u1", {"token": "val"})
208+
assert result is None

0 commit comments

Comments
 (0)