Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions AWS_DYNAMO_CONFIG_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
> **Date**: 2026-02-06
> **Scope**: `zebra_day` — shared printer config + ZPL templates via DynamoDB, with S3 backups

Zebra's Cognito integration now uses `daylily-auth-cognito` 2.0 for browser/session auth, runtime verification, and `daycog`-driven lifecycle management. Keep service runtime code out of `daylily_auth_cognito.cli`.

---

## 1. Problem Statement
Expand Down Expand Up @@ -598,7 +600,7 @@ aws = [
"boto3>=1.26.0",
]
auth = [
"daylily-cognito>=0.1.10",
"daylily-auth-cognito>=0.1.10",
"python-jose[cryptography]>=3.3.0",
"boto3>=1.26.0",
]
Expand Down Expand Up @@ -915,4 +917,4 @@ Key design decisions:
- **Application-side S3 backups** — no Lambda, no Streams, no extra infrastructure.
- **Optimistic locking** — safe concurrent access without distributed locks.
- **Fail-loud offline behavior** — no silent degradation.
- **moto-based testing** — full test coverage without AWS credentials.
- **moto-based testing** — full test coverage without AWS credentials.
4 changes: 3 additions & 1 deletion docs/major_refactor.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Implementation handoff for the approved `zebra_day` major release.
This release turns `zebra_day` into a deployment-scoped, TapDB-backed, first-level
service and library.

For Cognito, Zebra now relies on the `daylily-auth-cognito` 2.0 split: browser sessions use `browser.session`, Hosted UI helpers use `browser.oauth` and `browser.google`, bearer verification uses `runtime.verifier`, and shared Cognito lifecycle stays in `daycog` via `admin.*`. Service runtime code should not import `daylily_auth_cognito.cli`.

- Runtime activation moves to Atlas-style `source ./activate`.
- Local flat files stop being the runtime source of truth for printer fleet state.
- TapDB becomes the primary authority for printers, label profiles, templates, and
Expand Down Expand Up @@ -156,7 +158,7 @@ Deliverables:
Ownership:
- coordinated PRs in `dayhoff`
- coordinated PRs in `bloom`
- any required touchpoints in `daylily-cognito` or `daylily-tapdb`
- any required touchpoints in `daylily-auth-cognito` or `daylily-tapdb`

Deliverables:
- Dayhoff:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ dependencies = [
"pydantic>=2.0.0",
"python-multipart>=0.0.6",
"cli-core-yo>=1.3.1",
"daylily-cognito==1.2.0",
"daylily-auth-cognito==2.0.1",
"daylily-tapdb==4.1.1",
"typer>=0.21.0,<0.22.0",
"rich>=14.0.0,<15.0.0",
Expand Down
81 changes: 47 additions & 34 deletions tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import sys
from types import SimpleNamespace
from unittest.mock import Mock
from unittest.mock import AsyncMock, Mock

import pytest

Expand Down Expand Up @@ -55,20 +55,14 @@ class TestSetupCognitoAuth:
"""Tests for setup_cognito_auth function."""

def test_setup_cognito_raises_import_error_when_unavailable(self):
"""Test setup_cognito_auth raises ImportError when daylily-cognito not installed."""
"""Test setup_cognito_auth raises ImportError when daylily-auth-cognito not installed."""
if not auth.is_cognito_available():
with pytest.raises(ImportError) as exc_info:
auth.setup_cognito_auth(None, None)
assert "daylily-cognito" in str(exc_info.value)
assert "daylily-auth-cognito" in str(exc_info.value)


def test_exchange_code_verifies_access_token_and_profiles_from_id_token():
oauth = SimpleNamespace(
exchange_authorization_code=lambda **kwargs: {
"access_token": "access-token",
"id_token": "id-token",
}
)
auth_client = SimpleNamespace(
verify_token=lambda token: (
{"sub": "access-sub", "username": "atlas-user"}
Expand All @@ -86,10 +80,19 @@ def test_exchange_code_verifies_access_token_and_profiles_from_id_token():
user_pool_id="pool-id",
),
auth=auth_client,
oauth=oauth,
jwks=jwks,
)
binding.redirect_uri = lambda request: "https://localhost:8118/auth/callback"
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr(
"daylily_auth_cognito.browser.session.exchange_authorization_code_async",
AsyncMock(
return_value={
"access_token": "access-token",
"id_token": "id-token",
}
),
)

def _verify_id_token(token, *, access_token=None):
assert access_token == "access-token"
Expand All @@ -104,19 +107,16 @@ def _verify_id_token(token, *, access_token=None):

binding._verify_id_token = _verify_id_token

result = binding.exchange_code(object(), "auth-code")
try:
result = binding.exchange_code(object(), "auth-code")
finally:
monkeypatch.undo()

assert result["claims"]["sub"] == "access-sub"
assert result["profile_claims"]["email"] == "user@example.com"


def test_exchange_code_falls_back_to_unverified_id_token_profile_decode():
oauth = SimpleNamespace(
exchange_authorization_code=lambda **kwargs: {
"access_token": "access-token",
"id_token": "id-token",
}
)
auth_client = SimpleNamespace(
verify_token=lambda token: (
{"sub": "access-sub", "username": "atlas-user"}
Expand All @@ -133,10 +133,19 @@ def test_exchange_code_falls_back_to_unverified_id_token_profile_decode():
user_pool_id="pool-id",
),
auth=auth_client,
oauth=oauth,
jwks=SimpleNamespace(),
)
binding.redirect_uri = lambda request: "https://localhost:8118/auth/callback"
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr(
"daylily_auth_cognito.browser.session.exchange_authorization_code_async",
AsyncMock(
return_value={
"access_token": "access-token",
"id_token": "id-token",
}
),
)

def _verify_id_token(token, *, access_token=None):
assert access_token == "access-token"
Expand All @@ -152,19 +161,16 @@ def _verify_id_token(token, *, access_token=None):
else pytest.fail("expected id token fallback decode")
)

