Files
devplacepy/tests/unit/services/sync/records.py
T
retoorandClaude Sonnet 5.5 02ab747cd0
DevPlace CI / test (push) Failing after 44m45s
Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
Continuous Delivery could never be turned off once a crop was planted. Add a
one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool
game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant,
autoreplant claim or Refactor. Autoreplant now also respects crop locks through
the shared _crop_lock predicate. Plan and measurements in happyplanting.md.

Also includes the pending community sync, node network and related work in the
tree; the full suite passed (4999) with all of it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 02:23:43 +02:00

150 lines
5.6 KiB
Python

# retoor <retoor@molodetz.nl>
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)