Files

145 lines
5.1 KiB
Python
Raw Permalink Normal View History

"""Tests for the same-canvas node dedupe repair (app.services.node_dedupe).
Identity is the device link now: two nodes are duplicates when they draw the
same `device_inventory` row on the same design. The device facts live on that
row, so nothing has to be merged out of the extras before they go — only edges
and parent links are re-pointed.
"""
import pytest
from sqlalchemy import select
from app.db.models import Design, Edge, InventoryDevice, Node
from app.services.node_dedupe import dedupe_nodes_by_device
async def _design(db, name="d1"):
d = Design(name=name)
db.add(d)
await db.flush()
return d
async def _device(db, ieee="0xAAA", **kwargs):
device = InventoryDevice(ieee_address=ieee, status="approved", **kwargs)
db.add(device)
await db.flush()
return device
@pytest.mark.asyncio
async def test_collapses_same_device_same_design(db_session):
d = await _design(db_session)
device = await _device(db_session)
keep = Node(
label="Sensor", type="zigbee_enddevice", design_id=d.id,
device_id=device.id, pos_x=100, pos_y=200,
)
db_session.add(keep)
await db_session.flush()
dup = Node(label="Sensor", type="zigbee_enddevice", design_id=d.id, device_id=device.id)
db_session.add(dup)
await db_session.flush()
removed = await dedupe_nodes_by_device(db_session)
assert removed == 1
nodes = (
await db_session.execute(select(Node).where(Node.device_id == device.id))
).scalars().all()
assert len(nodes) == 1
survivor = nodes[0]
assert survivor.id == keep.id # oldest kept
assert survivor.pos_x == 100 # canvas position preserved
# The device facts were never on the node, so none can be lost here.
assert await db_session.get(InventoryDevice, device.id) is not None
@pytest.mark.asyncio
async def test_preserves_same_device_across_designs(db_session):
"""Same device on two canvases is valid — must NOT be merged."""
d1 = await _design(db_session, "d1")
d2 = await _design(db_session, "d2")
device = await _device(db_session, ieee="0xBBB")
for d in (d1, d2):
db_session.add(Node(label="S", type="zigbee_enddevice", design_id=d.id, device_id=device.id))
await db_session.flush()
removed = await dedupe_nodes_by_device(db_session)
assert removed == 0
nodes = (
await db_session.execute(select(Node).where(Node.device_id == device.id))
).scalars().all()
assert len(nodes) == 2
@pytest.mark.asyncio
async def test_leaves_furniture_alone(db_session):
"""Zones and text carry no device link, so they are never duplicates."""
d = await _design(db_session)
for _ in range(2):
db_session.add(Node(label="Zone", type="groupRect", design_id=d.id))
await db_session.flush()
assert await dedupe_nodes_by_device(db_session) == 0
@pytest.mark.asyncio
async def test_repoints_edges_and_drops_dupes(db_session):
d = await _design(db_session)
device = await _device(db_session, ieee="0xCCC")
keep = Node(label="A", type="server", design_id=d.id, device_id=device.id)
other = Node(label="B", type="server", design_id=d.id)
db_session.add_all([keep, other])
await db_session.flush()
dup = Node(label="A", type="server", design_id=d.id, device_id=device.id)
db_session.add(dup)
await db_session.flush()
# keep<->other and dup<->other (parallel after repoint), plus dup<->keep (self-loop).
db_session.add_all([
Edge(source=keep.id, target=other.id, type="ethernet", design_id=d.id),
Edge(source=dup.id, target=other.id, type="ethernet", design_id=d.id),
Edge(source=dup.id, target=keep.id, type="ethernet", design_id=d.id),
])
await db_session.flush()
removed = await dedupe_nodes_by_device(db_session)
assert removed == 1
edges = (await db_session.execute(select(Edge))).scalars().all()
# self-loop dropped, parallel edge collapsed -> a single keep<->other edge
assert len(edges) == 1
e = edges[0]
assert {e.source, e.target} == {keep.id, other.id}
@pytest.mark.asyncio
async def test_repoints_child_parent(db_session):
d = await _design(db_session)
device = await _device(db_session, ieee="0xDDD")
keep = Node(label="Host", type="proxmox", design_id=d.id, device_id=device.id)
db_session.add(keep)
await db_session.flush()
dup = Node(label="Host", type="proxmox", design_id=d.id, device_id=device.id)
db_session.add(dup)
await db_session.flush()
child = Node(label="VM", type="vm", design_id=d.id, parent_id=dup.id)
db_session.add(child)
await db_session.flush()
await dedupe_nodes_by_device(db_session)
await db_session.refresh(child)
assert child.parent_id == keep.id
@pytest.mark.asyncio
async def test_idempotent_noop_when_unique(db_session):
d = await _design(db_session)
device = await _device(db_session, ieee="0xEEE")
db_session.add(Node(label="X", type="server", design_id=d.id, device_id=device.id))
await db_session.flush()
assert await dedupe_nodes_by_device(db_session) == 0
assert await dedupe_nodes_by_device(db_session) == 0