""" update_device must keep users.device_serials in sync when a PUT changes the device's user_list (the MQTT ACL reads device_serials). Firestore is faked. """ import pytest from google.cloud.firestore_v1 import ArrayRemove, ArrayUnion from devices import service as devices_service from devices.models import DeviceUpdate from mqtt import app_users SERIAL = "BSVSPR-26C13X-STD01R-X7KQA" class _Snap: def __init__(self, ref, data): self.reference = ref self.id = ref.id self._data = data self.exists = data is not None def to_dict(self): return dict(self._data) if self._data is not None else None class _Ref: def __init__(self, db, collection, doc_id): self._db, self._col, self.id = db, collection, doc_id def get(self): return _Snap(self, self._db.docs[self._col].get(self.id)) def update(self, data): self._db.apply(self._col, self.id, data) class _Batch: def __init__(self, db): self._db, self.ops = db, [] def update(self, ref, data): self.ops.append((ref, data)) def commit(self): for ref, data in self.ops: ref.update(data) self._db.commits += 1 class _Collection: def __init__(self, db, name): self._db, self._name = db, name def document(self, doc_id): return _Ref(self._db, self._name, doc_id) class FakeDB: def __init__(self): self.docs = {"devices": {}, "users": {}} self.commits = 0 def collection(self, name): return _Collection(self, name) def batch(self): return _Batch(self) def apply(self, col, doc_id, data): doc = self.docs[col][doc_id] for key, val in data.items(): if isinstance(val, ArrayUnion): doc[key] = list(doc.get(key) or []) + [v for v in val.values if v not in (doc.get(key) or [])] elif isinstance(val, ArrayRemove): doc[key] = [v for v in (doc.get(key) or []) if v not in val.values] else: doc[key] = val @pytest.fixture def db(monkeypatch): fake = FakeDB() fake.docs["devices"]["dev1"] = { "serial_number": SERIAL, "mfg_status": "claimed", "user_list": ["users/alice", "users/bob"], } fake.docs["users"]["alice"] = {"uid": "alice", "device_serials": [SERIAL]} fake.docs["users"]["bob"] = {"uid": "bob", "device_serials": [SERIAL, "OTHER"]} fake.docs["users"]["carol"] = {"uid": "carol"} monkeypatch.setattr(devices_service, "get_db", lambda: fake) return fake def test_put_user_list_syncs_added_and_removed_users(db, monkeypatch): invalidated = [] monkeypatch.setattr(app_users, "invalidate", invalidated.append) devices_service.update_device("dev1", DeviceUpdate(user_list=["users/alice", "users/carol"])) assert db.docs["devices"]["dev1"]["user_list"] == ["users/alice", "users/carol"] assert db.docs["users"]["alice"]["device_serials"] == [SERIAL] # unchanged assert db.docs["users"]["bob"]["device_serials"] == ["OTHER"] # removed assert db.docs["users"]["carol"]["device_serials"] == [SERIAL] # added assert sorted(invalidated) == ["bob", "carol"] assert db.commits == 1 # one atomic batch def test_put_user_list_ignores_dangling_user_refs(db): devices_service.update_device("dev1", DeviceUpdate(user_list=["users/alice", "users/bob", "users/ghost"])) assert db.docs["devices"]["dev1"]["user_list"] == ["users/alice", "users/bob", "users/ghost"] assert "ghost" not in db.docs["users"] def test_put_without_user_list_does_not_touch_users(db): devices_service.update_device("dev1", DeviceUpdate(device_name="Church")) assert db.docs["devices"]["dev1"]["device_name"] == "Church" assert db.commits == 0 assert db.docs["users"]["bob"]["device_serials"] == [SERIAL, "OTHER"]