result = binding.exchange_code(object(), "auth-code")
try:
result = binding.exchange_code(object(), "auth-code")
finally:
monkeypatch.undo()

assert result["claims"]["sub"] == "access-sub"
assert result["profile_claims"]["email"] == "fallback@example.com"


def test_exchange_code_continues_when_id_token_cannot_be_decoded():
oauth = SimpleNamespace(
exchange_authorization_code=lambda **kwargs: {
"access_token": "access-token",
"id_token": "id-token",
}
)
auth_client = SimpleNamespace(
verify_token=lambda token: (
{"sub": "access-sub", "username": "atlas-user"}
Expand All @@ -181,10 +187,19 @@ def test_exchange_code_continues_when_id_token_cannot_be_decoded():
user_pool_id="pool-id",
),
auth=auth_client,
oauth=oauth,
jwks=SimpleNamespace(),
)
binding.redirect_uri = lambda request: "https://localhost:8118/auth/callback"
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr(
"daylily_auth_cognito.browser.session.exchange_authorization_code_async",
AsyncMock(
return_value={
"access_token": "access-token",
"id_token": "id-token",
}
),
)

def _verify_id_token(token, *, access_token=None):
assert access_token == "access-token"
Expand All @@ -195,7 +210,10 @@ def _verify_id_token(token, *, access_token=None):
ValueError("payload failed")
)

result = binding.exchange_code(object(), "auth-code")
try:
result = binding.exchange_code(object(), "auth-code")
finally:
monkeypatch.undo()

assert result["claims"]["sub"] == "access-sub"
assert result["profile_claims"] == {}
Expand Down Expand Up @@ -255,7 +273,6 @@ def test_redirect_and_logout_uris_prefer_daycog_contract_urls():
logout_url="https://0.0.0.0:8118/login",
),
auth=SimpleNamespace(),
oauth=SimpleNamespace(),
jwks=SimpleNamespace(),
)

Expand All @@ -280,7 +297,6 @@ def _fake_build_logout_url(**kwargs: str) -> str:
logout_url="https://0.0.0.0:8118/login",
),
auth=SimpleNamespace(),
oauth=SimpleNamespace(),
jwks=SimpleNamespace(),
)

Expand All @@ -289,8 +305,7 @@ def _fake_build_logout_url(**kwargs: str) -> str:
assert url == "https://example.com/logout"
assert captured["domain"] == "example.com"
assert captured["client_id"] == "client-id"
assert captured["redirect_uri"] == "https://localhost:8118/auth/callback"
assert "logout_uri" not in captured
assert captured["logout_uri"] == "https://localhost:8118/login"


