feat(multi-tenant): resolve per-caller credentials in every tool
build-image / test (push) Successful in 10s
build-image / build (push) Successful in 42s

All 20 tools now drop the athlete_id/api_key parameters and instead resolve the
authenticated caller's stored, enabled credentials via
credentials.resolve_caller_credentials() (get_access_token().subject -> store).

Security: there is no tool parameter a caller can pass to supply a key, so a
disabled/unapproved user cannot bypass the admin-approval gate — each tool
returns a helpful "not approved / set up your credentials" message instead.
Gear resolution now uses the caller's athlete id rather than an env var.

Tests: conftest autouse fixture runs tool tests as an enabled user; a
parametrized test asserts every tool refuses when unauthorized; existing tool
tests updated (no more athlete_id/api_key kwargs). 221 passing at 91.5%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 19:28:27 -04:00
parent 31eb45c3f8
commit f067f9639a
14 changed files with 256 additions and 195 deletions
+11 -11
View File
@@ -38,19 +38,19 @@ def _run(coro):
# --------------------------------------------------------------------------- #
def test_get_custom_items_lists(monkeypatch):
_patch(monkeypatch, lambda _k: [{"id": 1, "name": "Zones", "type": "ZONES", "description": "d"}])
out = _run(custom_items.get_custom_items(athlete_id="i1"))
out = _run(custom_items.get_custom_items())
assert "Custom Items:" in out
assert "ID: 1" in out and "Name: Zones" in out and "Type: ZONES" in out
def test_get_custom_items_error(monkeypatch):
_patch(monkeypatch, lambda _k: {"error": True, "message": "boom"})
assert "Error fetching custom items: boom" in _run(custom_items.get_custom_items(athlete_id="i1"))
assert "Error fetching custom items: boom" in _run(custom_items.get_custom_items())
def test_get_custom_item_by_id_not_found(monkeypatch):
_patch(monkeypatch, lambda _k: []) # falsy / not a dict
assert "No custom item found with ID 7" in _run(custom_items.get_custom_item_by_id(7, athlete_id="i1"))
assert "No custom item found with ID 7" in _run(custom_items.get_custom_item_by_id(7))
# --------------------------------------------------------------------------- #
@@ -60,7 +60,7 @@ def test_create_builds_full_payload(monkeypatch):
rec = _patch(monkeypatch, lambda _k: {"id": 9, "name": "Chart", "type": "FITNESS_CHART"})
out = _run(
custom_items.create_custom_item(
name="Chart", item_type="FITNESS_CHART", athlete_id="i1",
name="Chart", item_type="FITNESS_CHART",
description="desc", content={"a": 1}, visibility="PRIVATE",
)
)
@@ -78,7 +78,7 @@ def test_create_parses_json_string_content(monkeypatch):
rec = _patch(monkeypatch, lambda _k: {"id": 1, "name": "X", "type": "ZONES"})
_run(
custom_items.create_custom_item(
name="X", item_type="ZONES", athlete_id="i1", content='{"expression": "icu_training_load"}'
name="X", item_type="ZONES", content='{"expression": "icu_training_load"}'
)
)
assert rec.calls[0]["data"]["content"] == {"expression": "icu_training_load"} # parsed to dict
@@ -87,7 +87,7 @@ def test_create_parses_json_string_content(monkeypatch):
def test_create_rejects_invalid_json_string(monkeypatch):
rec = _patch(monkeypatch, lambda _k: {"id": 1})
out = _run(
custom_items.create_custom_item(name="X", item_type="ZONES", athlete_id="i1", content="{not json")
custom_items.create_custom_item(name="X", item_type="ZONES", content="{not json")
)
assert "content must be valid JSON" in out
assert rec.calls == [] # bailed before any request
@@ -95,7 +95,7 @@ def test_create_rejects_invalid_json_string(monkeypatch):
def test_create_error_surfaced(monkeypatch):
_patch(monkeypatch, lambda _k: {"error": True, "message": "bad type"})
out = _run(custom_items.create_custom_item(name="X", item_type="NOPE", athlete_id="i1"))
out = _run(custom_items.create_custom_item(name="X", item_type="NOPE"))
assert "Error creating custom item: bad type" in out
@@ -104,7 +104,7 @@ def test_create_error_surfaced(monkeypatch):
# --------------------------------------------------------------------------- #
def test_update_sends_only_provided_fields_via_put(monkeypatch):
rec = _patch(monkeypatch, lambda _k: {"id": 5, "name": "New", "type": "ZONES"})
_run(custom_items.update_custom_item(item_id=5, athlete_id="i1", name="New"))
_run(custom_items.update_custom_item(item_id=5, name="New"))
call = rec.calls[0]
assert call["method"] == "PUT"
assert call["url"] == "/athlete/i1/custom-item/5"
@@ -113,7 +113,7 @@ def test_update_sends_only_provided_fields_via_put(monkeypatch):
def test_update_invalid_json_string(monkeypatch):
rec = _patch(monkeypatch, lambda _k: {"id": 5})
out = _run(custom_items.update_custom_item(item_id=5, athlete_id="i1", content="{bad"))
out = _run(custom_items.update_custom_item(item_id=5, content="{bad"))
assert "content must be valid JSON" in out
assert rec.calls == []
@@ -123,7 +123,7 @@ def test_update_invalid_json_string(monkeypatch):
# --------------------------------------------------------------------------- #
def test_delete_uses_delete_method(monkeypatch):
rec = _patch(monkeypatch, lambda _k: {})
out = _run(custom_items.delete_custom_item(item_id=3, athlete_id="i1"))
out = _run(custom_items.delete_custom_item(item_id=3))
call = rec.calls[0]
assert call["method"] == "DELETE"
assert call["url"] == "/athlete/i1/custom-item/3"
@@ -132,4 +132,4 @@ def test_delete_uses_delete_method(monkeypatch):
def test_delete_error(monkeypatch):
_patch(monkeypatch, lambda _k: {"error": True, "message": "locked"})
assert "Error deleting custom item: locked" in _run(custom_items.delete_custom_item(item_id=3, athlete_id="i1"))
assert "Error deleting custom item: locked" in _run(custom_items.delete_custom_item(item_id=3))