Files
wehub-resource-sync fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

147 lines
5.3 KiB
Python

"""Tests for the shared PG-native JSON coercion utility."""
import json
from datetime import date, datetime, timezone
from decimal import Decimal
from uuid import UUID, uuid4
import pytest
from application.storage.db.serialization import (
PGNativeJSONEncoder,
coerce_pg_native,
decode_base64_bytes,
)
class TestCoercePgNative:
def test_uuid(self):
u = UUID("12345678-1234-5678-1234-567812345678")
assert coerce_pg_native(u) == "12345678-1234-5678-1234-567812345678"
def test_datetime_with_tz(self):
ts = datetime(2026, 5, 2, 12, 14, 32, tzinfo=timezone.utc)
assert coerce_pg_native(ts) == "2026-05-02T12:14:32+00:00"
def test_datetime_naive(self):
ts = datetime(2026, 5, 2, 12, 14, 32)
assert coerce_pg_native(ts) == "2026-05-02T12:14:32"
def test_date(self):
assert coerce_pg_native(date(2026, 5, 2)) == "2026-05-02"
def test_decimal_preserves_precision(self):
assert coerce_pg_native(Decimal("123.45000")) == "123.45000"
def test_bytes_base64_roundtrip(self):
b = b"\x00\x01\xff\x10gemini-binary"
coerced = coerce_pg_native(b)
assert isinstance(coerced, str)
assert decode_base64_bytes(coerced) == b
def test_dict_recurses_and_stringifies_keys(self):
u = uuid4()
got = coerce_pg_native({42: u, "n": [u, u]})
assert got == {"42": str(u), "n": [str(u), str(u)]}
def test_list_recurses(self):
ts = datetime(2026, 1, 1, tzinfo=timezone.utc)
got = coerce_pg_native([1, ts, {"a": ts}])
assert got == [1, "2026-01-01T00:00:00+00:00", {"a": "2026-01-01T00:00:00+00:00"}]
def test_tuple_becomes_list(self):
# Tuples have no JSON representation; the prior helper recursed
# only into list — verify we now flatten tuples to lists too.
u = uuid4()
got = coerce_pg_native(("x", u))
assert got == ["x", str(u)]
assert isinstance(got, list)
def test_passes_through_primitives(self):
assert coerce_pg_native("hello") == "hello"
assert coerce_pg_native(42) == 42
assert coerce_pg_native(3.14) == 3.14
assert coerce_pg_native(None) is None
assert coerce_pg_native(True) is True
def test_idempotent_for_already_safe_dict(self):
d = {"a": 1, "b": [2, "x"], "c": None}
assert coerce_pg_native(d) == d
def test_pg_row_dict_real_shape(self):
# Mirror the actual user_tools row dict shape that broke the
# continuation save: timestamp + UUID + nested actions.
row = {
"id": UUID("11111111-1111-1111-1111-111111111111"),
"name": "mcp_tool",
"status": True,
"actions": [{"name": "search", "active": True}],
"created_at": datetime(2026, 5, 2, 12, 14, 32, tzinfo=timezone.utc),
"updated_at": datetime(2026, 5, 2, 12, 14, 32, tzinfo=timezone.utc),
}
coerced = coerce_pg_native(row)
json.dumps(coerced) # would raise on raw datetime/UUID
assert coerced["id"] == "11111111-1111-1111-1111-111111111111"
assert coerced["created_at"] == "2026-05-02T12:14:32+00:00"
class TestPGNativeJSONEncoder:
def test_uuid(self):
u = uuid4()
assert json.loads(json.dumps(u, cls=PGNativeJSONEncoder)) == str(u)
def test_datetime(self):
ts = datetime(2026, 5, 2, 12, 14, 32, tzinfo=timezone.utc)
assert json.loads(json.dumps(ts, cls=PGNativeJSONEncoder)) == ts.isoformat()
def test_date(self):
d = date(2026, 5, 2)
assert json.loads(json.dumps(d, cls=PGNativeJSONEncoder)) == "2026-05-02"
def test_decimal(self):
assert json.loads(json.dumps(Decimal("99.99"), cls=PGNativeJSONEncoder)) == "99.99"
def test_bytes(self):
b = b"\x00\xff"
encoded = json.loads(json.dumps(b, cls=PGNativeJSONEncoder))
assert decode_base64_bytes(encoded) == b
def test_nested_pg_row(self):
row = {
"id": UUID("22222222-2222-2222-2222-222222222222"),
"amount": Decimal("42.00"),
"scheduled_for": date(2026, 6, 1),
"data": [{"created_at": datetime(2026, 5, 2, tzinfo=timezone.utc)}],
}
decoded = json.loads(json.dumps(row, cls=PGNativeJSONEncoder))
assert decoded["id"] == "22222222-2222-2222-2222-222222222222"
assert decoded["amount"] == "42.00"
assert decoded["scheduled_for"] == "2026-06-01"
assert decoded["data"][0]["created_at"] == "2026-05-02T00:00:00+00:00"
def test_unsupported_type_still_raises(self):
# Encoder must not silently swallow types it doesn't know how to
# handle — raising is how callers learn about gaps.
class Weird:
pass
with pytest.raises(TypeError):
json.dumps(Weird(), cls=PGNativeJSONEncoder)
class TestDecodeBase64Bytes:
def test_roundtrip(self):
b = b"\x00\x10\x20\xff"
from application.storage.db.serialization import _coerce_scalar
encoded = _coerce_scalar(b)
assert decode_base64_bytes(encoded) == b
def test_passes_through_bytes_unchanged(self):
assert decode_base64_bytes(b"raw") == b"raw"
def test_passes_through_none(self):
assert decode_base64_bytes(None) is None
def test_invalid_base64_falls_back_to_input(self):
assert decode_base64_bytes("not!valid!b64!") == "not!valid!b64!"