def test_load_daycog_contract_prefers_process_env_and_normalizes_domain(monkeypatch):
Expand Down Expand Up @@ -372,8 +387,7 @@ def fake_decode(
region="us-west-2",
user_pool_id="pool-id",
),
auth=SimpleNamespace(_jwks_cache=jwks_cache),
oauth=SimpleNamespace(),
auth=SimpleNamespace(cache=jwks_cache),
jwks=SimpleNamespace(
JWKSCache=lambda region, pool_id: pytest.fail("unexpected JWKS cache init")
),
Expand All @@ -391,7 +405,6 @@ def test_decode_id_token_unverified_disables_at_hash_verification(monkeypatch):
settings=SimpleNamespace(),
config=SimpleNamespace(),
auth=SimpleNamespace(),
oauth=SimpleNamespace(),
jwks=SimpleNamespace(),
)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_deploy_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_activate_uses_root_environment_yaml_and_repo_only_editable_install() ->
assert 'pip install -e "${ZEBRA_DAY_PROJECT_ROOT}"' in activate
assert "[dev,lint,auth]" not in activate
assert "../daylily-tapdb" not in activate
assert "../daylily-cognito" not in activate
assert "../daylily-auth-cognito" not in activate
assert "../cli-core-yo" not in activate


Expand Down
15 changes: 9 additions & 6 deletions tests/test_modern_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dataclasses import replace
from types import SimpleNamespace
from unittest.mock import AsyncMock
from urllib.parse import parse_qs, urlparse

from fastapi.testclient import TestClient
Expand Down Expand Up @@ -87,11 +88,13 @@ def _make_cognito_app(
lambda app, settings: _make_cognito_binding(claims, profile_claims),
)
monkeypatch.setattr(
"daylily_cognito.web_session.exchange_authorization_code",
lambda **kwargs: {
"access_token": "access-token",
"id_token": "id-token",
},
"daylily_auth_cognito.browser.session.exchange_authorization_code_async",
AsyncMock(
return_value={
"access_token": "access-token",
"id_token": "id-token",
}
),
)
monkeypatch.setattr("zebra_day.web.app.get_local_ip", lambda: "192.168.1.10")
return create_app(auth="cognito", client=_seed_client(tmp_path, monkeypatch))
Expand Down Expand Up @@ -370,7 +373,7 @@ def test_cognito_session_expired_after_restart_redirects_to_error(tmp_path, monk
server_instance_id="new-server-instance",
)
app.state.web_session_config = new_config
app.state.__dict__["_daylily_cognito_web_session_config"] = new_config
app.state.__dict__["_daylily_auth_cognito_web_session_config"] = new_config
response = test_client.get("/printers", follow_redirects=False)
assert response.status_code == 302
assert response.headers["location"] == "/auth/error?reason=session_expired"
Expand Down
13 changes: 8 additions & 5 deletions tests/test_observability_contract.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import AsyncMock
from urllib.parse import parse_qs, urlparse

from fastapi.testclient import TestClient
Expand Down Expand Up @@ -85,11 +86,13 @@ def _make_cognito_app(
lambda app, settings: _make_cognito_binding(claims, profile_claims),
)
monkeypatch.setattr(
"daylily_cognito.web_session.exchange_authorization_code",
lambda **kwargs: {
"access_token": "access-token",
"id_token": "id-token",
},
"daylily_auth_cognito.browser.session.exchange_authorization_code_async",
AsyncMock(
return_value={
"access_token": "access-token",
"id_token": "id-token",
}
),
)
monkeypatch.setattr("zebra_day.web.app.get_local_ip", lambda: "192.168.1.10")
return create_app(auth="cognito", client=_seed_client(tmp_path, monkeypatch))
Expand Down
2 changes: 1 addition & 1 deletion zebra_day/observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ def obs_services_payload(self, *, auth_mode: str) -> dict[str, Any]:
rich_auth = "none" if auth_mode == "none" else "bearer_token"
configured_dependencies = ["daylily-tapdb"]
if auth_mode == "cognito":
configured_dependencies.insert(0, "daylily-cognito")
configured_dependencies.insert(0, "daylily-auth-cognito")
endpoints = [
{"path": "/healthz", "auth": "none", "kind": "liveness"},
{"path": "/readyz", "auth": "none", "kind": "readiness"},
Expand Down
Loading
Loading