mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-10-30 19:52:03 +01:00
Fixed last seen issue
This commit is contained in:
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})")
|
||||
@@ -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
|
||||
Reference in new issue
Block a user