"""Unit tests for jwt_utils (V2 sid/jti claims).""" from __future__ import annotations from datetime import datetime, timedelta, timezone import jwt import pytest from agentkit.server.auth.jwt_utils import ( ACCESS_TOKEN_TTL, JWT_ALGORITHM, REFRESH_TOKEN_TTL, REFRESH_TOKEN_TTL_REMEMBER_ME, create_token_pair, verify_token, ) SECRET = "test-secret-with-at-least-32-bytes-1234" def test_create_token_pair_has_required_v1_claims(): pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET ) for token in (pair.access_token, pair.refresh_token): payload = jwt.decode(token, SECRET, algorithms=[JWT_ALGORITHM]) assert payload["sub"] == "u-1" assert payload["username"] == "alice" assert payload["role"] == "member" assert payload["iat"] and payload["exp"] def test_v1_pair_has_no_sid_or_jti(): pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET ) access = jwt.decode(pair.access_token, SECRET, algorithms=[JWT_ALGORITHM]) refresh = jwt.decode(pair.refresh_token, SECRET, algorithms=[JWT_ALGORITHM]) assert "sid" not in access assert "jti" not in access assert "sid" not in refresh def test_v2_pair_includes_sid_on_both_tokens_and_jti_on_access(): pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET, session_id="sess-abc", ) access = jwt.decode(pair.access_token, SECRET, algorithms=[JWT_ALGORITHM]) refresh = jwt.decode(pair.refresh_token, SECRET, algorithms=[JWT_ALGORITHM]) assert access["sid"] == "sess-abc" assert refresh["sid"] == "sess-abc" assert access["jti"] and isinstance(access["jti"], str) # Refresh intentionally has no jti — rotation uses the token hash. assert "jti" not in refresh def test_refresh_token_type_is_refresh(): pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET ) refresh = jwt.decode(pair.refresh_token, SECRET, algorithms=[JWT_ALGORITHM]) assert refresh["type"] == "refresh" def test_access_token_type_is_access(): pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET ) access = jwt.decode(pair.access_token, SECRET, algorithms=[JWT_ALGORITHM]) assert access["type"] == "access" def test_default_refresh_ttl_is_7_days(): now = datetime(2026, 6, 20, 0, 0, 0, tzinfo=timezone.utc) pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET, now=now, ) assert (pair.access_expires_at - now) == ACCESS_TOKEN_TTL assert (pair.refresh_expires_at - now) == REFRESH_TOKEN_TTL def test_remember_me_extends_refresh_ttl_to_30_days(): now = datetime(2026, 6, 20, 0, 0, 0, tzinfo=timezone.utc) pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET, now=now, remember_me=True, ) assert (pair.refresh_expires_at - now) == REFRESH_TOKEN_TTL_REMEMBER_ME def test_verify_token_accepts_access_and_refresh_by_default(): pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET ) a = verify_token(pair.access_token, SECRET) r = verify_token(pair.refresh_token, SECRET) assert a["type"] == "access" assert r["type"] == "refresh" def test_verify_token_expected_type_filters_other_type(): pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET ) with pytest.raises(jwt.InvalidTokenError): verify_token(pair.access_token, SECRET, expected_type="refresh") with pytest.raises(jwt.InvalidTokenError): verify_token(pair.refresh_token, SECRET, expected_type="access") def test_verify_token_rejects_expired(): # Use a "now" far in the past so the access token is definitely expired. past = datetime.now(timezone.utc) - timedelta(hours=1) pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET, now=past ) with pytest.raises(jwt.ExpiredSignatureError): verify_token(pair.access_token, SECRET) def test_verify_token_rejects_wrong_secret(): pair = create_token_pair( user_id="u-1", username="alice", role="member", secret=SECRET ) with pytest.raises(jwt.InvalidTokenError): verify_token(pair.access_token, "other-secret") def test_create_token_pair_rejects_empty_secret(): with pytest.raises(ValueError): create_token_pair(user_id="u-1", username="alice", role="member", secret="")