Files
MetaCrate/tests/compat/tests/marketplace_manager_semantics.rs

465 lines
21 KiB
Rust

// Exact Rust translation of LibreMetaverse.Tests/MarketplaceManagerTests.cs.
// Source SHA-256: 059a0cf2c190954fb6a52aed1c63ca286aa22a7eca72a6a5e9f18e3054f55f38
use libremetaverse::marketplace::{
MarketplaceErrorEventArgs, MarketplaceListing, MarketplaceListingChangedEventArgs,
MarketplaceListingStatus, MarketplaceListingsSyncedEventArgs, MarketplaceManager,
};
use libremetaverse::{GridClient, HttpCapsClient, Simulator};
use libremetaverse_compat_tests::block_on;
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{
ExternalError, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
use std::collections::{BTreeMap, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
const CAP_BASE: &str = "http://test.invalid/direct-delivery";
const SEED: &str = "http://test.invalid/seed";
const FOLDER_1: &str = "11111111-1111-1111-1111-111111111111";
const VERSION_1: &str = "22222222-2222-2222-2222-222222222222";
const FOLDER_2: &str = "33333333-3333-3333-3333-333333333333";
const VERSION_2: &str = "44444444-4444-4444-4444-444444444444";
fn uuid(text: &str) -> UUID {
UUID::new_with_string(text.into()).expect("UUID string constructor")
}
fn random_uuid() -> UUID {
UUID::random().expect("UUID Random")
}
fn response(status: u16, body: &str) -> HttpResponse {
HttpResponse {
status_code: status,
headers: BTreeMap::new(),
content_type: Some("application/json".into()),
body: body.as_bytes().to_vec(),
}
}
struct Harness {
manager: MarketplaceManager,
requests: Arc<Mutex<Vec<HttpRequest>>>,
responses: Arc<Mutex<VecDeque<(String, HttpResponse)>>>,
}
impl Harness {
fn new() -> Self {
let requests = Arc::new(Mutex::new(Vec::new()));
let responses = Arc::new(Mutex::new(VecDeque::new()));
let captured = Arc::clone(&requests);
let queued = Arc::clone(&responses);
let handler = HttpMessageHandler::new(move |request, _| {
let reply = if request.uri.0 == SEED {
HttpResponse {
status_code: 200,
headers: BTreeMap::new(),
content_type: Some("application/llsd+xml".into()),
body: format!(
"<llsd><map><key>DirectDelivery</key><uri>{CAP_BASE}</uri></map></llsd>"
)
.into_bytes(),
}
} else {
let uri = request.uri.0.clone();
captured.lock().unwrap().push(request);
let mut queued = queued.lock().unwrap();
let index = queued
.iter()
.position(|(expected, _)| expected == &uri)
.expect("queued response for request URI");
queued.remove(index).unwrap().1
};
async move { reply }
});
let mut client = GridClient::new().expect("GridClient constructor");
client.set_http_caps_client(HttpCapsClient::new(handler.clone()).unwrap());
let mut simulator_client = GridClient::new().expect("simulator GridClient constructor");
simulator_client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
let simulator = Simulator::new(
simulator_client,
"127.0.0.1:13".parse().unwrap(),
0,
None,
None,
)
.expect("Simulator constructor");
simulator
.set_seed_caps(Some(Uri(SEED.into())), Some(true))
.unwrap();
let mut network = client.network();
network.set_current_sim(Some(simulator));
client.set_network(network);
Self {
manager: MarketplaceManager::new(client).expect("MarketplaceManager constructor"),
requests,
responses,
}
}
fn add(&self, path: &str, status: u16, body: &str) {
self.responses
.lock()
.unwrap()
.push_back((format!("{CAP_BASE}/{path}"), response(status, body)));
}
fn requests(&self) -> Vec<HttpRequest> {
self.requests.lock().unwrap().clone()
}
fn last_request(&self) -> HttpRequest {
self.requests().last().expect("captured request").clone()
}
fn seed_listing(&self, id: i32, folder: &str, version: &str, count: i32) {
self.add(
"listings",
200,
&format!(
"{{\"listings\":[{{\"id\":{id},\"is_listed\":false,\"inventory_info\":{{\"listing_folder_id\":\"{folder}\",\"version_folder_id\":\"{version}\",\"count_on_hand\":{count}}}}}]}}"
),
);
block_on(self.manager.fetch_listings(None)).unwrap();
}
}
fn listing_json(id: i32, folder: &str) -> String {
format!(
"{{\"listings\":[{{\"id\":{id},\"inventory_info\":{{\"listing_folder_id\":\"{folder}\"}}}}]}}"
)
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_ListingsArrayResponse_PopulatesBothCaches::test 1ec347288488be26f8a4b923d304ae9598d1d4f40423356012c353b3527ae089 translated
#[test]
fn fetch_listings_populates_both_caches() {
let h = Harness::new();
h.add("listings", 200, &format!("{{\"listings\":[{{\"id\":101,\"is_listed\":true,\"edit_url\":\"http://mp.sl.com/edit/101\",\"inventory_info\":{{\"listing_folder_id\":\"{FOLDER_1}\",\"version_folder_id\":\"{VERSION_1}\",\"count_on_hand\":3}}}}]}}"));
block_on(h.manager.fetch_listings(None)).unwrap();
let by_id = h.manager.listings_by_id();
let by_folder = h.manager.listings_by_folder();
assert!(by_id.contains_key(&101));
assert!(by_folder.contains_key(&uuid(FOLDER_1)));
let listing = &by_id[&101];
assert_eq!(listing.status(), MarketplaceListingStatus::Listed);
assert_eq!(listing.stock_count(), 3);
assert_eq!(listing.version_folder_uuid(), uuid(VERSION_1));
assert_eq!(
listing.edit_url().as_deref(),
Some("http://mp.sl.com/edit/101")
);
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_UnlistedEntry_ParsesAsUnlisted::test 3e5b22da9fcc6a38ddec3ffb215367320df035aed33c9c0690461fdce786b37d translated
#[test]
fn fetch_listings_parses_unlisted_entry() {
let h = Harness::new();
h.add("listings", 200, &format!("{{\"listings\":[{{\"id\":102,\"is_listed\":false,\"inventory_info\":{{\"listing_folder_id\":\"{FOLDER_2}\"}}}}]}}"));
block_on(h.manager.fetch_listings(None)).unwrap();
assert_eq!(
h.manager.listings_by_id()[&102].status(),
MarketplaceListingStatus::Unlisted
);
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_FiresListingsSyncedEvent::test ec70956ce5eb51fe650a6e2e77924ec21051b0c06396bce7c9c82a277f6aa82a translated
#[test]
fn fetch_listings_fires_synced_event() {
let h = Harness::new();
h.add("listings", 200, &listing_json(101, FOLDER_1));
let raised = Arc::new(Mutex::new(None));
let captured = Arc::clone(&raised);
let _subscription = h.manager.subscribe_listings_synced(Arc::new(move |args| {
let listings = args.listings();
*captured.lock().unwrap() = Some((listings.len(), listings[0].listing_id()));
}));
block_on(h.manager.fetch_listings(None)).unwrap();
assert_eq!(*raised.lock().unwrap(), Some((1, 101)));
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_HttpError_FiresErrorEventAndLeavesEmptyCache::test 0a71f4db07be054bf77752f07223268bd25b673a9c5c8cfe8a5f78ee36b4a463 translated
#[test]
fn fetch_listings_http_error_fires_error_and_leaves_empty_cache() {
let h = Harness::new();
h.add("listings", 500, "");
let error = Arc::new(Mutex::new(None));
let captured = Arc::clone(&error);
let _subscription = h.manager.subscribe_error(Arc::new(move |args| {
*captured.lock().unwrap() = Some(args.message());
}));
block_on(h.manager.fetch_listings(None)).unwrap();
assert!(
error
.lock()
.unwrap()
.as_ref()
.is_some_and(|message| !message.is_empty())
);
assert!(h.manager.listings_by_id().is_empty());
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_EmptyListingsArray_ResultsInEmptyCache::test 79ab6108d67cba54f55a8603f3929f1329cbe84ce3b591717d4b4712c2bbe0a2 translated
#[test]
fn fetch_listings_empty_array_results_in_empty_cache() {
let h = Harness::new();
h.add("listings", 200, "{\"listings\":[]}");
block_on(h.manager.fetch_listings(None)).unwrap();
assert!(h.manager.listings_by_id().is_empty());
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.FetchListingsAsync_SecondCall_ReplacesCacheWithNewData::test 2327332d62529d276a8e1e074b20c3a37a47b5c820910644e6562317fdd4651b translated
#[test]
fn fetch_listings_second_call_replaces_cache() {
let h = Harness::new();
h.add("listings", 200, &listing_json(101, FOLDER_1));
block_on(h.manager.fetch_listings(None)).unwrap();
assert!(h.manager.listings_by_id().contains_key(&101));
h.add("listings", 200, &listing_json(202, FOLDER_2));
block_on(h.manager.fetch_listings(None)).unwrap();
let listings = h.manager.listings_by_id();
assert!(!listings.contains_key(&101));
assert!(listings.contains_key(&202));
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.CreateListingAsync_SendsNestedInventoryInfoNotFlatUuidField::test efda655bf20ec6157df4738df4cf4ddcc1ee9e1ade01f1f0177630659b54b1d6 translated
#[test]
fn create_listing_sends_nested_inventory_info() {
let h = Harness::new();
h.add("listings", 200, &listing_json(202, FOLDER_2));
let result = block_on(h.manager.create_listing(
uuid(FOLDER_2),
uuid(VERSION_2),
"My Listing".into(),
Some(5),
None,
))
.unwrap()
.expect("created listing");
assert_eq!(result.listing_id(), 202);
let request = h.last_request();
assert_eq!(request.method, "POST");
assert_eq!(request.uri.0, format!("{CAP_BASE}/listings"));
let body = String::from_utf8(request.body).unwrap();
assert!(body.contains("\"name\":\"My Listing\""));
assert!(body.contains(&format!("\"listing_folder_id\":\"{FOLDER_2}\"")));
assert!(body.contains(&format!("\"version_folder_id\":\"{VERSION_2}\"")));
assert!(body.contains("\"count_on_hand\":5"));
assert!(!body.contains("listing_folder_uuid"));
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.CreateListingAsync_FiresListingChangedEvent::test 242073ff6fbae379c518d047cd63e1ad368ff37ffc2cad379b8c007fb725b74e translated
#[test]
fn create_listing_fires_changed_event() {
let h = Harness::new();
h.add("listings", 200, &listing_json(202, FOLDER_2));
let changed = Arc::new(Mutex::new(None));
let captured = Arc::clone(&changed);
let _subscription = h.manager.subscribe_listing_changed(Arc::new(move |args| {
let listing = args.listing().expect("changed listing");
*captured.lock().unwrap() = Some((args.listing_id(), listing.listing_folder_uuid()));
}));
block_on(
h.manager
.create_listing(uuid(FOLDER_2), uuid(VERSION_2), "Name".into(), None, None),
)
.unwrap();
assert_eq!(*changed.lock().unwrap(), Some((202, uuid(FOLDER_2))));
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.CreateListingAsync_HttpError_ReturnsNullAndFiresErrorEvent::test fa999c4cce5bee688ef3bb6342e276867268ab9f32571eff25646cb1384bcece translated
#[test]
fn create_listing_http_error_returns_none_and_fires_error() {
let h = Harness::new();
h.add("listings", 400, "");
let fired = Arc::new(Mutex::new(false));
let captured = Arc::clone(&fired);
let _subscription = h
.manager
.subscribe_error(Arc::new(move |_| *captured.lock().unwrap() = true));
let result =
block_on(
h.manager
.create_listing(random_uuid(), random_uuid(), "Name".into(), None, None),
)
.unwrap();
assert!(result.is_none());
assert!(*fired.lock().unwrap());
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.DeleteListingAsync_UsesSingularListingRoute::test aea0672be109f05632df326a0c2caf7b7d14d23cf0d66e6ab867902351ebb554 translated
#[test]
fn delete_listing_uses_singular_route_and_evicts_caches() {
let h = Harness::new();
h.add("listings", 200, &listing_json(101, FOLDER_1));
block_on(h.manager.fetch_listings(None)).unwrap();
let mut listing = None;
assert!(h.manager.try_get_by_id(101, &mut listing));
h.add("listing/101", 200, "");
assert!(block_on(h.manager.delete_listing(101, None)).unwrap());
assert!(!h.manager.try_get_by_id(101, &mut None));
assert!(!h.manager.try_get_by_folder(uuid(FOLDER_1), &mut None));
assert_eq!(h.last_request().uri.0, format!("{CAP_BASE}/listing/101"));
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.DeleteListingAsync_HttpError_ReturnsFalseAndFiresErrorEvent::test 52976e09904483e7f33dbe016d695e380646f75b2fdd1b1f744c3fc69dd9d192 translated
#[test]
fn delete_listing_http_error_returns_false_and_fires_error() {
let h = Harness::new();
h.add("listing/999", 404, "");
let fired = Arc::new(Mutex::new(false));
let captured = Arc::clone(&fired);
let _subscription = h
.manager
.subscribe_error(Arc::new(move |_| *captured.lock().unwrap() = true));
assert!(!block_on(h.manager.delete_listing(999, None)).unwrap());
assert!(*fired.lock().unwrap());
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.ActivateListingAsync_SendsIsListedTrueAndCachedInventoryInfoToSingularRoute::test 4b6daed0e333c504a7aed8666980152990c37d81830d08f3a31faf39dbbc706f translated
#[test]
fn activate_listing_sends_full_cached_inventory_info() {
let h = Harness::new();
h.seed_listing(101, FOLDER_1, VERSION_1, 7);
h.add("listing/101", 200, &format!("{{\"listings\":[{{\"id\":101,\"is_listed\":true,\"inventory_info\":{{\"listing_folder_id\":\"{FOLDER_1}\",\"version_folder_id\":\"{VERSION_1}\",\"count_on_hand\":7}}}}]}}"));
assert!(block_on(h.manager.activate_listing(101, None)).unwrap());
let request = h.last_request();
assert_eq!(request.method, "PUT");
assert_eq!(request.uri.0, format!("{CAP_BASE}/listing/101"));
let body = String::from_utf8(request.body).unwrap();
for expected in [
"\"is_listed\":true",
"\"id\":101",
&format!("\"listing_folder_id\":\"{FOLDER_1}\""),
&format!("\"version_folder_id\":\"{VERSION_1}\""),
"\"count_on_hand\":7",
] {
assert!(body.contains(expected));
}
let mut listing = None;
assert!(h.manager.try_get_by_id(101, &mut listing));
assert_eq!(listing.unwrap().status(), MarketplaceListingStatus::Listed);
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.ActivateListingAsync_NoCachedListing_ReturnsFalseWithoutSendingRequest::test 1c4d407a29ba139b13e868882f1721327cfdd64dc2d2e9a32a0c71678905bb65 translated
#[test]
fn activate_listing_without_cached_listing_sends_nothing() {
let h = Harness::new();
assert!(!block_on(h.manager.activate_listing(999, None)).unwrap());
assert!(h.requests().is_empty());
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.ActivateListingAsync_HttpError_ReturnsFalse::test 1f7b39a95adfd13cf159fbadac9d157a70ebbdcb360a1cf1cf4d21b891bd61d6 translated
#[test]
fn activate_listing_http_error_returns_false() {
let h = Harness::new();
h.seed_listing(101, FOLDER_1, VERSION_1, 0);
h.add("listing/101", 400, "");
assert!(!block_on(h.manager.activate_listing(101, None)).unwrap());
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.DeactivateListingAsync_SendsIsListedFalse::test 1fa6eb4bfc86fb9a5fe746b18e43568b9dcc1a723e3a667b1bc57a1ebb66fb50 translated
#[test]
fn deactivate_listing_sends_is_listed_false() {
let h = Harness::new();
h.seed_listing(101, FOLDER_1, VERSION_1, 0);
h.add("listing/101", 200, &format!("{{\"listings\":[{{\"id\":101,\"is_listed\":false,\"inventory_info\":{{\"listing_folder_id\":\"{FOLDER_1}\"}}}}]}}"));
block_on(h.manager.deactivate_listing(101, None)).unwrap();
assert!(
String::from_utf8(h.last_request().body)
.unwrap()
.contains("\"is_listed\":false")
);
let mut listing = None;
assert!(h.manager.try_get_by_id(101, &mut listing));
assert_eq!(
listing.unwrap().status(),
MarketplaceListingStatus::Unlisted
);
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.TryGetByFolder_AfterFetch_ReturnsMatchingListing::test cb0b2a301f9096a238ba078739f0b6488a4dcc20fecdd59d51c930f7ea28902c translated
#[test]
fn try_get_by_folder_returns_matching_listing() {
let h = Harness::new();
h.add("listings", 200, &listing_json(101, FOLDER_1));
block_on(h.manager.fetch_listings(None)).unwrap();
let mut listing = None;
assert!(h.manager.try_get_by_folder(uuid(FOLDER_1), &mut listing));
assert_eq!(listing.unwrap().listing_id(), 101);
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.TryGetByFolder_MissingUUID_ReturnsFalse::test 86fe3e33c410412e829efbc4e775b164492b2dd14d82421478552e9a01c35ef8 translated
#[test]
fn try_get_by_folder_missing_uuid_returns_false() {
let h = Harness::new();
h.add("listings", 200, "{\"listings\":[]}");
block_on(h.manager.fetch_listings(None)).unwrap();
assert!(!h.manager.try_get_by_folder(random_uuid(), &mut None));
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.TryGetById_EmptyCache_ReturnsFalse::test 8d49aeaf5444d8d2d2098f932df01dac6e6942a86b105287c5f40cb54e501dcc translated
#[test]
fn try_get_by_id_empty_cache_returns_false() {
let h = Harness::new();
assert!(!h.manager.try_get_by_id(9999, &mut None));
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceListingsSyncedEventArgs_StoresListings::test c0744e6b9f9282613692f7be962009e9129c234e3ca863ab40b581baa90a6a68 translated
#[test]
fn listings_synced_event_args_store_listings() {
let mut listing = MarketplaceListing::new().unwrap();
listing.set_listing_id(1);
let args = MarketplaceListingsSyncedEventArgs::new(vec![listing]).unwrap();
let listings = args.listings();
assert_eq!(listings.len(), 1);
assert_eq!(listings[0].listing_id(), 1);
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceListingChangedEventArgs_StoresListingIdAndListing::test 874578d97b250b4667e3cb0c28626948ede80fba14e9ae9add153e2dfddc1902 translated
#[test]
fn listing_changed_event_args_store_same_listing() {
let mut listing = MarketplaceListing::new().unwrap();
listing.set_listing_id(42);
let listing = Arc::new(listing);
let args = MarketplaceListingChangedEventArgs::new(42, Some(Arc::clone(&listing))).unwrap();
assert_eq!(args.listing_id(), 42);
assert!(Arc::ptr_eq(&args.listing().unwrap(), &listing));
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceListingChangedEventArgs_NullListing_IsValidForDeleteScenario::test 6a4fadad3a00e0e17977506badf7e08a53278f7c89fc4c7597a502164257f50d translated
#[test]
fn listing_changed_event_args_allow_none_for_delete() {
let args = MarketplaceListingChangedEventArgs::new(99, None).unwrap();
assert_eq!(args.listing_id(), 99);
assert!(args.listing().is_none());
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceErrorEventArgs_StoresMessageAndException::test cb0213208ab6b63f32a950f1771354fae4b6904c72720b1705ddf5b8355ffc7d translated
#[test]
fn error_event_args_store_same_message_and_exception() {
let error = Arc::new(ExternalError("inner error".into()));
let args = MarketplaceErrorEventArgs::new("Something failed.".into(), Some(Arc::clone(&error)))
.unwrap();
assert_eq!(args.message(), "Something failed.");
assert!(Arc::ptr_eq(&args.exception().unwrap(), &error));
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceErrorEventArgs_WithoutException_HasNullException::test de7c8addfb291cba5dee890a98ac8b5f27b192d0b75894c4102303a49785de47 translated
#[test]
fn error_event_args_without_exception_has_none() {
let args = MarketplaceErrorEventArgs::new("No exception here.".into(), None).unwrap();
assert_eq!(args.message(), "No exception here.");
assert!(args.exception().is_none());
}
// parity-case: LibreMetaverse.Tests/MarketplaceManagerTests.cs::MarketplaceManagerTests.MarketplaceListing_DefaultValues_AreCorrect::test 70a29163581702fdc32c54a398ad4cefa608da8b1bc9d54a4e9e191e95398fb7 translated
#[test]
fn marketplace_listing_defaults_are_correct() {
let listing = MarketplaceListing::new().unwrap();
assert_eq!(listing.status(), MarketplaceListingStatus::Unknown);
assert_eq!(listing.last_sync_utc(), SystemTime::UNIX_EPOCH);
assert!(listing.edit_url().is_none());
}