From 34f48e58a1c89ca4a900e9ac790a26dd900e2180 Mon Sep 17 00:00:00 2001 From: nendo Date: Wed, 4 Feb 2026 11:10:40 +0900 Subject: [PATCH] feat(devices): default to returning existing device on duplicate registration Change allow_existing default to True so duplicate fingerprint matches return the existing device (200) instead of 409 Conflict. Add model validator to force allow_existing=False when allow_duplicate is set. --- backend/endpoints/device.py | 10 +++++-- backend/tests/endpoints/test_device.py | 39 ++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/backend/endpoints/device.py b/backend/endpoints/device.py index 6780c9293..8911dfbf4 100644 --- a/backend/endpoints/device.py +++ b/backend/endpoints/device.py @@ -2,7 +2,7 @@ import uuid from datetime import datetime, timezone from fastapi import HTTPException, Request, Response, status -from pydantic import BaseModel +from pydantic import BaseModel, model_validator from decorators.auth import protected_route from endpoints.responses.device import DeviceCreateResponse, DeviceSchema @@ -26,10 +26,16 @@ class DeviceCreatePayload(BaseModel): ip_address: str | None = None mac_address: str | None = None hostname: str | None = None - allow_existing: bool = False + allow_existing: bool = True allow_duplicate: bool = False reset_syncs: bool = False + @model_validator(mode="after") + def _duplicate_disables_existing(self) -> "DeviceCreatePayload": + if self.allow_duplicate: + self.allow_existing = False + return self + class DeviceUpdatePayload(BaseModel): name: str | None = None diff --git a/backend/tests/endpoints/test_device.py b/backend/tests/endpoints/test_device.py index e8cffa3d1..9cc8de583 100644 --- a/backend/tests/endpoints/test_device.py +++ b/backend/tests/endpoints/test_device.py @@ -282,7 +282,7 @@ class TestDeviceUserIsolation: class TestDeviceDuplicateHandling: - def test_duplicate_mac_address_returns_409( + def test_duplicate_mac_address_returns_existing( self, client, access_token: str, admin_user: User ): db_device_handler.add_device( @@ -303,12 +303,12 @@ class TestDeviceDuplicateHandling: headers={"Authorization": f"Bearer {access_token}"}, ) - assert response.status_code == status.HTTP_409_CONFLICT - data = response.json()["detail"] - assert data["error"] == "device_exists" + assert response.status_code == status.HTTP_200_OK + data = response.json() assert data["device_id"] == "existing-mac-device" + assert data["name"] == "Existing Device" - def test_duplicate_hostname_platform_returns_409( + def test_duplicate_hostname_platform_returns_existing( self, client, access_token: str, admin_user: User ): db_device_handler.add_device( @@ -331,10 +331,37 @@ class TestDeviceDuplicateHandling: headers={"Authorization": f"Bearer {access_token}"}, ) + assert response.status_code == status.HTTP_200_OK + data = response.json() + assert data["device_id"] == "existing-hostname-device" + assert data["name"] == "Existing Device" + + def test_duplicate_with_allow_existing_false_returns_409( + self, client, access_token: str, admin_user: User + ): + db_device_handler.add_device( + Device( + id="reject-duplicate-device", + user_id=admin_user.id, + name="Existing Device", + mac_address="FF:EE:DD:CC:BB:AA", + ) + ) + + response = client.post( + "/api/devices", + json={ + "name": "New Device", + "mac_address": "FF:EE:DD:CC:BB:AA", + "allow_existing": False, + }, + headers={"Authorization": f"Bearer {access_token}"}, + ) + assert response.status_code == status.HTTP_409_CONFLICT data = response.json()["detail"] assert data["error"] == "device_exists" - assert data["device_id"] == "existing-hostname-device" + assert data["device_id"] == "reject-duplicate-device" def test_allow_existing_returns_existing_device( self, client, access_token: str, admin_user: User