fix(photon): unify project identifiers and update documentation for Spectrum provisioning
Co-Authored-By: Marvin <marvin@photon.codes> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6b0eb4de0
commit
5b3fa26366
@ -54,8 +54,10 @@ hermes gateway start
|
|||||||
1. **Device login** (RFC 8628, `client_id=photon-cli`) — opens
|
1. **Device login** (RFC 8628, `client_id=photon-cli`) — opens
|
||||||
`https://app.photon.codes/` for approval and stores the bearer token.
|
`https://app.photon.codes/` for approval and stores the bearer token.
|
||||||
2. **Find or create** the `Hermes Agent` project on the Photon dashboard.
|
2. **Find or create** the `Hermes Agent` project on the Photon dashboard.
|
||||||
3. **Enable Spectrum**, read the project's `spectrumProjectId`, rotate the
|
3. **Provision the project secret** — mint a fresh project secret (the
|
||||||
project secret, and persist both.
|
dashboard reveals it only once) and persist it to `~/.hermes/.env` so the
|
||||||
|
sidecar can authenticate `spectrum-ts`. Spectrum is always on, so there's no
|
||||||
|
separate enable step.
|
||||||
4. **Register your phone number** as a Spectrum user (idempotent — skipped if
|
4. **Register your phone number** as a Spectrum user (idempotent — skipped if
|
||||||
a user with that number already exists).
|
a user with that number already exists).
|
||||||
5. **Print the assigned iMessage line** — the number you text to reach your
|
5. **Print the assigned iMessage line** — the number you text to reach your
|
||||||
@ -75,7 +77,7 @@ Runtime SDK credentials live in `~/.hermes/.env` (the same place every other
|
|||||||
channel keeps its token), and the adapter reads them from the environment:
|
channel keeps its token), and the adapter reads them from the environment:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
PHOTON_PROJECT_ID=<spectrumProjectId> # the SDK's projectId
|
PHOTON_PROJECT_ID=<projectId> # the SDK's projectId (same as the dashboard project id)
|
||||||
PHOTON_PROJECT_SECRET=<projectSecret>
|
PHOTON_PROJECT_SECRET=<projectSecret>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -89,8 +91,8 @@ Management metadata lives in `~/.hermes/auth.json` under `credential_pool`:
|
|||||||
],
|
],
|
||||||
"photon_project": [
|
"photon_project": [
|
||||||
{
|
{
|
||||||
"dashboard_project_id": "<dashboard id>",
|
"dashboard_project_id": "<project id>",
|
||||||
"spectrum_project_id": "<spectrumProjectId>",
|
"spectrum_project_id": "<project id>",
|
||||||
"project_secret": "<projectSecret>",
|
"project_secret": "<projectSecret>",
|
||||||
"name": "Hermes Agent"
|
"name": "Hermes Agent"
|
||||||
}
|
}
|
||||||
@ -99,9 +101,9 @@ Management metadata lives in `~/.hermes/auth.json` under `credential_pool`:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note on ids.** A Photon project has two identifiers: the dashboard `id`
|
> **Note on ids.** A Photon project's dashboard id and its Spectrum project id
|
||||||
> (used for management API calls) and the `spectrumProjectId` (what the SDK
|
> are the same value, exposed as `PHOTON_PROJECT_ID`. The `dashboard_project_id`
|
||||||
> authenticates with). `PHOTON_PROJECT_ID` is the **spectrum** id.
|
> and `spectrum_project_id` keys in `auth.json` both hold that id.
|
||||||
|
|
||||||
## Configuration knobs
|
## Configuration knobs
|
||||||
|
|
||||||
|
|||||||
@ -3,29 +3,29 @@ Photon Dashboard API client + device-code login flow.
|
|||||||
|
|
||||||
This module is pure Python — it intentionally does not depend on
|
This module is pure Python — it intentionally does not depend on
|
||||||
``spectrum-ts``. Every management-plane operation (login, find/create
|
``spectrum-ts``. Every management-plane operation (login, find/create
|
||||||
project, enable Spectrum, rotate the project secret, register a user,
|
project, rotate the project secret, register a user, list the assigned
|
||||||
list the assigned iMessage line) talks to Photon's **Dashboard API** on a
|
iMessage line) talks to Photon's **Dashboard API** on a single host,
|
||||||
single host, exactly like the official Photon CLI (``photon-hq/cli``):
|
exactly like the official Photon CLI (``photon-hq/cli``):
|
||||||
|
|
||||||
Dashboard API https://app.photon.codes/api/...
|
Dashboard API https://app.photon.codes/api/...
|
||||||
OAuth 2.0 device flow, Bearer access token
|
OAuth 2.0 device flow, Bearer access token
|
||||||
|
|
||||||
A Photon project carries two distinct identifiers:
|
A Photon project has a single identifier: the dashboard ``id`` *is* the
|
||||||
|
Spectrum Cloud project id. They used to diverge (a separate
|
||||||
* ``id`` — the Dashboard project id (used in API paths)
|
``spectrumProjectId`` field), but the dashboard unified them — every
|
||||||
* ``spectrumProjectId`` — the Spectrum Cloud project id, populated when
|
project is created with matching ids and the pre-existing diverged rows
|
||||||
Spectrum is enabled on the project
|
were backfilled so ``project.id == spectrumProjectId`` everywhere
|
||||||
|
(dashboard ENG-1582). Spectrum is always enabled and provisioned at
|
||||||
|
create-time, so there is no enable/toggle step anymore.
|
||||||
|
|
||||||
The ``spectrum-ts`` SDK (run by the Node sidecar) authenticates to Spectrum
|
The ``spectrum-ts`` SDK (run by the Node sidecar) authenticates to Spectrum
|
||||||
Cloud with ``(spectrumProjectId, projectSecret)`` — so the value we persist
|
Cloud with ``(id, projectSecret)`` — the same ``id`` used in Dashboard API
|
||||||
as ``PHOTON_PROJECT_ID`` for the runtime is the **spectrumProjectId**, not
|
paths — which we persist as ``PHOTON_PROJECT_ID`` for the runtime.
|
||||||
the Dashboard ``id``. The Dashboard ``id`` is kept only for management
|
|
||||||
calls.
|
|
||||||
|
|
||||||
Credential storage mirrors every other Hermes channel:
|
Credential storage mirrors every other Hermes channel:
|
||||||
|
|
||||||
* runtime SDK creds -> ``~/.hermes/.env`` (``PHOTON_PROJECT_ID`` =
|
* runtime SDK creds -> ``~/.hermes/.env`` (``PHOTON_PROJECT_ID`` =
|
||||||
spectrumProjectId, ``PHOTON_PROJECT_SECRET``) via ``save_env_value``
|
project id, ``PHOTON_PROJECT_SECRET``) via ``save_env_value``
|
||||||
* management metadata -> ``~/.hermes/auth.json`` under
|
* management metadata -> ``~/.hermes/auth.json`` under
|
||||||
``credential_pool.photon`` (device token),
|
``credential_pool.photon`` (device token),
|
||||||
``credential_pool.photon_project`` (dashboard id, spectrum id, name), and
|
``credential_pool.photon_project`` (dashboard id, spectrum id, name), and
|
||||||
@ -148,8 +148,8 @@ def load_project_credentials() -> Tuple[Optional[str], Optional[str]]:
|
|||||||
|
|
||||||
Precedence: process env (``~/.hermes/.env`` is loaded into the gateway's
|
Precedence: process env (``~/.hermes/.env`` is loaded into the gateway's
|
||||||
environment at startup) wins, then ``auth.json`` for offline / status
|
environment at startup) wins, then ``auth.json`` for offline / status
|
||||||
use. This is the pair the Node sidecar feeds to ``spectrum-ts`` — the id
|
use. This is the pair the Node sidecar feeds to ``spectrum-ts``; the id
|
||||||
is the **spectrumProjectId**, not the Dashboard id.
|
is the unified project id (dashboard id == spectrumProjectId).
|
||||||
"""
|
"""
|
||||||
env_id = os.getenv("PHOTON_PROJECT_ID")
|
env_id = os.getenv("PHOTON_PROJECT_ID")
|
||||||
env_sec = os.getenv("PHOTON_PROJECT_SECRET")
|
env_sec = os.getenv("PHOTON_PROJECT_SECRET")
|
||||||
@ -166,14 +166,26 @@ def load_project_credentials() -> Tuple[Optional[str], Optional[str]]:
|
|||||||
|
|
||||||
|
|
||||||
def load_dashboard_project_id() -> Optional[str]:
|
def load_dashboard_project_id() -> Optional[str]:
|
||||||
"""Return the Dashboard project id (for management API calls)."""
|
"""Return the project id used for management API calls.
|
||||||
|
|
||||||
|
Post-unification the dashboard id and the Spectrum id are the same value,
|
||||||
|
so we prefer the stored ``spectrum_project_id``: for pre-backfill installs
|
||||||
|
the old ``dashboard_project_id`` is the diverged id that the unification
|
||||||
|
rewrote (it now 404s), while the Spectrum id always matches the live row.
|
||||||
|
Falls back to the legacy keys for older records.
|
||||||
|
"""
|
||||||
env_id = os.getenv("PHOTON_DASHBOARD_PROJECT_ID")
|
env_id = os.getenv("PHOTON_DASHBOARD_PROJECT_ID")
|
||||||
if env_id:
|
if env_id:
|
||||||
return env_id
|
return env_id
|
||||||
auth = _load_auth()
|
auth = _load_auth()
|
||||||
proj = auth.get("credential_pool", {}).get("photon_project") or []
|
proj = auth.get("credential_pool", {}).get("photon_project") or []
|
||||||
if isinstance(proj, list) and proj:
|
if isinstance(proj, list) and proj:
|
||||||
return proj[0].get("dashboard_project_id") or proj[0].get("project_id")
|
entry = proj[0]
|
||||||
|
return (
|
||||||
|
entry.get("spectrum_project_id")
|
||||||
|
or entry.get("dashboard_project_id")
|
||||||
|
or entry.get("project_id")
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -646,30 +658,23 @@ def find_project_by_name(token: str, name: str) -> Optional[Dict[str, Any]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_project(token: str, project_id: str) -> Dict[str, Any]:
|
|
||||||
"""GET ``/api/projects/{id}`` — includes ``spectrum`` + ``spectrumProjectId``."""
|
|
||||||
if httpx is None:
|
|
||||||
raise RuntimeError("httpx is required for Photon")
|
|
||||||
url = f"{_dashboard_host()}/api/projects/{project_id}"
|
|
||||||
resp = httpx.get(url, headers=_bearer(token), timeout=30.0)
|
|
||||||
resp.raise_for_status()
|
|
||||||
return resp.json() or {}
|
|
||||||
|
|
||||||
|
|
||||||
def create_project(
|
def create_project(
|
||||||
token: str,
|
token: str,
|
||||||
*,
|
*,
|
||||||
name: str = DEFAULT_PROJECT_NAME,
|
name: str = DEFAULT_PROJECT_NAME,
|
||||||
location: str = "United States",
|
location: str = "United States",
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""POST ``/api/projects`` with ``spectrum: true`` and return ``{success, id}``."""
|
"""POST ``/api/projects`` and return ``{success, id}``.
|
||||||
|
|
||||||
|
Spectrum is always provisioned at create-time, so the request body no
|
||||||
|
longer carries a ``spectrum`` flag (the field was dropped from the API).
|
||||||
|
"""
|
||||||
if httpx is None:
|
if httpx is None:
|
||||||
raise RuntimeError("httpx is required for Photon project creation")
|
raise RuntimeError("httpx is required for Photon project creation")
|
||||||
url = f"{_dashboard_host()}/api/projects"
|
url = f"{_dashboard_host()}/api/projects"
|
||||||
body: Dict[str, Any] = {
|
body: Dict[str, Any] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"location": location,
|
"location": location,
|
||||||
"spectrum": True,
|
|
||||||
"template": False,
|
"template": False,
|
||||||
"observability": False,
|
"observability": False,
|
||||||
}
|
}
|
||||||
@ -683,29 +688,6 @@ def create_project(
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def ensure_spectrum_enabled(token: str, project_id: str) -> Dict[str, Any]:
|
|
||||||
"""Enable Spectrum on the project if needed; return the project dict.
|
|
||||||
|
|
||||||
The dashboard exposes Spectrum as a toggle, so we only flip it when
|
|
||||||
``spectrum`` is currently false, then re-fetch to pick up the freshly
|
|
||||||
populated ``spectrumProjectId``.
|
|
||||||
"""
|
|
||||||
if httpx is None:
|
|
||||||
raise RuntimeError("httpx is required for Photon")
|
|
||||||
proj = get_project(token, project_id)
|
|
||||||
if not proj.get("spectrum"):
|
|
||||||
url = f"{_dashboard_host()}/api/projects/{project_id}/spectrum/toggle"
|
|
||||||
resp = httpx.post(url, json={}, headers=_bearer(token), timeout=30.0)
|
|
||||||
resp.raise_for_status()
|
|
||||||
proj = get_project(token, project_id)
|
|
||||||
if not proj.get("spectrumProjectId"):
|
|
||||||
raise RuntimeError(
|
|
||||||
"Spectrum is enabled but the project has no spectrumProjectId yet — "
|
|
||||||
"retry in a moment, or enable Spectrum from the dashboard."
|
|
||||||
)
|
|
||||||
return proj
|
|
||||||
|
|
||||||
|
|
||||||
def regenerate_project_secret(token: str, project_id: str) -> str:
|
def regenerate_project_secret(token: str, project_id: str) -> str:
|
||||||
"""POST ``/api/projects/{id}/regenerate-secret`` → the new project secret.
|
"""POST ``/api/projects/{id}/regenerate-secret`` → the new project secret.
|
||||||
|
|
||||||
@ -1007,8 +989,9 @@ def print_credential_summary(emit: Any = print) -> None:
|
|||||||
else "✗ missing (run `hermes photon setup`)"
|
else "✗ missing (run `hermes photon setup`)"
|
||||||
)
|
)
|
||||||
sid, sec = load_project_credentials()
|
sid, sec = load_project_credentials()
|
||||||
labels["spectrum_project_id"] = sid if sid else "✗ missing"
|
# Dashboard id and Spectrum id are the same value now (ids unified), so
|
||||||
labels["dashboard_project_id"] = load_dashboard_project_id() or "—"
|
# there's a single project id to show.
|
||||||
|
labels["project_id"] = sid if sid else "✗ missing"
|
||||||
labels["project_key"] = "✓ stored" if sec else "✗ missing"
|
labels["project_key"] = "✓ stored" if sec else "✗ missing"
|
||||||
phone, assigned = load_user_numbers()
|
phone, assigned = load_user_numbers()
|
||||||
labels["phone_number"] = (
|
labels["phone_number"] = (
|
||||||
@ -1022,8 +1005,7 @@ def print_credential_summary(emit: Any = print) -> None:
|
|||||||
"Photon iMessage status",
|
"Photon iMessage status",
|
||||||
"──────────────────────",
|
"──────────────────────",
|
||||||
" device token : " + labels["device_token"],
|
" device token : " + labels["device_token"],
|
||||||
" dashboard project : " + labels["dashboard_project_id"],
|
" project id : " + labels["project_id"],
|
||||||
" spectrum project id : " + labels["spectrum_project_id"],
|
|
||||||
" project secret : " + labels["project_key"],
|
" project secret : " + labels["project_key"],
|
||||||
" my number : " + labels["phone_number"],
|
" my number : " + labels["phone_number"],
|
||||||
" assigned number : " + labels["assigned_phone_number"],
|
" assigned number : " + labels["assigned_phone_number"],
|
||||||
@ -1039,7 +1021,7 @@ def credential_summary() -> Dict[str, str]:
|
|||||||
else "✗ missing (run `hermes photon setup`)"
|
else "✗ missing (run `hermes photon setup`)"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _present_spectrum_id() -> str:
|
def _present_project_id() -> str:
|
||||||
sid, _sec = load_project_credentials()
|
sid, _sec = load_project_credentials()
|
||||||
return sid or "✗ missing"
|
return sid or "✗ missing"
|
||||||
|
|
||||||
@ -1057,8 +1039,7 @@ def credential_summary() -> Dict[str, str]:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"device_token": _present_token(),
|
"device_token": _present_token(),
|
||||||
"dashboard_project_id": load_dashboard_project_id() or "—",
|
"project_id": _present_project_id(),
|
||||||
"spectrum_project_id": _present_spectrum_id(),
|
|
||||||
"project_key": _present_secret(),
|
"project_key": _present_secret(),
|
||||||
"phone_number": _present_phone(),
|
"phone_number": _present_phone(),
|
||||||
"assigned_phone_number": _present_assigned_phone(),
|
"assigned_phone_number": _present_assigned_phone(),
|
||||||
|
|||||||
@ -164,16 +164,14 @@ def _cmd_setup(args: argparse.Namespace) -> int:
|
|||||||
print("could not resolve a Photon project id", file=sys.stderr)
|
print("could not resolve a Photon project id", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# 3. Enable Spectrum, fetch the spectrum project id, rotate the secret,
|
# 3. Rotate the project secret and persist creds (runtime -> ~/.hermes/.env,
|
||||||
# and persist both (runtime creds -> ~/.hermes/.env, ids -> auth.json).
|
# ids -> auth.json). Spectrum is always enabled and provisioned at
|
||||||
|
# create-time, and the dashboard project id *is* the Spectrum project id
|
||||||
|
# (ids unified), so there's nothing to enable — the id we already have is
|
||||||
|
# the Spectrum id.
|
||||||
try:
|
try:
|
||||||
print("[3/5] Enabling Spectrum and provisioning credentials...")
|
print("[3/5] Provisioning Spectrum credentials...")
|
||||||
proj = photon_auth.ensure_spectrum_enabled(token, dashboard_id)
|
spectrum_id = dashboard_id
|
||||||
spectrum_id = proj.get("spectrumProjectId")
|
|
||||||
if not spectrum_id:
|
|
||||||
print("spectrum provisioning failed: no spectrum project id", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
spectrum_id = str(spectrum_id)
|
|
||||||
secret = photon_auth.regenerate_project_secret(token, dashboard_id)
|
secret = photon_auth.regenerate_project_secret(token, dashboard_id)
|
||||||
photon_auth.store_project_credentials(
|
photon_auth.store_project_credentials(
|
||||||
spectrum_project_id=spectrum_id,
|
spectrum_project_id=spectrum_id,
|
||||||
@ -182,7 +180,7 @@ def _cmd_setup(args: argparse.Namespace) -> int:
|
|||||||
name=name,
|
name=name,
|
||||||
)
|
)
|
||||||
# spectrum_id is an opaque non-secret id; safe to show.
|
# spectrum_id is an opaque non-secret id; safe to show.
|
||||||
print(f" ✓ Spectrum enabled (project id {spectrum_id}) — secret saved")
|
print(f" ✓ Spectrum ready (project id {spectrum_id}) — secret saved")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"spectrum provisioning failed: {e}", file=sys.stderr)
|
print(f"spectrum provisioning failed: {e}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@ -88,7 +88,10 @@ def test_store_project_credentials_round_trip(
|
|||||||
sid, secret = photon_auth.load_project_credentials()
|
sid, secret = photon_auth.load_project_credentials()
|
||||||
assert sid == "sp-123"
|
assert sid == "sp-123"
|
||||||
assert secret == "secret-key"
|
assert secret == "secret-key"
|
||||||
assert photon_auth.load_dashboard_project_id() == "dash-456"
|
# Post-unification the management id resolves to the Spectrum id, not the
|
||||||
|
# stored dashboard id — so a pre-backfill diverged install (whose old
|
||||||
|
# dashboard id was rewritten and now 404s) still reaches the live row.
|
||||||
|
assert photon_auth.load_dashboard_project_id() == "sp-123"
|
||||||
|
|
||||||
|
|
||||||
def test_store_project_credentials_writes_env(tmp_hermes_home: Path) -> None:
|
def test_store_project_credentials_writes_env(tmp_hermes_home: Path) -> None:
|
||||||
@ -284,7 +287,7 @@ def test_find_project_by_name_case_insensitive(monkeypatch: pytest.MonkeyPatch)
|
|||||||
assert proj is not None and proj["id"] == "p2"
|
assert proj is not None and proj["id"] == "p2"
|
||||||
|
|
||||||
|
|
||||||
def test_create_project_sends_spectrum_true(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_create_project_omits_spectrum_flag(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
captured: Dict[str, Any] = {}
|
captured: Dict[str, Any] = {}
|
||||||
|
|
||||||
def fake_post(url: str, **kwargs: Any) -> _FakeResponse:
|
def fake_post(url: str, **kwargs: Any) -> _FakeResponse:
|
||||||
@ -296,7 +299,9 @@ def test_create_project_sends_spectrum_true(monkeypatch: pytest.MonkeyPatch) ->
|
|||||||
monkeypatch.setattr(photon_auth.httpx, "post", fake_post)
|
monkeypatch.setattr(photon_auth.httpx, "post", fake_post)
|
||||||
data = photon_auth.create_project("tok", name="Hermes Agent")
|
data = photon_auth.create_project("tok", name="Hermes Agent")
|
||||||
assert data["id"] == "new-proj"
|
assert data["id"] == "new-proj"
|
||||||
assert captured["body"]["spectrum"] is True
|
# Spectrum is always provisioned at create-time; the field was dropped
|
||||||
|
# from the API schema, so we must not send it.
|
||||||
|
assert "spectrum" not in captured["body"]
|
||||||
assert captured["body"]["name"] == "Hermes Agent"
|
assert captured["body"]["name"] == "Hermes Agent"
|
||||||
assert captured["headers"]["Authorization"] == "Bearer tok"
|
assert captured["headers"]["Authorization"] == "Bearer tok"
|
||||||
assert captured["url"].endswith("/api/projects")
|
assert captured["url"].endswith("/api/projects")
|
||||||
@ -311,46 +316,6 @@ def test_create_project_raises_without_id(monkeypatch: pytest.MonkeyPatch) -> No
|
|||||||
photon_auth.create_project("tok")
|
photon_auth.create_project("tok")
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_spectrum_enabled_toggles_when_off(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
get_calls = {"n": 0}
|
|
||||||
posted = {"toggle": False}
|
|
||||||
|
|
||||||
def fake_get(url: str, **kwargs: Any) -> _FakeResponse:
|
|
||||||
get_calls["n"] += 1
|
|
||||||
if get_calls["n"] == 1:
|
|
||||||
return _FakeResponse(json_body={"id": "p", "spectrum": False, "spectrumProjectId": None})
|
|
||||||
return _FakeResponse(json_body={"id": "p", "spectrum": True, "spectrumProjectId": "sp-1"})
|
|
||||||
|
|
||||||
def fake_post(url: str, **kwargs: Any) -> _FakeResponse:
|
|
||||||
if url.endswith("/spectrum/toggle"):
|
|
||||||
posted["toggle"] = True
|
|
||||||
return _FakeResponse(json_body={"success": True})
|
|
||||||
|
|
||||||
monkeypatch.setattr(photon_auth.httpx, "get", fake_get)
|
|
||||||
monkeypatch.setattr(photon_auth.httpx, "post", fake_post)
|
|
||||||
proj = photon_auth.ensure_spectrum_enabled("tok", "p")
|
|
||||||
assert posted["toggle"] is True
|
|
||||||
assert proj["spectrumProjectId"] == "sp-1"
|
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_spectrum_enabled_skips_toggle_when_on(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
posted = {"toggle": False}
|
|
||||||
|
|
||||||
def fake_get(url: str, **kwargs: Any) -> _FakeResponse:
|
|
||||||
return _FakeResponse(json_body={"id": "p", "spectrum": True, "spectrumProjectId": "sp-1"})
|
|
||||||
|
|
||||||
def fake_post(url: str, **kwargs: Any) -> _FakeResponse:
|
|
||||||
if url.endswith("/spectrum/toggle"):
|
|
||||||
posted["toggle"] = True
|
|
||||||
return _FakeResponse(json_body={"success": True})
|
|
||||||
|
|
||||||
monkeypatch.setattr(photon_auth.httpx, "get", fake_get)
|
|
||||||
monkeypatch.setattr(photon_auth.httpx, "post", fake_post)
|
|
||||||
proj = photon_auth.ensure_spectrum_enabled("tok", "p")
|
|
||||||
assert posted["toggle"] is False
|
|
||||||
assert proj["spectrumProjectId"] == "sp-1"
|
|
||||||
|
|
||||||
|
|
||||||
def test_regenerate_project_secret(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_regenerate_project_secret(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
def fake_post(url: str, **kwargs: Any) -> _FakeResponse:
|
def fake_post(url: str, **kwargs: Any) -> _FakeResponse:
|
||||||
assert url.endswith("/regenerate-secret")
|
assert url.endswith("/regenerate-secret")
|
||||||
@ -498,8 +463,8 @@ def test_credential_summary_no_secret_leak(
|
|||||||
assert "secret-bbbb" not in blob
|
assert "secret-bbbb" not in blob
|
||||||
assert summary["device_token"].startswith("✓")
|
assert summary["device_token"].startswith("✓")
|
||||||
assert summary["project_key"].startswith("✓")
|
assert summary["project_key"].startswith("✓")
|
||||||
assert summary["spectrum_project_id"] == "sp-uuid"
|
# Unified id: dashboard id == Spectrum id, surfaced as one project id.
|
||||||
assert summary["dashboard_project_id"] == "dash-uuid"
|
assert summary["project_id"] == "sp-uuid"
|
||||||
assert summary["phone_number"].startswith("✗ missing")
|
assert summary["phone_number"].startswith("✗ missing")
|
||||||
assert summary["assigned_phone_number"].startswith("✗ missing")
|
assert summary["assigned_phone_number"].startswith("✗ missing")
|
||||||
|
|
||||||
|
|||||||
@ -73,12 +73,10 @@ def test_env_enablement_home_channel_defaults_name(monkeypatch: pytest.MonkeyPat
|
|||||||
|
|
||||||
def test_setup_hint_uses_gateway_service_command(monkeypatch: pytest.MonkeyPatch, capsys) -> None:
|
def test_setup_hint_uses_gateway_service_command(monkeypatch: pytest.MonkeyPatch, capsys) -> None:
|
||||||
monkeypatch.setattr(cli.photon_auth, "load_photon_token", lambda: "token")
|
monkeypatch.setattr(cli.photon_auth, "load_photon_token", lambda: "token")
|
||||||
|
# The dashboard id *is* the Spectrum project id (ids unified), so setup no
|
||||||
|
# longer enables Spectrum or fetches a separate spectrumProjectId — it
|
||||||
|
# reuses this id directly.
|
||||||
monkeypatch.setattr(cli.photon_auth, "load_dashboard_project_id", lambda: "dashboard")
|
monkeypatch.setattr(cli.photon_auth, "load_dashboard_project_id", lambda: "dashboard")
|
||||||
monkeypatch.setattr(
|
|
||||||
cli.photon_auth,
|
|
||||||
"ensure_spectrum_enabled",
|
|
||||||
lambda token, dashboard_id: {"spectrumProjectId": "project_123"},
|
|
||||||
)
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
cli.photon_auth,
|
cli.photon_auth,
|
||||||
"regenerate_project_secret",
|
"regenerate_project_secret",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user