# retoor import pytest from pydantic import ValidationError from devplacepy.services.sync.records import ( CommentRecord, PostRecord, SyncBatchForm, UserRecord, ) from devplacepy.services.sync.spec import LIMITS BASE = {"origin_uid": "r1", "created_at": "2025-01-01T00:00:00Z"} def test_updated_at_defaults_to_created_and_never_precedes_it(): record = PostRecord.model_validate( {**BASE, "author_uid": "u", "content": "ten chars!!"} ) assert record.updated_at == record.created_at earlier = PostRecord.model_validate( {**BASE, "author_uid": "u", "content": "ten chars!!", "updated_at": "2024-01-01"} ) assert earlier.updated_at == earlier.created_at def test_unknown_topic_becomes_random_and_known_is_kept(): base = {**BASE, "author_uid": "u", "content": "ten chars!!"} assert PostRecord.model_validate({**base, "topic": "zzz"}).topic == "random" assert PostRecord.model_validate({**base, "topic": "DevLog"}).topic == "devlog" @pytest.mark.parametrize( "payload", [ {"author_uid": "u", "content": "short"}, {"author_uid": "u", "content": "x" * (LIMITS.max_post_length + 1)}, {"author_uid": "", "content": "ten chars!!"}, {"content": "ten chars!!"}, {"author_uid": "u", "content": "ten chars!!", "title": "t" * (LIMITS.max_title_length + 1)}, ], ) def test_invalid_posts_are_rejected(payload): with pytest.raises(ValidationError): PostRecord.model_validate({**BASE, **payload}) def test_comment_parent_is_optional_and_post_is_required(): base = {**BASE, "author_uid": "u", "content": "fine"} assert CommentRecord.model_validate({**base, "post_uid": "p"}).parent_uid is None assert CommentRecord.model_validate({**base, "post_uid": "p", "parent_uid": ""}).parent_uid is None with pytest.raises(ValidationError): CommentRecord.model_validate(base) def test_user_urls_are_fixed_or_dropped(): record = UserRecord.model_validate( {**BASE, "username": "bob", "website": "example.com", "git_link": "not a url"} ) assert record.website == "https://example.com" assert record.git_link == "" def test_user_bio_over_limit_is_rejected(): with pytest.raises(ValidationError): UserRecord.model_validate({**BASE, "username": "bob", "bio": "x" * (LIMITS.max_bio_length + 1)}) def test_user_counters_and_credentials_are_not_fields(): known = UserRecord.known_fields() for name in ("email", "password_hash", "role", "xp", "level", "stars", "api_key"): assert name not in known assert "stars" not in PostRecord.known_fields() def test_envelope_normalizes_origin_and_bounds_batch(): form = SyncBatchForm(origin="Other.ORG", records=[{}]) assert form.origin == "other.org" with pytest.raises(ValidationError): SyncBatchForm(origin="other.org", records=[]) with pytest.raises(ValidationError): SyncBatchForm(origin="other.org", records=[{}] * (LIMITS.max_batch_records + 1)) with pytest.raises(ValidationError): SyncBatchForm(origin="bad origin", records=[{}]) def test_new_kinds_have_models_and_a_published_schema(): from devplacepy.services.sync.records import RECORD_MODELS from devplacepy.services.sync.spec import KINDS assert set(RECORD_MODELS) == set(KINDS) for model in RECORD_MODELS.values(): assert model.model_json_schema()["title"] def test_vote_value_and_target_kind_are_closed_sets(): import pytest from pydantic import ValidationError from devplacepy.services.sync.records import VoteRecord base = {"origin_uid": "v", "voter_uid": "u", "target_kind": "post", "target_uid": "p", "value": 1, "created_at": "2025-01-01T00:00:00Z"} assert VoteRecord.model_validate(base).value == 1 for change in ({"value": 0}, {"value": 2}, {"target_kind": "gist"}, {"voter_uid": "bad id"}): with pytest.raises(ValidationError): VoteRecord.model_validate({**base, **change}) def test_attachment_fingerprint_ignores_the_blob_but_not_its_hash(): import base64 from devplacepy.services.sync.records import AttachmentRecord def make(data): return AttachmentRecord.model_validate({ "origin_uid": "a", "author_uid": "u", "target_kind": "post", "target_uid": "p", "filename": "dir/evil\\name.png", "content_b64": base64.b64encode(data).decode(), "created_at": "2025-01-01T00:00:00Z", }) first, second = make(b"one"), make(b"two") assert first.filename == "name.png" assert "content_b64" not in first.content_fields() and first.content_fields()["sha256"] != second.content_fields()["sha256"] assert first.content_bytes() == b"one" def test_batch_byte_cap(monkeypatch): import pytest from dataclasses import replace from pydantic import ValidationError from devplacepy.services.sync import records from devplacepy.services.sync.spec import LIMITS monkeypatch.setattr(records, "LIMITS", replace(LIMITS, max_batch_bytes=100)) with pytest.raises(ValidationError): records.SyncBatchForm(origin="a.b", records=[{"x": "y" * 200}]) def test_ack_form_validates_kinds_and_ids(): import pytest from pydantic import ValidationError from devplacepy.services.sync.records import SyncAckForm assert SyncAckForm(origin="A.B", items='[{"kind": "posts", "origin_uid": "p1"}]').items == [{"kind": "posts", "origin_uid": "p1"}] for items in ([], [{"kind": "gists", "origin_uid": "x"}], [{"kind": "posts", "origin_uid": ""}], "not json"): with pytest.raises(ValidationError): SyncAckForm(origin="a.b", items=items)