Skip to content

Commit 64de373

Browse files
committed
feat(models): add ActorEndpoints and expand model test coverage
This commit introduces the ActorEndpoints model, integrating it into the system's initial model rebuild process. Extensive new test suites have been added to enhance coverage and validate the functionality of several models, including: - Actor and its various subtypes - CryptographicKey, covering PEM and multibase key handling - DataIntegrityProof, including datetime conversions and serialization - Multikey, with multibase encoding/decoding for public and private keys - Question, validating fields like oneOf, anyOf, and closed - Tombstone, verifying formerType and deleted fields - ActivityPubModel, testing base class features and context aggregation - Core key utility functions for multibase encoding/decoding Additionally, the datetime serialization in the Question model has been updated to consistently use the 'Z' suffix for UTC offsets, ensuring standard compliance and predictability. This also includes validation for the recently adjusted @context serialization behavior.
1 parent bbaa4f9 commit 64de373

File tree

11 files changed

+1288
-1
lines changed

11 files changed

+1288
-1
lines changed

src/apmodel/_core/_initial/_rebuild.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
from ...vocab.activity.view import View # noqa: F401
6363
from ...vocab.actor import ( # noqa: F401
6464
Actor,
65+
ActorEndpoints,
6566
Application,
6667
Group,
6768
Organization,
@@ -87,6 +88,7 @@
8788
OrderedCollectionPage,
8889
# Actors
8990
Actor,
91+
ActorEndpoints,
9092
Application,
9193
Group,
9294
Organization,

src/apmodel/vocab/activity/question.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@ def validate_closed(
5353
@field_serializer("closed", when_used="always")
5454
def serialize_closed(self, value: Any, _) -> str | bool | Any:
5555
if isinstance(value, datetime.datetime):
56-
return value.isoformat(timespec="seconds")
56+
return value.isoformat(timespec="seconds").replace('+00:00', 'Z')
5757

5858
return value

tests/test_actor.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from apmodel.vocab.actor import Actor, Person, Application, Group, Organization, Service, ActorEndpoints
2+
from apmodel.core.collection import OrderedCollection
3+
4+
5+
def test_actor_creation():
6+
# Test creating an Actor instance
7+
actor = Actor(
8+
id="https://example.com/actor/1",
9+
name="Test Actor"
10+
)
11+
12+
assert actor.id == "https://example.com/actor/1"
13+
assert actor.name == "Test Actor"
14+
15+
16+
def test_actor_subtypes():
17+
# Test creating different actor subtypes
18+
person = Person(id="https://example.com/person/1", name="Test Person")
19+
application = Application(id="https://example.com/app/1", name="Test App")
20+
group = Group(id="https://example.com/group/1", name="Test Group")
21+
organization = Organization(id="https://example.com/org/1", name="Test Org")
22+
service = Service(id="https://example.com/service/1", name="Test Service")
23+
24+
assert person.type == "Person"
25+
assert application.type == "Application"
26+
assert group.type == "Group"
27+
assert organization.type == "Organization"
28+
assert service.type == "Service"
29+
30+
31+
def test_actor_with_collections():
32+
# Test creating an Actor with collection fields
33+
actor = Actor(
34+
id="https://example.com/actor/1",
35+
name="Test Actor",
36+
inbox="https://example.com/actor/1/inbox",
37+
outbox="https://example.com/actor/1/outbox",
38+
followers="https://example.com/actor/1/followers"
39+
)
40+
41+
assert actor.id == "https://example.com/actor/1"
42+
assert actor.name == "Test Actor"
43+
assert actor.inbox == "https://example.com/actor/1/inbox"
44+
assert actor.outbox == "https://example.com/actor/1/outbox"
45+
assert actor.followers == "https://example.com/actor/1/followers"
46+
47+
48+
def test_actor_with_ordered_collections():
49+
# Test creating an Actor with OrderedCollection fields
50+
inbox_collection = OrderedCollection(id="https://example.com/actor/1/inbox")
51+
actor = Actor(
52+
id="https://example.com/actor/1",
53+
name="Test Actor",
54+
inbox=inbox_collection
55+
)
56+
57+
assert actor.id == "https://example.com/actor/1"
58+
assert actor.name == "Test Actor"
59+
assert actor.inbox is not None
60+
assert hasattr(actor.inbox, 'id')
61+
62+
63+
def test_actor_with_additional_properties():
64+
# Test creating an Actor with additional properties
65+
actor = Actor(
66+
id="https://example.com/actor/1",
67+
name="Test Actor",
68+
preferred_username="testuser",
69+
discoverable=True,
70+
indexable=False
71+
)
72+
73+
assert actor.id == "https://example.com/actor/1"
74+
assert actor.name == "Test Actor"
75+
assert actor.preferred_username == "testuser"
76+
assert actor.discoverable is True
77+
assert actor.indexable is False
78+
79+
80+
def test_actor_serialization():
81+
# Test serialization of an Actor
82+
actor = Actor(
83+
id="https://example.com/actor/1",
84+
name="Test Actor",
85+
preferred_username="testuser",
86+
inbox="https://example.com/actor/1/inbox"
87+
)
88+
89+
serialized = actor.model_dump(by_alias=True)
90+
91+
assert serialized["id"] == "https://example.com/actor/1"
92+
assert serialized["name"] == "Test Actor"
93+
assert serialized["preferredUsername"] == "testuser"
94+
assert serialized["inbox"] == "https://example.com/actor/1/inbox"
95+
96+
97+
def test_actor_context_inference():
98+
# Test that actor context inference works properly
99+
actor = Actor(
100+
id="https://example.com/actor/1",
101+
name="Test Actor",
102+
public_key=None # This should not trigger security context
103+
)
104+
105+
# Call the _inference_context method to ensure it works
106+
result = {"id": "https://example.com/actor/1", "name": "Test Actor"}
107+
inferred_result = actor._inference_context(result)
108+
109+
# The result should have the basic context
110+
assert "@context" in inferred_result
111+
assert "https://www.w3.org/ns/activitystreams" in inferred_result["@context"]

tests/test_cryptographickey.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import pytest
2+
from cryptography.hazmat.primitives import serialization
3+
from cryptography.hazmat.primitives.asymmetric import rsa
4+
5+
from apmodel.extra.security import CryptographicKey
6+
7+
8+
def test_cryptographic_key_creation():
9+
# Test creating a CryptographicKey instance
10+
key = CryptographicKey(
11+
id="did:example:123#key-1",
12+
owner="did:example:123"
13+
)
14+
15+
assert key.id == "did:example:123#key-1"
16+
assert key.owner == "did:example:123"
17+
assert key.type == "CryptographicKey"
18+
19+
20+
def test_cryptographic_key_public_key_property_with_pem_string():
21+
# Generate an RSA key pair for testing
22+
private_key = rsa.generate_private_key(
23+
public_exponent=65537,
24+
key_size=2048,
25+
)
26+
public_key = private_key.public_key()
27+
28+
# Get the PEM representation of the public key
29+
public_pem = public_key.public_bytes(
30+
encoding=serialization.Encoding.PEM,
31+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
32+
).decode("utf-8")
33+
34+
# Create a CryptographicKey instance
35+
key = CryptographicKey(
36+
id="did:example:123#key-1",
37+
owner="did:example:123",
38+
public_key_pem=public_pem
39+
)
40+
41+
# Access the public key property
42+
retrieved_public_key = key.public_key
43+
44+
assert retrieved_public_key is not None
45+
assert isinstance(retrieved_public_key, rsa.RSAPublicKey)
46+
47+
48+
def test_cryptographic_key_public_key_property_with_pem_bytes():
49+
# Generate an RSA key pair for testing
50+
private_key = rsa.generate_private_key(
51+
public_exponent=65537,
52+
key_size=2048,
53+
)
54+
public_key = private_key.public_key()
55+
56+
# Get the PEM representation of the public key as bytes
57+
public_pem_bytes = public_key.public_bytes(
58+
encoding=serialization.Encoding.PEM,
59+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
60+
)
61+
62+
# Create a CryptographicKey instance
63+
key = CryptographicKey(
64+
id="did:example:123#key-1",
65+
owner="did:example:123",
66+
public_key_pem=public_pem_bytes
67+
)
68+
69+
# Access the public key property
70+
retrieved_public_key = key.public_key
71+
72+
assert retrieved_public_key is not None
73+
assert isinstance(retrieved_public_key, rsa.RSAPublicKey)
74+
75+
76+
def test_cryptographic_key_public_key_property_with_no_pem():
77+
# Create a CryptographicKey instance without public key PEM
78+
key = CryptographicKey(
79+
id="did:example:123#key-1",
80+
owner="did:example:123"
81+
)
82+
83+
# Access the public key property
84+
retrieved_public_key = key.public_key
85+
86+
assert retrieved_public_key is None
87+
88+
89+
def test_cryptographic_key_set_public_key_with_rsa_public_key():
90+
# Generate an RSA key pair for testing
91+
private_key = rsa.generate_private_key(
92+
public_exponent=65537,
93+
key_size=2048,
94+
)
95+
public_key = private_key.public_key()
96+
97+
# Create a CryptographicKey instance
98+
key = CryptographicKey(
99+
id="did:example:123#key-1",
100+
owner="did:example:123"
101+
)
102+
103+
# Set the public key
104+
key.set_public_key = public_key
105+
106+
# Check that the PEM representation was set
107+
assert key.public_key_pem is not None
108+
assert isinstance(key.public_key_pem, str)
109+
110+
# Check that the public key property returns the correct key
111+
retrieved_key = key.public_key
112+
assert retrieved_key is not None
113+
assert isinstance(retrieved_key, rsa.RSAPublicKey)
114+
115+
116+
def test_cryptographic_key_set_public_key_with_rsa_private_key():
117+
# Generate an RSA key pair for testing
118+
private_key = rsa.generate_private_key(
119+
public_exponent=65537,
120+
key_size=2048,
121+
)
122+
123+
# Create a CryptographicKey instance
124+
key = CryptographicKey(
125+
id="did:example:123#key-1",
126+
owner="did:example:123"
127+
)
128+
129+
# Set the public key using the private key (should extract the public key)
130+
key.set_public_key = private_key
131+
132+
# Check that the PEM representation was set
133+
assert key.public_key_pem is not None
134+
assert isinstance(key.public_key_pem, str)
135+
136+
# Check that the public key property returns the correct key
137+
retrieved_key = key.public_key
138+
assert retrieved_key is not None
139+
assert isinstance(retrieved_key, rsa.RSAPublicKey)
140+
141+
142+
def test_cryptographic_key_invalid_key_type():
143+
# Test with an invalid key type that is not RSA
144+
class InvalidKeyType:
145+
pass
146+
147+
key = CryptographicKey(
148+
id="did:example:123#key-1",
149+
owner="did:example:123",
150+
public_key_pem="invalid pem data"
151+
)
152+
153+
# This should raise a ValueError when accessing public_key
154+
with pytest.raises(ValueError):
155+
_ = key.public_key
156+
157+
158+
def test_cryptographic_key_serialization():
159+
# Generate an RSA key pair for testing
160+
private_key = rsa.generate_private_key(
161+
public_exponent=65537,
162+
key_size=2048,
163+
)
164+
public_key = private_key.public_key()
165+
166+
# Get the PEM representation of the public key
167+
public_pem = public_key.public_bytes(
168+
encoding=serialization.Encoding.PEM,
169+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
170+
).decode("utf-8")
171+
172+
# Create a CryptographicKey instance
173+
key = CryptographicKey(
174+
id="did:example:123#key-1",
175+
owner="did:example:123",
176+
public_key_pem=public_pem
177+
)
178+
179+
# Check serialization
180+
serialized = key.model_dump(by_alias=True)
181+
assert serialized["id"] == "did:example:123#key-1"
182+
assert serialized["owner"] == "did:example:123"
183+
assert "publicKeyPem" in serialized
184+
assert serialized["type"] == "CryptographicKey"

0 commit comments

Comments
 (0)