diff --git a/examples/README.md b/examples/README.md index e543b0cb8bf7cd8ade5dffd7387c95c9ad1585be..bd6669ac1e032856c29da5a14fdbf4badc94165d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -57,9 +57,25 @@ The key server will handle key management and HTTP signatures automatically. 1. Ensure both ngrok and the key server are running -2. Run the example: +2. Run the examples: ```bash -python examples/send_to_mastodon.py +python examples/send_message.py +``` +or +```bash +python examples/follow.py +``` +or +```bash +python examples/like.py +``` +or +```bash +python examples/announce.py +``` +or +```bash +python examples/block.py ``` 3. Check the logs for the federation process: diff --git a/examples/announce_mastodon.py b/examples/announce.py similarity index 100% rename from examples/announce_mastodon.py rename to examples/announce.py diff --git a/examples/block_mastodon.py b/examples/block.py similarity index 100% rename from examples/block_mastodon.py rename to examples/block.py diff --git a/examples/follow_mastodon.py b/examples/follow.py similarity index 100% rename from examples/follow_mastodon.py rename to examples/follow.py diff --git a/examples/like_mastodon.py b/examples/like.py similarity index 100% rename from examples/like_mastodon.py rename to examples/like.py diff --git a/examples/send_to_mastodon.py b/examples/send_to_mastodon.py deleted file mode 100644 index eb4f00493e234823d2734a457ca09b46a9303564..0000000000000000000000000000000000000000 --- a/examples/send_to_mastodon.py +++ /dev/null @@ -1,148 +0,0 @@ -""" -Example of sending a message to Mastodon following ActivityPub spec. -""" - -import asyncio -import logging -from pathlib import Path -import sys -from config import CONFIG -from datetime import datetime, timezone - -# Add src directory to Python path -src_path = Path(__file__).parent.parent / "src" -sys.path.insert(0, str(src_path)) - -from pyfed.models import APPerson, APNote, APCreate -from pyfed.federation.delivery import ActivityDelivery -from pyfed.federation.discovery import InstanceDiscovery -from pyfed.security.key_management import KeyManager - -logging.basicConfig(level=logging.DEBUG) # Set to DEBUG for more detailed logs -logger = logging.getLogger(__name__) - - # @gvelez17@mas.to -async def send_activity_to_mastodon(): - # Initialize components with config - key_manager = KeyManager( - domain=CONFIG["domain"], - keys_path=CONFIG["keys_path"], - rotation_config=False - ) - await key_manager.initialize() - - # Force generate a new key with proper format - # await key_manager.rotate_keys() - - active_key = await key_manager.get_active_key() - logger.debug(f"Using active key ID: {active_key.key_id}") - logger.debug(f"Key document URL: https://{CONFIG['domain']}/keys/{active_key.key_id.split('/')[-1]}") - - discovery = InstanceDiscovery() - await discovery.initialize() - - delivery = ActivityDelivery(key_manager=key_manager, discovery=discovery) - - try: - # 1. First, perform WebFinger lookup to get the actor's URL - logger.info("Performing WebFinger lookup...") - webfinger_result = await discovery.webfinger( - resource="acct:kene29@mastodon.social" - ) - logger.info(f"WebFinger result: {webfinger_result}") - - if not webfinger_result: - raise Exception("Could not find user through WebFinger") - - # Find ActivityPub actor URL from WebFinger result - actor_url = None - for link in webfinger_result.get('links', []): - if link.get('rel') == 'self' and link.get('type') == 'application/activity+json': - actor_url = link.get('href') - break - - if not actor_url: - raise Exception("Could not find ActivityPub actor URL") - - # 2. Fetch the actor's profile to get their inbox URL - logger.info(f"Fetching actor profile from {actor_url}") - async with discovery.session.get(actor_url) as response: - if response.status != 200: - raise Exception(f"Failed to fetch actor profile: {response.status}") - actor_data = await response.json() - - # Get the inbox URL from the actor's profile - inbox_url = actor_data.get('inbox') - if not inbox_url: - raise Exception("Could not find actor's inbox URL") - - logger.info(f"Found actor's inbox: {inbox_url}") - - # 3. Create the Activity with ngrok domain - note_id = f"https://{CONFIG['domain']}/notes/{int(asyncio.get_event_loop().time() * 1000)}" - - # Create actor - actor = APPerson( - id=f"https://{CONFIG['domain']}/users/{CONFIG['user']}", - name="Alice", - preferred_username="alice", - inbox="https://example.com/users/alice/inbox", - outbox="https://example.com/users/alice/outbox", - followers="https://example.com/users/alice/followers" - ) - - # Create note with string attributed_to - note = APNote( - id=note_id, - content=f"Hello @gvelez17@mas.to! This is a test note to test mention to Golda", - attributed_to=str(actor.id), # Convert URL to string - to=[inbox_url], - cc=["https://www.w3.org/ns/activitystreams#Public"], - published=datetime.utcnow().isoformat() + "Z", - url=str(note_id), - tag=[{ - "type": "Mention", - "href": actor_url, - "name": "@gvelez17@mas.to" - }] - ) - - # Create activity - create_activity = APCreate( - id=f"https://example.com/activities/{datetime.now(timezone.utc).timestamp()}", - actor=str(actor.id), # Convert URL to string - object=note, - to=note.to, - cc=note.cc, - published=datetime.utcnow().isoformat() + "Z", - ) - - # 4. Deliver the activity - logger.info("Delivering activity...") - result = await delivery.deliver_to_inbox( - activity=create_activity.serialize(), - inbox_url=inbox_url, - username=CONFIG["user"] - ) - logger.info(f"Delivery result: {result}") - - if result.success: - logger.info("Activity delivered successfully!") - logger.info(f"Successfully delivered to: {result.success}") - else: - logger.error("Activity delivery failed!") - if result.failed: - logger.error(f"Failed recipients: {result.failed}") - if result.error_message: - logger.error(f"Error: {result.error_message}") - - except Exception as e: - logger.error(f"Error: {e}") - - finally: - # Clean up - await discovery.close() - await delivery.close() - -if __name__ == "__main__": - asyncio.run(send_activity_to_mastodon()) \ No newline at end of file diff --git a/src/pyfed/federation/__init__.py b/src/pyfed/federation/__init__.py index a298d4341900269b23a1f4dbdc324b135a76b629..34316d5937267a5bccc2e3cd90e5ffcfe8957077 100644 --- a/src/pyfed/federation/__init__.py +++ b/src/pyfed/federation/__init__.py @@ -13,5 +13,5 @@ __all__ = [ 'ResourceFetcher', 'ActivityPubResolver', 'InstanceDiscovery', - 'WebFingerClient' + 'WebFingerClient', ] \ No newline at end of file diff --git a/src/pyfed/handlers/accept.py b/src/pyfed/handlers/accept.py index 2ccbc964d18cd13306cdcc98df0b94ed91508d6a..cfbca4b98cc46b58f20613c6b095dcf2a5865108 100644 --- a/src/pyfed/handlers/accept.py +++ b/src/pyfed/handlers/accept.py @@ -27,7 +27,7 @@ class AcceptHandler(ActivityHandler): if object_type not in self.ACCEPTABLE_TYPES: raise ValidationError(f"Cannot accept activity type: {object_type}") - async def handle(self, activity: Dict[str, Any]) -> None: + async def process(self, activity: Dict[str, Any]) -> None: """Handle Accept activity.""" try: await self.validate(activity) diff --git a/src/pyfed/handlers/create.py b/src/pyfed/handlers/create.py index 44178d0d06473e519cab699d9d7dae2e833e3480..a5047ffa55de4b817ab24ea4acf71ef6d88962f8 100644 --- a/src/pyfed/handlers/create.py +++ b/src/pyfed/handlers/create.py @@ -103,32 +103,32 @@ class CreateHandler(ActivityHandler): async def _check_rate_limits(self, actor_id: str) -> None: """Check rate limits for actor.""" - # Implementation here + async def _validate_content(self, obj: Dict[str, Any]) -> None: """Validate object content.""" - # Implementation here + async def _extract_mentions(self, obj: Dict[str, Any]) -> List[str]: """Extract mentions from object.""" - # Implementation here + async def _handle_mentions(self, mentions: List[str], activity: Dict[str, Any]) -> None: """Handle mentions in activity.""" - # Implementation here + async def _process_attachments(self, attachments: List[Dict[str, Any]], object_id: str) -> None: """Process media attachments.""" - # Implementation here + async def _send_notifications(self, activity: Dict[str, Any]) -> None: """Send notifications for activity.""" - # Implementation here + async def _update_collections(self, activity: Dict[str, Any]) -> None: """Update relevant collections.""" - # Implementation here \ No newline at end of file + \ No newline at end of file diff --git a/src/pyfed/handlers/delete.py b/src/pyfed/handlers/delete.py index 142a7e6ee60fdd97c84d28c7ff6fbd3d71f5495f..8066c5f1af28d4dcc5f6efd5229286a536a6f772 100644 --- a/src/pyfed/handlers/delete.py +++ b/src/pyfed/handlers/delete.py @@ -76,6 +76,5 @@ class DeleteHandler(ActivityHandler): async def _update_collections(self, object_id: str) -> None: """Update collections after deletion.""" - # Remove from collections - # Implementation here + \ No newline at end of file diff --git a/src/pyfed/handlers/follow.py b/src/pyfed/handlers/follow.py index 45e042ec059f4e27dad83039586d907f6dab7e83..114a8dda429763babdd6dce4d2a73141b27da9b7 100644 --- a/src/pyfed/handlers/follow.py +++ b/src/pyfed/handlers/follow.py @@ -23,7 +23,7 @@ class FollowHandler(ActivityHandler): if not activity.get('object'): raise ValidationError("Follow must have an object") - async def handle(self, activity: Dict[str, Any]) -> None: + async def process(self, activity: Dict[str, Any]) -> None: """Handle Follow activity.""" try: await self.validate(activity) diff --git a/src/pyfed/handlers/like.py b/src/pyfed/handlers/like.py index 4e193a13d8bfe63323462598cfc0a8724f112593..23db32a29959ae61f506afd117797808fa2b9c81 100644 --- a/src/pyfed/handlers/like.py +++ b/src/pyfed/handlers/like.py @@ -21,7 +21,7 @@ class LikeHandler(ActivityHandler): if not activity.get('object'): raise ValidationError("Like must have an object") - async def handle(self, activity: Dict[str, Any]) -> None: + async def process(self, activity: Dict[str, Any]) -> None: """Handle Like activity.""" try: await self.validate(activity) diff --git a/src/pyfed/models/actors.py b/src/pyfed/models/actors.py index 0056ff87f7e0d365aede0d9b3d3df86e90f759b7..9636d4b2184253feb606a95d7d39cf212bf0219e 100644 --- a/src/pyfed/models/actors.py +++ b/src/pyfed/models/actors.py @@ -63,74 +63,6 @@ class APActor(APObject): except ValueError: raise InvalidURLError(f"Invalid URL: {v}") - # async def send_to_inbox(self, activity: Dict[str, Any]) -> bool: - # """ - # Send an activity to this actor's inbox. - - # Args: - # activity (Dict[str, Any]): The activity to send. - - # Returns: - # bool: True if the activity was successfully delivered, False otherwise. - - # Note: This method should implement the logic described in: - # https://www.w3.org/TR/activitypub/#delivery - # """ - # logger.info(f"Sending activity to inbox: {self.inbox}") - # # Execute pre-send hook - # # plugin_manager.execute_hook('pre_send_to_inbox', self, activity) - - # # Placeholder for actual implementation - # return True - - # async def fetch_followers(self) -> List[APActor]: - # """ - # Fetch the followers of this actor. - - # Returns: - # List[APActor]: A list of actors following this actor. - # """ - # logger.info(f"Fetching followers for actor: {self.id}") - - # # Check cache first - # cached_followers = object_cache.get(f"followers:{self.id}") - # if cached_followers is not None: - # return cached_followers - - # # Execute pre-fetch hook - # # plugin_manager.execute_hook('pre_fetch_followers', self) - - # # Fetch followers (placeholder implementation) - # followers = [] # Actual implementation would go here - - # # Cache the result - # object_cache.set(f"followers:{self.id}", followers) - - # return followers - - # async def create_activity(self, activity_type: str, object: Dict[str, Any]) -> ActivityDict: - """ - Create an activity with this actor as the 'actor'. - - Args: - activity_type (str): The type of activity (e.g., "Create", "Like", "Follow"). - object (Dict[str, Any]): The object of the activity. - - Returns: - Dict[str, Any]: The created activity. - - Note: This method should create activities as described in: - Specification: https://www.w3.org/TR/activitypub/#create-activity-outbox - """ - logger.debug(f"Creating activity of type: {activity_type}") - return { - "@context": "https://www.w3.org/ns/activitystreams", - "type": activity_type, - "actor": self.id, - "object": object, - "published": datetime.now(datetime.UTC).isoformat() + "Z" - } - class APPerson(APActor): """ Represents a Person actor in ActivityPub. diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 5729d04cc2a8c2523ade1fc2d80e9f46f159ea3d..0000000000000000000000000000000000000000 --- a/tests/conftest.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Common test fixtures for PyFed unit tests. -""" -import pytest -from datetime import datetime, timezone -from pathlib import Path -import sys - -# Add src directory to Python path -src_path = Path(__file__).parent.parent / "src" -sys.path.insert(0, str(src_path)) - -from pyfed.models import APObject, APPerson, APNote - -@pytest.fixture -def valid_actor(): - """Return a valid actor URL.""" - return "https://example.com/user/1" - -@pytest.fixture -def valid_object(): - """Return a valid object dictionary.""" - return { - "id": "https://example.com/note/123", - "type": "Note", - "content": "Hello, World!" - } - -@pytest.fixture -def valid_person(): - """Return a valid APPerson object.""" - return APPerson( - id="https://example.com/users/alice", - type="Person", - name="Alice", - preferredUsername="alice", - inbox="https://example.com/users/alice/inbox", - outbox="https://example.com/users/alice/outbox" - ) - -@pytest.fixture -def valid_note(): - """Return a valid APNote object.""" - return APNote( - id="https://example.com/notes/123", - type="Note", - content="This is a test note", - published=datetime.now(timezone.utc) - ) - -@pytest.fixture -def complex_object(valid_person): - """Return a complex APObject with nested objects.""" - return APObject( - id="https://example.com/object/123", - type="Object", - name="Test Object", - content="This is a test object.", - attachment=[ - APObject(id="https://example.com/attachment/1", type="Image"), - APObject(id="https://example.com/attachment/2", type="Video") - ], - attributedTo=valid_person.id - ) diff --git a/tests/unit_tests/models/test_activities.py b/tests/unit_tests/models/test_activities.py index b870eeaafb861a59ac7a2c1105dc321ca62f6721..5f516325245806df0c0e4248ae07efb22dd8289a 100644 --- a/tests/unit_tests/models/test_activities.py +++ b/tests/unit_tests/models/test_activities.py @@ -1,166 +1,135 @@ """ -Tests for ActivityPub activity types. +test_activities.py +This module contains tests for the Activity types in ActivityPub. """ import pytest -from datetime import datetime, timezone +from datetime import datetime from pydantic import ValidationError - from pyfed.models import ( APCreate, APUpdate, APDelete, APFollow, - APUndo, APLike, APAnnounce, APActivity + APUndo, APLike, APAnnounce ) -@pytest.fixture -def valid_actor(): - return "https://example.com/user/1" - -@pytest.fixture -def valid_object(): - return {"id": "https://example.com/note/123", "type": "Note", "content": "Hello, World!"} - -@pytest.fixture -def valid_person(): - return {"id": "https://example.com/person/1", "type": "Person"} - -@pytest.fixture -def valid_note(): - return {"id": "https://example.com/note/123", "type": "Note"} - -class TestActivityCreation: - @pytest.mark.parametrize("activity_class,activity_type", [ - (APCreate, "Create"), - (APUpdate, "Update"), - (APDelete, "Delete"), - (APFollow, "Follow"), - (APUndo, "Undo"), - (APLike, "Like"), - (APAnnounce, "Announce") - ]) - def test_activity_type(self, activity_class, activity_type, valid_actor, valid_object): - """Test that each activity has the correct type.""" - activity = activity_class( - id="https://example.com/activity/123", - actor=valid_actor, - object=valid_object - ) - assert activity.type == activity_type - assert str(activity.actor) == valid_actor - - @pytest.mark.parametrize("invalid_actor", [ - None, - "", - "not-a-url", - "http:/invalid.com" - ]) - def test_invalid_actor(self, invalid_actor, valid_object): - """Test activity creation with invalid actors.""" - with pytest.raises(ValidationError): - APCreate( - id="https://example.com/activity/123", - actor=invalid_actor, - object=valid_object - ) - - @pytest.mark.parametrize("activity_class", [ - APCreate, APUpdate, APDelete, APLike, APAnnounce - ]) - def test_missing_object(self, activity_class, valid_actor): - """Test that non-intransitive activities require an object.""" - with pytest.raises(ValidationError): - activity_class( - id="https://example.com/activity/123", - actor=valid_actor - ) - -class TestActivityMetadata: - def test_activity_with_published(self, valid_actor, valid_object): - """Test activity with published timestamp.""" - now = datetime.now(timezone.utc) - activity = APCreate( - id="https://example.com/activity/123", - actor=valid_actor, - object=valid_object, - published=now - ) - assert activity.published == now - - def test_activity_with_target(self, valid_actor, valid_object): - """Test activity with a target.""" - target = {"id": "https://example.com/target/123", "type": "Collection"} - activity = APCreate( - id="https://example.com/activity/123", - actor=valid_actor, - object=valid_object, - target=target - ) - assert activity.target["id"] == target["id"] - assert activity.target["type"] == target["type"] - - def test_activity_with_result(self, valid_actor, valid_object): - """Test activity with a result.""" - result = {"id": "https://example.com/result/123", "type": "Note"} - activity = APCreate( - id="https://example.com/activity/123", - actor=valid_actor, - object=valid_object, - result=result - ) - assert activity.result["id"] == result["id"] - assert activity.result["type"] == result["type"] - -class TestSpecificActivities: - def test_create_activity(self, valid_actor, valid_object): - """Test specific behavior of Create activity.""" - activity = APCreate( - id="https://example.com/activity/123", - actor=valid_actor, - object=valid_object - ) - assert activity.type == "Create" - assert activity.object["content"] == valid_object["content"] - - def test_update_activity(self, valid_actor): - """Test specific behavior of Update activity.""" - updated_object = { +def test_create_activity(): + """Test creating a Create activity.""" + activity = APCreate( + id="https://example.com/activity/123", + actor="https://example.com/user/1", + object={ + "id": "https://example.com/note/123", + "type": "Note", + "content": "Hello, World!" + } + ) + assert activity.type == "Create" + assert str(activity.actor) == "https://example.com/user/1" + assert activity.object["type"] == "Note" + +def test_update_activity(): + """Test creating an Update activity.""" + activity = APUpdate( + id="https://example.com/activity/123", + actor="https://example.com/user/1", + object={ "id": "https://example.com/note/123", "type": "Note", "content": "Updated content" } - activity = APUpdate( - id="https://example.com/activity/123", - actor=valid_actor, - object=updated_object - ) - assert activity.type == "Update" - assert activity.object["content"] == "Updated content" - - def test_delete_activity(self, valid_actor): - """Test specific behavior of Delete activity.""" - object_id = "https://example.com/note/123" - activity = APDelete( - id="https://example.com/activity/123", - actor=valid_actor, - object=object_id - ) - assert activity.type == "Delete" - assert str(activity.object) == object_id + ) + assert activity.type == "Update" + assert activity.object["content"] == "Updated content" + +def test_delete_activity(): + """Test creating a Delete activity.""" + activity = APDelete( + id="https://example.com/activity/123", + actor="https://example.com/user/1", + object="https://example.com/note/123" + ) + assert activity.type == "Delete" + assert str(activity.object) == "https://example.com/note/123" + +def test_follow_activity(): + """Test creating a Follow activity.""" + activity = APFollow( + id="https://example.com/activity/123", + actor="https://example.com/user/1", + object="https://example.com/user/2" + ) + assert activity.type == "Follow" + assert str(activity.object) == "https://example.com/user/2" + +def test_undo_activity(): + """Test creating an Undo activity.""" + activity = APUndo( + id="https://example.com/activity/123", + actor="https://example.com/user/1", + object={ + "id": "https://example.com/activity/456", + "type": "Follow", + "actor": "https://example.com/user/1", + "object": "https://example.com/user/2" + } + ) + assert activity.type == "Undo" + assert activity.object["type"] == "Follow" + +def test_like_activity(): + """Test creating a Like activity.""" + activity = APLike( + id="https://example.com/activity/123", + actor="https://example.com/user/1", + object="https://example.com/note/123" + ) + assert activity.type == "Like" + assert str(activity.object) == "https://example.com/note/123" + +def test_announce_activity(): + """Test creating an Announce activity.""" + activity = APAnnounce( + id="https://example.com/activity/123", + actor="https://example.com/user/1", + object="https://example.com/note/123" + ) + assert activity.type == "Announce" + assert str(activity.object) == "https://example.com/note/123" + +def test_activity_with_target(): + """Test creating an activity with a target.""" + activity = APCreate( + id="https://example.com/activity/123", + actor="https://example.com/user/1", + object="https://example.com/note/123", + target="https://example.com/collection/1" + ) + assert str(activity.target) == "https://example.com/collection/1" + +def test_activity_with_result(): + """Test creating an activity with a result.""" + activity = APCreate( + id="https://example.com/activity/123", + actor="https://example.com/user/1", + object="https://example.com/note/123", + result={ + "id": "https://example.com/result/1", + "type": "Note", + "content": "Result content" + } + ) + assert activity.result["type"] == "Note" - def test_follow_activity(self, valid_actor, valid_person): - """Test specific behavior of Follow activity.""" - activity = APFollow( +def test_invalid_activity_missing_actor(): + """Test that activity creation fails when actor is missing.""" + with pytest.raises(ValidationError): + APCreate( id="https://example.com/activity/123", - actor=valid_actor, - object=valid_person.id + object="https://example.com/note/123" ) - assert activity.type == "Follow" - assert str(activity.object) == valid_person.id - def test_like_activity(self, valid_actor, valid_note): - """Test specific behavior of Like activity.""" - activity = APLike( +def test_invalid_activity_missing_object(): + """Test that activity creation fails when object is missing for non-intransitive activities.""" + with pytest.raises(ValidationError): + APCreate( id="https://example.com/activity/123", - actor=valid_actor, - object=valid_note.id + actor="https://example.com/user/1" ) - assert activity.type == "Like" - assert str(activity.object) == valid_note.id diff --git a/tests/unit_tests/serializers/test_serialization.py b/tests/unit_tests/serializers/test_serialization.py index 594d884a0965d8ff731482dc51381b0888659a40..fbeca7daed172a43d497980e0ce065b481f08ab2 100644 --- a/tests/unit_tests/serializers/test_serialization.py +++ b/tests/unit_tests/serializers/test_serialization.py @@ -1,6 +1,7 @@ """ Tests for ActivityPub serialization. """ + import pytest from datetime import datetime, timezone import json @@ -11,194 +12,176 @@ from pyfed.models import ( ) from pyfed.serializers.json_serializer import ActivityPubSerializer -class TestBasicSerialization: - @pytest.mark.parametrize("field,value", [ - ("name", "Test Object"), - ("content", "This is a test object."), - ("summary", "A summary"), - ("url", "https://example.com/object"), - ]) - def test_simple_field_serialization(self, field, value): - """Test serialization of simple field types.""" - obj = APObject( - id="https://example.com/object/123", - type="Object", - **{field: value} - ) - serialized = obj.serialize() - assert serialized[field] == value - - def test_datetime_serialization(self): - """Test serialization of datetime fields.""" - now = datetime.now(timezone.utc) - obj = APObject( - id="https://example.com/object/123", - type="Object", - published=now, - updated=now - ) - serialized = obj.serialize() - assert serialized["published"] == now.isoformat() - assert serialized["updated"] == now.isoformat() - - def test_context_handling(self): - """Test that @context is properly handled.""" - obj = APObject( - id="https://example.com/object/123", - type="Object" - ) - serialized = obj.serialize() - assert "@context" in serialized - assert serialized["@context"] == "https://www.w3.org/ns/activitystreams" +def test_serialize_ap_object(): + """Test basic object serialization.""" + obj = APObject( + id="https://example.com/object/123", + type="Object", + name="Test Object", + content="This is a test object." + ) + serialized = obj.serialize() + + # Verify serialization + assert serialized["@context"] == ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'] + assert serialized["id"] == "https://example.com/object/123" + assert serialized["type"] == "Object" + assert serialized["name"] == "Test Object" + assert serialized["content"] == "This is a test object." -class TestComplexSerialization: - def test_nested_object_serialization(self, valid_person, valid_note): - """Test serialization of objects with nested objects.""" - note = valid_note - note.attributedTo = valid_person - serialized = note.serialize() - - assert serialized["attributedTo"]["id"] == valid_person.id - assert serialized["attributedTo"]["type"] == "Person" - assert serialized["attributedTo"]["name"] == valid_person.name +def test_serialize_with_datetime(): + """Test serialization of objects with datetime fields.""" + now = datetime.now(timezone.utc) + obj = APObject( + id="https://example.com/object/123", + type="Object", + published=now, + updated=now + ) + serialized = obj.serialize() + + # Verify datetime serialization + assert serialized["published"] == now.isoformat() + assert serialized["updated"] == now.isoformat() - def test_collection_serialization(self): - """Test serialization of collections.""" - items = [ - APObject(id=f"https://example.com/item/{i}", type="Object") - for i in range(3) - ] - collection = APCollection( - id="https://example.com/collection/1", - type="Collection", - totalItems=len(items), - items=items - ) - serialized = collection.serialize() - - assert serialized["type"] == "Collection" - assert serialized["totalItems"] == 3 - assert len(serialized["items"]) == 3 - assert all(item["type"] == "Object" for item in serialized["items"]) +def test_serialize_nested_objects(): + """Test serialization of objects with nested objects.""" + author = APPerson( + id="https://example.com/users/alice", + type="Person", + name="Alice", + inbox="https://example.com/users/alice/inbox", + outbox="https://example.com/users/alice/outbox" + ) + note = APNote( + id="https://example.com/notes/123", + type="Note", + content="Hello, World!", + attributed_to=author + ) + serialized = note.serialize() + + # Verify nested object serialization + assert serialized["attributedTo"]["id"] == "https://example.com/users/alice" + assert serialized["attributedTo"]["type"] == "Person" + assert serialized["attributedTo"]["name"] == "Alice" - def test_activity_serialization(self, valid_person, valid_note): - """Test serialization of activities.""" - activity = APCreate( - id="https://example.com/activity/1", - type="Create", - actor=valid_person, - object=valid_note - ) - serialized = activity.serialize() - - assert serialized["type"] == "Create" - assert serialized["actor"]["id"] == valid_person.id - assert serialized["object"]["id"] == valid_note.id - -class TestDeserialization: - def test_basic_deserialization(self): - """Test basic object deserialization.""" - json_data = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/object/123", - "type": "Object", - "name": "Test Object" - } - obj = APObject.deserialize(json.dumps(json_data)) - assert obj.id == json_data["id"] - assert obj.type == json_data["type"] - assert obj.name == json_data["name"] +def test_serialize_collection(): + """Test serialization of collections.""" + items = [ + APNote( + id=f"https://example.com/notes/{i}", + type="Note", + content=f"Note {i}" + ).serialize() for i in range(3) + ] + collection = APCollection( + id="https://example.com/collection/1", + type="Collection", + total_items=len(items), + items=items + ) + serialized = collection.serialize() + + # Verify collection serialization + assert serialized["type"] == "Collection" + assert serialized["totalItems"] == 3 + assert len(serialized["items"]) == 3 + assert all(item["type"] == "Note" for item in serialized["items"]) - @pytest.mark.parametrize("invalid_json", [ - "", - "not json", - "{invalid json}", - "[]", - "null" - ]) - def test_invalid_json_deserialization(self, invalid_json): - """Test deserialization with invalid JSON.""" - with pytest.raises((json.JSONDecodeError, ValidationError)): - APObject.deserialize(invalid_json) +def test_serialize_activity(): + """Test serialization of activities.""" + note = APNote( + id="https://example.com/notes/123", + type="Note", + content="Hello, World!" + ).serialize() + create = APCreate( + id="https://example.com/activities/1", + type="Create", + actor="https://example.com/users/alice", + object=note + ) + serialized = create.serialize() + + # Verify activity serialization + assert serialized["type"] == "Create" + assert serialized["actor"] == "https://example.com/users/alice" + assert serialized["object"]["type"] == "Note" + assert serialized["object"]["content"] == "Hello, World!" - def test_missing_required_fields(self): - """Test deserialization with missing required fields.""" - json_data = { - "@context": "https://www.w3.org/ns/activitystreams", - "name": "Test Object" - } - with pytest.raises(ValidationError): - APObject.deserialize(json.dumps(json_data)) +def test_deserialize_ap_object(): + """Test basic object deserialization.""" + data = { + "@context": "https://www.w3.org/ns/activitystreams", + "type": "Object", + "id": "https://example.com/object/123", + "name": "Test Object", + "content": "This is a test object." + } + obj = ActivityPubSerializer.deserialize(data, APObject) + + # Verify deserialization + assert str(obj.id) == "https://example.com/object/123" + assert obj.type == "Object" + assert obj.name == "Test Object" + assert obj.content == "This is a test object." - def test_extra_fields_handling(self): - """Test deserialization with extra unknown fields.""" - json_data = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/object/123", - "type": "Object", - "name": "Test Object", - "unknownField": "value", - "anotherUnknown": 123 - } - obj = APObject.deserialize(json.dumps(json_data)) - assert obj.id == json_data["id"] - assert obj.type == json_data["type"] - assert obj.name == json_data["name"] +def test_deserialize_from_json_string(): + """Test deserialization from JSON string.""" + json_str = json.dumps({ + "type": "Object", + "id": "https://example.com/object/123", + "name": "Test Object" + }) + obj = ActivityPubSerializer.deserialize(json_str, APObject) + + # Verify deserialization from string + assert str(obj.id) == "https://example.com/object/123" + assert obj.type == "Object" + assert obj.name == "Test Object" -class TestEdgeCases: - def test_empty_collection_serialization(self): - """Test serialization of empty collections.""" - collection = APCollection( - id="https://example.com/collection/1", - type="Collection", - totalItems=0, - items=[] - ) - serialized = collection.serialize() - assert serialized["totalItems"] == 0 - assert serialized["items"] == [] +def test_deserialize_invalid_json(): + """Test deserialization of invalid JSON.""" + with pytest.raises(ValueError): + ActivityPubSerializer.deserialize("invalid json", APObject) - def test_large_collection_serialization(self): - """Test serialization of large collections.""" - items = [ - APObject(id=f"https://example.com/item/{i}", type="Object") - for i in range(1000) - ] - collection = APCollection( - id="https://example.com/collection/1", - type="Collection", - totalItems=len(items), - items=items - ) - serialized = collection.serialize() - assert len(serialized["items"]) == 1000 +def test_deserialize_missing_required_fields(): + """Test deserialization with missing required fields.""" + data = {"type": "Object", "name": "Test"} # Missing required 'id' + with pytest.raises(Exception): # Pydantic will raise validation error + ActivityPubSerializer.deserialize(data, APObject) - @pytest.mark.parametrize("long_string", [ - "a" * 1000, - "b" * 10000, - "c" * 100000 - ]) - def test_long_string_serialization(self, long_string): - """Test serialization of objects with very long string fields.""" - obj = APObject( - id="https://example.com/object/123", - type="Object", - content=long_string - ) - serialized = obj.serialize() - assert serialized["content"] == long_string +def test_serialize_deserialize_complex_object(): + """Test round-trip serialization and deserialization.""" + original = APNote( + id="https://example.com/notes/123", + type="Note", + content="Test content", + to=["https://example.com/users/bob"], + cc=["https://www.w3.org/ns/activitystreams#Public"] + ) + serialized = original.serialize() + deserialized = ActivityPubSerializer.deserialize(serialized, APNote) + + # Verify round-trip + assert str(deserialized.id) == str(original.id) + assert deserialized.type == original.type + assert deserialized.content == original.content + assert deserialized.to == original.to + assert deserialized.cc == original.cc - def test_circular_reference_handling(self, valid_person): - """Test handling of circular references in serialization.""" - person = valid_person - note = APNote( - id="https://example.com/note/123", - type="Note", - content="Test note", - attributedTo=person - ) - person.liked = [note] # Create circular reference - - # Should not cause infinite recursion - serialized_person = person.serialize() - assert serialized_person["liked"][0]["id"] == note.id +def test_deserialize_with_extra_fields(): + """Test deserialization with extra fields in JSON.""" + data = { + "type": "Object", + "id": "https://example.com/object/123", + "name": "Test Object", + "extra_field": "Should be ignored" + } + obj = ActivityPubSerializer.deserialize(data, APObject) + + # Verify extra fields are handled + assert str(obj.id) == "https://example.com/object/123" + assert obj.type == "Object" + assert obj.name == "Test Object"