Implement identity graph stitching and configurable profile storage
Issue persistent signed anonymous IDs and accept cookie/token-bound events. Resolve verified email, phone, OAuth, and namespaced third-party IDs into transactionally merged master identities with historical event aliases. Protect shared-device account switching, tenant isolation, and event retries. Store sparse canonical profiles as PostgreSQL JSONB or local SQLite JSON. Support an optional versioned Cassandra wide-column projection through a transactional outbox, retry worker, and immediate canonical read fallback. Document deployment settings and provide opt-in database integration tests. Validation: 43 local tests passed; 6 external database tests skipped. Ruff checks, formatting, and offline uv lock verification passed.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""Optional supplied Cassandra environment; never install/start a server here."""
|
||||
|
||||
import os
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from adcloud_server.database import Database
|
||||
from adcloud_server.identity_models import ProfilePatch, StitchRequest
|
||||
from adcloud_server.identity_service import IdentityService
|
||||
from adcloud_server.profile_store import CassandraProfileStore
|
||||
from adcloud_server.projection import ProfileProjector
|
||||
from adcloud_server.settings import Settings
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.getenv("ADCLOUD_TEST_CASSANDRA_HOSTS"),
|
||||
reason="No Cassandra integration cluster configured",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def components(tmp_path):
|
||||
settings = Settings(
|
||||
database_url="sqlite:///" + str(tmp_path / "graph.db"),
|
||||
profile_backend="cassandra",
|
||||
tenant_id="test-" + str(uuid4()),
|
||||
identity_secret="test-cassandra-" + "x" * 48,
|
||||
cassandra_hosts=tuple(os.environ["ADCLOUD_TEST_CASSANDRA_HOSTS"].split(",")),
|
||||
cassandra_port=int(os.getenv("ADCLOUD_TEST_CASSANDRA_PORT", "9042")),
|
||||
cassandra_keyspace=os.environ["ADCLOUD_TEST_CASSANDRA_KEYSPACE"],
|
||||
cassandra_username=os.getenv("ADCLOUD_TEST_CASSANDRA_USERNAME", ""),
|
||||
cassandra_password=os.getenv("ADCLOUD_TEST_CASSANDRA_PASSWORD", ""),
|
||||
cassandra_tls=os.getenv("ADCLOUD_TEST_CASSANDRA_TLS", "true").lower() == "true",
|
||||
cassandra_ca_file=os.getenv("ADCLOUD_TEST_CASSANDRA_CA_FILE") or None,
|
||||
)
|
||||
db = Database(settings.database_url, settings.tenant_id)
|
||||
db.initialize()
|
||||
store = CassandraProfileStore(settings)
|
||||
try:
|
||||
yield IdentityService(settings, db, store), ProfileProjector(db, store), store
|
||||
finally:
|
||||
store.close()
|
||||
db.close()
|
||||
|
||||
|
||||
def test_cassandra_sparse_snapshot_roundtrip(components):
|
||||
service, _worker, store = components
|
||||
master = str(uuid4())
|
||||
document = {
|
||||
"attributes": {"nested": {"interest": "running"}, "enabled": True, "score": 0},
|
||||
"tags": ["runner"],
|
||||
}
|
||||
store.put(service.tenant, master, 1, document)
|
||||
assert store.get(service.tenant, master, 1) == document
|
||||
assert store.get(service.tenant, master, 2) is None
|
||||
|
||||
|
||||
def test_cassandra_immutable_revision_prevents_stale_attribute_resurrection(components):
|
||||
service, _worker, store = components
|
||||
master = str(uuid4())
|
||||
old = {"attributes": {"remove": "old"}, "tags": []}
|
||||
new = {"attributes": {}, "tags": []}
|
||||
store.put(service.tenant, master, 2, new)
|
||||
store.put(service.tenant, master, 1, old)
|
||||
assert store.get(service.tenant, master, 2) == new
|
||||
|
||||
|
||||
def test_cassandra_worker_publishes_stitched_profile(components):
|
||||
service, worker, _store = components
|
||||
anon = service.anonymous()
|
||||
stitched = service.stitch(
|
||||
anon["anonymous_token"],
|
||||
StitchRequest(
|
||||
identifiers=[{"kind": "email", "value": "alice@example.com"}],
|
||||
attributes={"interest": "running"},
|
||||
),
|
||||
)
|
||||
assert service.profile(stitched["master_id"])["projection_status"] == "pending"
|
||||
worker.flush()
|
||||
current = service.profile(stitched["master_id"])
|
||||
assert current["projection_status"] == "current"
|
||||
assert current["attributes"] == {"interest": "running"}
|
||||
service.patch_profile(
|
||||
stitched["master_id"], ProfilePatch(attributes={"interest": None})
|
||||
)
|
||||
worker.flush()
|
||||
assert service.profile(stitched["master_id"])["attributes"] == {}
|
||||
Reference in New Issue
Block a user