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
15 changes: 8 additions & 7 deletions src/nwp500/mqtt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,20 +150,21 @@ def __init__(
- None: Auto-detect from device (default)

Raises:
MqttCredentialsError: If auth client is not authenticated, tokens
are stale/expired, or AWS credentials are not available
MqttCredentialsError: If auth client is not authenticated or AWS
credentials are not available
"""
if not auth_client.is_authenticated:
raise MqttCredentialsError(
"Authentication client must be authenticated before "
"creating MQTT client. Call auth_client.sign_in() first."
)

if not auth_client.has_valid_tokens:
raise MqttCredentialsError(
"Tokens are stale/expired. "
"Call ensure_valid_token() or re_authenticate() first."
)
# Token validity is checked in connect() which also refreshes stale
# tokens automatically. This allows creating MQTT clients with
# restored tokens that may have expired between sessions. Token
# validation and refresh are deferred until connect() is called; if
# connect() is never called, tokens are not revalidated/refreshed
# and no MQTT connection is established.

if not auth_client.current_tokens:
raise MqttCredentialsError("No tokens available from auth client")
Expand Down
78 changes: 41 additions & 37 deletions tests/test_mqtt_client_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,44 +108,40 @@ def test_mqtt_client_init_rejects_not_authenticated(

assert "must be authenticated" in str(exc_info.value).lower()

def test_mqtt_client_init_rejects_expired_jwt(
def test_mqtt_client_init_accepts_expired_jwt(
self, auth_client_with_expired_jwt
):
"""Test MQTT client rejects auth client with expired JWT tokens."""
with pytest.raises(MqttCredentialsError) as exc_info:
NavienMqttClient(auth_client_with_expired_jwt)
"""Test MQTT client can be created with expired JWT tokens.

error_msg = str(exc_info.value).lower()
assert "stale/expired" in error_msg
assert (
"ensure_valid_token" in error_msg or "re_authenticate" in error_msg
)
Token refresh happens in connect(), not in __init__().
"""
# Should not raise - token refresh happens during connect()
mqtt_client = NavienMqttClient(auth_client_with_expired_jwt)
assert mqtt_client is not None
assert not mqtt_client.is_connected

def test_mqtt_client_init_rejects_expired_aws_credentials(
def test_mqtt_client_init_accepts_expired_aws_credentials(
self, auth_client_with_expired_aws_credentials
):
"""Test MQTT client rejects auth client with expired AWS credentials."""
with pytest.raises(MqttCredentialsError) as exc_info:
NavienMqttClient(auth_client_with_expired_aws_credentials)
"""Test MQTT client can be created with expired AWS credentials.

error_msg = str(exc_info.value).lower()
assert "stale/expired" in error_msg
assert (
"ensure_valid_token" in error_msg or "re_authenticate" in error_msg
)
Token refresh happens in connect(), not in __init__().
"""
# Should not raise - token refresh happens during connect()
mqtt_client = NavienMqttClient(auth_client_with_expired_aws_credentials)
assert mqtt_client is not None
assert not mqtt_client.is_connected

def test_mqtt_client_init_error_message_guidance(
def test_mqtt_client_init_accepts_expired_tokens_without_error(
self, auth_client_with_expired_jwt
):
"""Test MQTT client init error provides clear guidance on recovery."""
with pytest.raises(MqttCredentialsError) as exc_info:
NavienMqttClient(auth_client_with_expired_jwt)
"""Test MQTT client accepts expired tokens without error.

error_msg = str(exc_info.value)
# Should mention recovery methods
assert (
"ensure_valid_token" in error_msg or "re_authenticate" in error_msg
), f"Error message should mention recovery methods: {error_msg}"
Token validation moved to connect() which handles refresh automatically.
"""
# Should not raise any error about stale/expired tokens
mqtt_client = NavienMqttClient(auth_client_with_expired_jwt)
assert mqtt_client is not None


class TestHasValidTokensProperty:
Expand Down Expand Up @@ -195,12 +191,16 @@ def test_has_valid_tokens_true_with_no_aws_expiration(self):
# Should be True: JWT valid and AWS credentials have no expiration
assert auth_client.has_valid_tokens is True

def test_has_valid_tokens_integration_with_mqtt_init(
def test_mqtt_client_creation_succeeds_with_valid_tokens(
self, auth_client_with_valid_tokens
):
"""Test that has_valid_tokens integrates correctly with MQTT init."""
# When has_valid_tokens is True, MQTT init should succeed
assert auth_client_with_valid_tokens.has_valid_tokens is True
"""Test that MQTT client creation succeeds with valid tokens.

Verifies MQTT client can be created successfully when auth_client has
valid tokens. Token validation is deferred to connect().
"""
# MQTT init should succeed regardless of token validity
# Token validation is deferred to connect()
mqtt_client = NavienMqttClient(auth_client_with_valid_tokens)
assert mqtt_client is not None

Expand Down Expand Up @@ -475,7 +475,11 @@ class TestTokenValidationEdgeCases:
"""Test edge cases in token validation."""

def test_expired_jwt_near_expiry_buffer(self):
"""Test token considered expired within 5-minute buffer."""
"""Test token considered expired within 5-minute buffer.

MQTT init no longer rejects expired tokens since validation
happens in connect().
"""
auth_client = NavienAuthClient("test@example.com", "password")
# Token expires in 3 minutes - should be considered expired
near_expiry = datetime.now() - timedelta(seconds=3420)
Expand All @@ -497,11 +501,11 @@ def test_expired_jwt_near_expiry_buffer(self):
assert tokens.is_expired is True
assert auth_client.has_valid_tokens is False

# MQTT init should reject it
with pytest.raises(MqttCredentialsError) as exc_info:
NavienMqttClient(auth_client)

assert "stale/expired" in str(exc_info.value).lower()
# MQTT init should NOT reject it - token validation moved to
# connect()
mqtt_client = NavienMqttClient(auth_client)
assert mqtt_client is not None
assert not mqtt_client.is_connected

def test_multiple_validation_checks_mqtt_init(
self, auth_client_with_valid_tokens
Expand Down