tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit 3974d2497cb756060fe04a17c29d8d1abd24d729
parent 71788230c6e99b3324c7261c05934efd087044f5
Author: triesap <tyson@radroots.org>
Date:   Fri,  5 Jun 2026 21:24:00 -0700

test-support: add in memory repository

Diffstat:
MCargo.lock | 1+
Mcrates/tangle_test_support/Cargo.toml | 1+
Mcrates/tangle_test_support/src/lib.rs | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 162 insertions(+), 4 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -416,6 +416,7 @@ dependencies = [ "tangle_crypto", "tangle_nips", "tangle_protocol", + "tangle_store", ] [[package]] diff --git a/crates/tangle_test_support/Cargo.toml b/crates/tangle_test_support/Cargo.toml @@ -14,6 +14,7 @@ serde_json = "1" tangle_crypto = { path = "../tangle_crypto" } tangle_nips = { path = "../tangle_nips" } tangle_protocol = { path = "../tangle_protocol" } +tangle_store = { path = "../tangle_store" } [lints] workspace = true diff --git a/crates/tangle_test_support/src/lib.rs b/crates/tangle_test_support/src/lib.rs @@ -4,9 +4,16 @@ use core::fmt; use k256::schnorr::signature::Signer; use k256::schnorr::{Signature, SigningKey}; use serde::Deserialize; +use std::collections::BTreeMap; use tangle_crypto::compute_event_id; +use tangle_nips::ListingProjection; use tangle_protocol::{ - Event, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, UnsignedEvent, event_to_value, + AddressCoordinate, Event, EventId, Kind, PublicKeyHex, SignatureHex, Tag, UnixTimestamp, + UnsignedEvent, event_to_value, +}; +use tangle_store::{ + DeletionMarker, DeletionMarkerRepository, ListingProjectionRepository, RawEventRepository, + RepositoryError, StoreEventOutcome, StoreProjectionOutcome, StoredEvent, }; const VALID_PUBLIC_LISTING_JSON: &str = @@ -136,6 +143,69 @@ pub fn fixture_event_json(event: &Event) -> serde_json::Value { event_to_value(event) } +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct InMemoryRepository { + events: BTreeMap<EventId, StoredEvent>, + listing_projections: BTreeMap<AddressCoordinate, ListingProjection>, + deletion_markers: Vec<DeletionMarker>, +} + +impl InMemoryRepository { + pub fn new() -> Self { + Self::default() + } +} + +impl RawEventRepository for InMemoryRepository { + fn put_event(&mut self, record: StoredEvent) -> Result<StoreEventOutcome, RepositoryError> { + let event_id = record.event().id().clone(); + if self.events.contains_key(&event_id) { + return Ok(StoreEventOutcome::Duplicate); + } + self.events.insert(event_id, record); + Ok(StoreEventOutcome::Inserted) + } + + fn event_by_id(&self, event_id: &EventId) -> Result<Option<StoredEvent>, RepositoryError> { + Ok(self.events.get(event_id).cloned()) + } + + fn events(&self) -> Result<Vec<StoredEvent>, RepositoryError> { + Ok(self.events.values().cloned().collect()) + } +} + +impl ListingProjectionRepository for InMemoryRepository { + fn put_listing_projection( + &mut self, + projection: ListingProjection, + ) -> Result<StoreProjectionOutcome, RepositoryError> { + let address = projection.identity().address().clone(); + match self.listing_projections.insert(address, projection) { + Some(_) => Ok(StoreProjectionOutcome::Replaced), + None => Ok(StoreProjectionOutcome::Inserted), + } + } + + fn listing_projection( + &self, + address: &AddressCoordinate, + ) -> Result<Option<ListingProjection>, RepositoryError> { + Ok(self.listing_projections.get(address).cloned()) + } +} + +impl DeletionMarkerRepository for InMemoryRepository { + fn put_deletion_marker(&mut self, marker: DeletionMarker) -> Result<(), RepositoryError> { + self.deletion_markers.push(marker); + Ok(()) + } + + fn deletion_markers(&self) -> Result<Vec<DeletionMarker>, RepositoryError> { + Ok(self.deletion_markers.clone()) + } +} + fn sign_unsigned_event(fixture_key: FixtureKey, unsigned: UnsignedEvent) -> Result<Event, String> { let signing_key = fixture_key.signing_key(); let event_id = compute_event_id(&unsigned); @@ -182,16 +252,21 @@ fn lower_hex(bytes: &[u8]) -> String { #[cfg(test)] mod tests { use super::{ - FixtureKey, auth_event_spec, build_fixture_event, deletion_event_spec, fixed_hex_bytes, - fixture_event_json, fixture_spec_from_json, projection_ineligible_listing_spec, - valid_public_listing_spec, + FixtureKey, InMemoryRepository, auth_event_spec, build_fixture_event, deletion_event_spec, + fixed_hex_bytes, fixture_event_json, fixture_spec_from_json, + projection_ineligible_listing_spec, valid_public_listing_spec, }; use tangle_crypto::{event_id_matches, verify_event_signature}; use tangle_nips::{ DeletionTarget, ListingProjectionEvaluation, evaluate_listing_projection, parse_deletion_request, parse_relay_auth_event, }; + use tangle_protocol::UnixTimestamp; use tangle_protocol::{EventId, PublicKeyHex}; + use tangle_store::{ + DeletionMarker, DeletionMarkerRepository, ListingProjectionRepository, RawEventRepository, + StoreEventOutcome, StoreProjectionOutcome, StoredEvent, + }; #[test] fn fixture_specs_load_from_canonical_json() { @@ -356,4 +431,85 @@ mod tests { "fixture must be lowercase hex" ); } + + #[test] + fn in_memory_repository_stores_raw_events_by_id_and_preserves_first_insert() { + let mut repository = InMemoryRepository::new(); + let event = build_fixture_event(&valid_public_listing_spec()).expect("event"); + let stored = StoredEvent::new(event.clone(), UnixTimestamp::new(100)); + let duplicate = StoredEvent::new(event.clone(), UnixTimestamp::new(200)); + let missing = EventId::new(&"f".repeat(EventId::HEX_LENGTH)).expect("missing"); + + assert_eq!( + repository.put_event(stored.clone()).expect("insert"), + StoreEventOutcome::Inserted + ); + assert_eq!( + repository.put_event(duplicate).expect("duplicate"), + StoreEventOutcome::Duplicate + ); + assert_eq!( + repository.event_by_id(event.id()).expect("lookup"), + Some(stored.clone()) + ); + assert_eq!(repository.event_by_id(&missing).expect("missing"), None); + assert_eq!(repository.events().expect("events"), vec![stored]); + } + + #[test] + fn in_memory_repository_stores_listing_projections_by_address() { + let mut repository = InMemoryRepository::new(); + let event = build_fixture_event(&valid_public_listing_spec()).expect("event"); + let projection = evaluate_listing_projection(&event) + .projection() + .expect("projection") + .clone(); + let address = projection.identity().address().clone(); + + assert_eq!( + repository + .put_listing_projection(projection.clone()) + .expect("insert"), + StoreProjectionOutcome::Inserted + ); + assert_eq!( + repository + .listing_projection(&address) + .expect("projection lookup"), + Some(projection.clone()) + ); + assert_eq!( + repository + .put_listing_projection(projection) + .expect("replace"), + StoreProjectionOutcome::Replaced + ); + } + + #[test] + fn in_memory_repository_appends_deletion_markers() { + let mut repository = InMemoryRepository::new(); + let deletion = build_fixture_event(&deletion_event_spec()).expect("deletion"); + let request = parse_deletion_request(&deletion) + .expect("deletion parse") + .expect("deletion request"); + let marker = DeletionMarker::new( + deletion.id().clone(), + deletion.unsigned().pubkey().clone(), + request.targets()[0].clone(), + UnixTimestamp::new(300), + ); + + repository + .put_deletion_marker(marker.clone()) + .expect("marker"); + repository + .put_deletion_marker(marker.clone()) + .expect("marker again"); + + assert_eq!( + repository.deletion_markers().expect("markers"), + vec![marker.clone(), marker] + ); + } }