Fixed last seen issue

This commit is contained in:
Louis King committed 2025-12-08 00:15:25 +00:00
1 parent 89ceee8741
commit 65b8418af4
4 files changed
+219 -6

No files matched your search

@@ -0,0 +1,39 @@
"""Make Node.last_seen nullable
Revision ID: 0b944542ccd8
Revises: 005
Create Date: 2025-12-08 00:07:49.891245+00:00
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "0b944542ccd8"
down_revision: Union[str, None] = "005"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# Make Node.last_seen nullable since nodes from contact sync
# haven't actually been "seen" on the mesh yet
with op.batch_alter_table("nodes", schema=None) as batch_op:
batch_op.alter_column("last_seen", existing_type=sa.DATETIME(), nullable=True)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# Revert Node.last_seen to non-nullable
# Note: This will fail if there are NULL values in last_seen
with op.batch_alter_table("nodes", schema=None) as batch_op:
batch_op.alter_column("last_seen", existing_type=sa.DATETIME(), nullable=False)
# ### end Alembic commands ###
@@ -73,15 +73,17 @@ def handle_contact(
node.name = name
if node_type and not node.adv_type:
node.adv_type = node_type
node.last_seen = now
# Do NOT update last_seen for contact sync - only advertisement events
# should update last_seen since that's when the node was actually seen
else:
# Create new node
# Create new node from contact database
# Set last_seen=None since we haven't actually seen this node advertise yet
node = Node(
public_key=contact_key,
name=name,
adv_type=node_type,
first_seen=now,
last_seen=now,
last_seen=None, # Will be set when we receive an advertisement
)
session.add(node)
logger.info(f"Created node from contact: {contact_key[:12]}... ({name})")
+3 -3
View File
@@ -52,10 +52,10 @@ class Node(Base, UUIDMixin, TimestampMixin):
default=utc_now,
nullable=False,
)
last_seen: Mapped[datetime] = mapped_column(
last_seen: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True),
default=utc_now,
nullable=False,
default=None,
nullable=True,
)
# Relationships
@@ -0,0 +1,172 @@
"""Tests for contact handler."""
import pytest
from contextlib import contextmanager
from datetime import datetime, timezone
from unittest.mock import MagicMock
from meshcore_hub.collector.handlers.contacts import handle_contact
from meshcore_hub.common.models import Node
@pytest.fixture
def mock_db_manager(db_session):
"""Create a mock database manager that uses the test session."""
mock_db = MagicMock()
@contextmanager
def session_scope():
try:
yield db_session
db_session.commit()
except Exception:
db_session.rollback()
raise
mock_db.session_scope = session_scope
return mock_db
def test_handle_contact_creates_new_node(db_session, mock_db_manager):
"""Test that contact handler creates new node with last_seen=None."""
payload = {
"public_key": "a" * 64,
"adv_name": "TestNode",
"type": 1, # chat
}
handle_contact("receiver123", "contact", payload, mock_db_manager)
# Verify node was created
node = db_session.query(Node).filter_by(public_key="a" * 64).first()
assert node is not None
assert node.name == "TestNode"
assert node.adv_type == "chat"
assert node.first_seen is not None
assert node.last_seen is None # Should NOT be set by contact sync
def test_handle_contact_updates_existing_node_name(db_session, mock_db_manager):
"""Test that contact handler updates name but NOT last_seen."""
# Create existing node with a last_seen timestamp
last_seen_time = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
node = Node(
public_key="b" * 64,
name="OldName",
adv_type="chat",
first_seen=datetime.now(timezone.utc),
last_seen=last_seen_time,
)
db_session.add(node)
db_session.commit()
# Process contact with new name
payload = {
"public_key": "b" * 64,
"adv_name": "NewName",
"type": 1,
}
handle_contact("receiver123", "contact", payload, mock_db_manager)
# Verify name was updated but last_seen was NOT
db_session.expire_all()
node = db_session.query(Node).filter_by(public_key="b" * 64).first()
assert node.name == "NewName"
# Compare timestamps without timezone (SQLite strips timezone info)
assert node.last_seen is not None
assert node.last_seen.replace(tzinfo=None) == last_seen_time.replace(tzinfo=None)
def test_handle_contact_preserves_existing_adv_type(db_session, mock_db_manager):
"""Test that contact handler doesn't overwrite existing adv_type."""
# Create existing node with adv_type
node = Node(
public_key="c" * 64,
name="TestNode",
adv_type="repeater",
first_seen=datetime.now(timezone.utc),
last_seen=None,
)
db_session.add(node)
db_session.commit()
# Process contact with different type
payload = {
"public_key": "c" * 64,
"adv_name": "TestNode",
"type": 1, # chat
}
handle_contact("receiver123", "contact", payload, mock_db_manager)
# Verify adv_type was NOT changed
db_session.expire_all()
node = db_session.query(Node).filter_by(public_key="c" * 64).first()
assert node.adv_type == "repeater" # Should preserve existing
def test_handle_contact_sets_adv_type_if_missing(db_session, mock_db_manager):
"""Test that contact handler sets adv_type if node doesn't have one."""
# Create existing node without adv_type
node = Node(
public_key="d" * 64,
name="TestNode",
adv_type=None,
first_seen=datetime.now(timezone.utc),
last_seen=None,
)
db_session.add(node)
db_session.commit()
# Process contact with type
payload = {
"public_key": "d" * 64,
"adv_name": "TestNode",
"type": 2, # repeater
}
handle_contact("receiver123", "contact", payload, mock_db_manager)
# Verify adv_type was set
db_session.expire_all()
node = db_session.query(Node).filter_by(public_key="d" * 64).first()
assert node.adv_type == "repeater"
def test_handle_contact_ignores_missing_public_key(db_session, mock_db_manager, caplog):
"""Test that contact handler handles missing public_key gracefully."""
payload = {
"adv_name": "TestNode",
"type": 1,
}
handle_contact("receiver123", "contact", payload, mock_db_manager)
# Verify warning was logged and no node created
assert "missing public_key" in caplog.text
count = db_session.query(Node).count()
assert count == 0
def test_handle_contact_node_type_mapping(db_session, mock_db_manager):
"""Test that numeric node types are correctly mapped to strings."""
test_cases = [
(0, "none"),
(1, "chat"),
(2, "repeater"),
(3, "room"),
]
for numeric_type, expected_string in test_cases:
public_key = str(numeric_type) * 64
payload = {
"public_key": public_key,
"adv_name": f"Node{numeric_type}",
"type": numeric_type,
}
handle_contact("receiver123", "contact", payload, mock_db_manager)
node = db_session.query(Node).filter_by(public_key=public_key).first()
assert node.adv_type == expected_string