diff --git a/tests/unit/frontends/test_webrtc.py b/tests/unit/frontends/test_webrtc.py index b77b6486..c147bd36 100644 --- a/tests/unit/frontends/test_webrtc.py +++ b/tests/unit/frontends/test_webrtc.py @@ -28,7 +28,13 @@ from pytest_twisted import ensureDeferred as ed from libervia.backend.core import exceptions from libervia.backend.tools.common import data_format -from libervia.frontends.tools import webrtc as webrtc_mod +try: + from libervia.frontends.tools import webrtc as webrtc_mod +except exceptions.MissingModule as e: + pytest.skip( + "Can't import webrtc due to missing modules: {e}", + allow_module_level=True + ) @pytest.fixture diff --git a/tests/unit/test_plugin_misc_pubsub_contacts.py b/tests/unit/test_plugin_misc_pubsub_contacts.py new file mode 100644 index 00000000..3d378f3d --- /dev/null +++ b/tests/unit/test_plugin_misc_pubsub_contacts.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python3 + +# Tests for Libervia's Pubsub Contacts plugin +# Copyright (C) 2009-2026 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from twisted.words.xish import domish +from wokkel.pubsub import NS_PUBSUB + +from libervia.backend.plugins.plugin_misc_pubsub_contacts import ( + Contact, + Contacts, + ContactsData, + Group, + Groups, + NS_CONTACTS, +) + + +class TestGroup: + """Tests for Group model.""" + + def test_from_element(self): + """Group is correctly parsed from element.""" + element = domish.Element((NS_CONTACTS, "group")) + element["id"] = "group123" + element["name"] = "My Group" + desc_elt = element.addElement((NS_CONTACTS, "description")) + desc_elt.addContent("A test group") + + group = Group.from_element(element) + + assert group.id == "group123" + assert group.name == "My Group" + assert group.description == "A test group" + + def test_from_element_nested_in_parent(self): + """Group is correctly parsed from element's parent.""" + parent = domish.Element((None, "parent")) + group_elt = parent.addElement((NS_CONTACTS, "group")) + group_elt["id"] = "group123" + group_elt["name"] = "Test Group" + + group = Group.from_element(parent) + + assert group.id == "group123" + assert group.name == "Test Group" + + def test_from_element_set_encrypted(self): + """ "encrypted" flag is correctly set when present in metadata.""" + element = domish.Element((NS_CONTACTS, "group")) + element["id"] = "group123" + metadata = {"encrypted": {"group123": True}} + + group = Group.from_element(element, metadata) + + assert group.encrypted is True + + def test_from_element_invalid_type_raises_error(self): + """ValueError is raised when element is not a group element.""" + element = domish.Element((NS_CONTACTS, "contact")) + + try: + Group.from_element(element) + assert False, "Expected ValueError" + except ValueError as e: + assert "Invalid element" in str(e) + + def test_to_element(self): + """Group element is correctly created with all fields.""" + group = Group( + id="group123", name="My Group", description="A test group", encrypted=True + ) + element = group.to_element() + + assert element.uri == NS_CONTACTS + assert element.name == "group" + assert element["id"] == "group123" + assert element["name"] == "My Group" + + desc_elements = list(element.elements(NS_CONTACTS, "description")) + assert len(desc_elements) == 1 + assert str(desc_elements[0]) == "A test group" + + def test_to_element_missing_name_raises_error(self): + """ValueError is raised when name is missing and id_only is False.""" + group = Group(id="group123") + + try: + group.to_element(id_only=False) + assert False, "Expected ValueError" + except ValueError as e: + assert "name must be set" in str(e) + + +class TestGroups: + """Tests for Groups model.""" + + def test_append_and_iterate(self): + """Groups can be appended and iterated.""" + groups = Groups([]) + groups.append(Group(id="g1", name="Group 1")) + groups.append(Group(id="g2", name="Group 2")) + + group_list = list(groups) + assert len(group_list) == 2 + assert group_list[0].id == "g1" + assert group_list[1].id == "g2" + + def test_len_and_remove(self): + """Groups len and remove work correctly.""" + groups = Groups([]) + groups.append(Group(id="g1", name="Group 1")) + groups.append(Group(id="g2", name="Group 2")) + + assert len(groups) == 2 + groups.remove(groups[0]) + assert len(groups) == 1 + assert groups[0].id == "g2" + + def test_extend(self): + """Groups extend works correctly.""" + groups = Groups([]) + groups.extend([Group(id="g1"), Group(id="g2")]) + + assert len(groups) == 2 + + def test_clear(self): + """Groups clear works correctly.""" + groups = Groups([Group(id="g1"), Group(id="g2")]) + groups.clear() + + assert len(groups) == 0 + + def test_sort(self): + """Groups sort works correctly.""" + groups = Groups([Group(id="g2"), Group(id="g1")]) + groups.sort(key=lambda g: g.id) + + assert groups[0].id == "g1" + assert groups[1].id == "g2" + + def test_getitem(self): + """Groups __getitem__ works correctly.""" + groups = Groups([Group(id="g1"), Group(id="g2")]) + assert groups[0].id == "g1" + assert groups[1].id == "g2" + + +class TestContact: + """Tests for Contact model.""" + + def test_from_element_with_jid_id(self): + """Contact is correctly parsed from element with jid identity.""" + contact_elt = domish.Element((NS_CONTACTS, "contact")) + contact_elt["name"] = "Test Name" + identity_elt = contact_elt.addElement((NS_CONTACTS, "identity")) + identity_elt["type"] = "jid" + identity_elt.addContent("user@example.com") + + contact = Contact.from_element(contact_elt, ContactsData()) + + assert contact.identities == {"jid": "user@example.com"} + assert contact.name == "Test Name" + assert len(contact.groups) == 0 + + def test_from_element_with_group(self): + """Contact is correctly parsed with group reference.""" + parent = domish.Element((NS_PUBSUB, "items")) + item_elt = parent.addElement((NS_PUBSUB, "item")) + item_elt["id"] = "contact123" + contact_elt = item_elt.addElement((NS_CONTACTS, "contact")) + identity_elt = contact_elt.addElement((NS_CONTACTS, "identity")) + identity_elt["type"] = "jid" + identity_elt.addContent("user@example.com") + group_elt = contact_elt.addElement((NS_CONTACTS, "group")) + group_elt["id"] = "g1" + + contacts_data = ContactsData() + contacts_data.groups = Groups([Group(id="g1", name="Friends")]) + + contact = Contact.from_element(contact_elt, contacts_data) + + assert contact.id == "contact123" + assert len(contact.groups) == 1 + assert contact.groups[0].id == "g1" + assert contact.groups[0].name == "Friends" + + def test_from_element_with_ancestor_item_id(self): + """Contact id is extracted from ancestor item element.""" + parent = domish.Element((NS_PUBSUB, "items")) + item_elt = parent.addElement((NS_PUBSUB, "item")) + item_elt["id"] = "contact123" + contact_elt = item_elt.addElement((NS_CONTACTS, "contact")) + identity_elt = contact_elt.addElement((NS_CONTACTS, "identity")) + identity_elt["type"] = "jid" + identity_elt.addContent("user@example.com") + + contact = Contact.from_element(contact_elt, ContactsData()) + + assert contact.id == "contact123" + + def test_from_element_with_ancestor_item_encrypted(self): + """Contact encrypted flag is set from item metadata.""" + parent = domish.Element((NS_PUBSUB, "items")) + item_elt = parent.addElement((NS_PUBSUB, "item")) + item_elt["id"] = "contact123" + item_elt["encrypted"] = "true" + contact_elt = item_elt.addElement((NS_CONTACTS, "contact")) + identity_elt = contact_elt.addElement((NS_CONTACTS, "identity")) + identity_elt["type"] = "jid" + identity_elt.addContent("user@example.com") + + metadata = {"encrypted": {"contact123": True}} + contact = Contact.from_element(contact_elt, ContactsData(), metadata) + + assert contact.encrypted is True + + def test_to_element(self): + """Contact element is correctly created.""" + contact = Contact( + id="contact123", + name="Test Name", + identities={"jid": "user@example.com"}, + groups=Groups([Group(id="g1", name="Friends")]), + ) + element = contact.to_element() + + assert element.uri == NS_CONTACTS + assert element.name == "contact" + assert element["name"] == "Test Name" + + identity_elements = list(element.elements(NS_CONTACTS, "identity")) + assert len(identity_elements) == 1 + assert identity_elements[0]["type"] == "jid" + assert str(identity_elements[0]) == "user@example.com" + + group_elements = list(element.elements(NS_CONTACTS, "group")) + assert len(group_elements) == 1 + group_element = group_elements[0] + assert group_element["id"] == "g1" + assert not group_element.hasAttribute( + "name" + ), "name attribute should not be present in group with id_only" + +class TestContacts: + """Tests for Contacts root model.""" + + def test_append_and_iterate(self): + """Contacts can be appended and iterated.""" + contacts = Contacts([]) + contacts.append(Contact(identities={"jid": "user1@example.com"}, id="c1")) + contacts.append(Contact(identities={"jid": "user2@example.com"}, id="c2")) + + contact_list = list(contacts) + assert len(contact_list) == 2 + assert contact_list[0].id == "c1" + assert contact_list[1].id == "c2" + + def test_len_and_remove(self): + """Contacts len and remove work correctly.""" + contacts = Contacts([]) + contacts.append(Contact(identities={"jid": "user1@example.com"}, id="c1")) + contacts.append(Contact(identities={"jid": "user2@example.com"}, id="c2")) + + assert len(contacts) == 2 + contacts.remove(contacts[0]) + assert len(contacts) == 1 + assert contacts[0].id == "c2" + + def test_extend(self): + """Contacts extend works correctly.""" + contacts = Contacts([]) + contacts.extend( + [ + Contact(identities={"jid": "user1@example.com"}), + Contact(identities={"jid": "user2@example.com"}), + ] + ) + + assert len(contacts) == 2 + + def test_clear(self): + """Contacts clear works correctly.""" + contacts = Contacts( + [ + Contact(identities={"jid": "user1@example.com"}), + Contact(identities={"jid": "user2@example.com"}), + ] + ) + contacts.clear() + + assert len(contacts) == 0 + + def test_sort(self): + """Contacts sort works correctly.""" + contacts = Contacts( + [ + Contact(identities={"jid": "user2@example.com"}, id="c2"), + Contact(identities={"jid": "user1@example.com"}, id="c1"), + ] + ) + contacts.sort(key=lambda c: c.id) + + assert contacts[0].id == "c1" + assert contacts[1].id == "c2" + + def test_getitem(self): + """Contacts __getitem__ works correctly.""" + contacts = Contacts( + [ + Contact(identities={"jid": "user1@example.com"}, id="c1"), + Contact(identities={"jid": "user2@example.com"}, id="c2"), + ] + ) + assert contacts[0].id == "c1" + assert contacts[1].id == "c2" + + +class TestContactsData: + """Tests for ContactsData class.""" + + def test_jid_to_contacts_empty(self): + """jid_to_contacts returns empty dict when contacts is empty.""" + data = ContactsData() + assert data.jid_to_contacts == {} + + def test_jid_to_contacts(self): + """jid_to_contacts correctly maps JIDs to contacts.""" + contact1 = Contact( + id="c1", + identities={"jid": "user1@example.com"}, + ) + contact2 = Contact( + id="c2", + identities={"jid": "user2@example.com"}, + ) + data = ContactsData(contacts=Contacts([contact1, contact2])) + + jid_map = data.jid_to_contacts + + assert len(jid_map) == 2 + assert jid_map["user1@example.com"].id == "c1" + assert jid_map["user2@example.com"].id == "c2" + + def test_jid_to_contacts_skips_contact_without_id(self): + """jid_to_contacts skips contacts without id.""" + contact1 = Contact( + id="c1", + identities={"jid": "user1@example.com"}, + ) + contact2 = Contact( + id="c2", + identities={"jid": "user2@example.com"}, + ) + contact_no_id = Contact( + id=None, + identities={"jid": "user3@example.com"}, + ) + data = ContactsData(contacts=Contacts([contact1, contact2, contact_no_id])) + + jid_map = data.jid_to_contacts + + assert len(jid_map) == 2 + assert "user1@example.com" in jid_map + assert "user2@example.com" in jid_map + assert "user3@example.com" not in jid_map + + def test_ids_to_groups(self): + """ids_to_groups correctly maps group IDs to groups.""" + group1 = Group(id="g1", name="Group 1") + group2 = Group(id="g2", name="Group 2") + data = ContactsData(groups=Groups([group1, group2])) + + id_map = data.ids_to_groups + + assert len(id_map) == 2 + assert id_map["g1"].name == "Group 1" + assert id_map["g2"].name == "Group 2" + + def test_ids_to_groups_skips_missing_id(self): + """ids_to_groups skips groups without ID.""" + group = Group(id="g1", name="Group 1") + group_no_id = Group(name="No ID Group") + data = ContactsData(groups=Groups([group, group_no_id])) + + id_map = data.ids_to_groups + + assert len(id_map) == 1 + assert "g1" in id_map + + def test_reset(self): + """Reset clears cached mappings.""" + contact = Contact( + id="c1", + identities={"jid": "user1@example.com"}, + ) + group = Group(id="g1", name="Group 1") + data = ContactsData(contacts=Contacts([contact]), groups=Groups([group])) + + # Access the mappings to populate caches, and check that they are not empty. + assert len(data.jid_to_contacts) == 1 + assert len(data.ids_to_groups) == 1 + + data.reset() + + assert data._jid_to_contacts is None + assert data._ids_to_groups is None