Files
MetaCrate/tests/compat/tests/task_inventory_semantics.rs
Chili Palmer 1ea44e5e42
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m51s
CI / required (push) Failing after 28m9s
fix(ci): close capability races and green audit
2026-08-13 09:43:42 +00:00

245 lines
9.4 KiB
Rust

// Exact Rust translations of LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs.
// Source SHA-256: f444adc7615a805eddeef538ce939375dc2a2fa2cd871aab2ebce37b4e955941
use libremetaverse::{
GridClient, HttpCapsClient, InventoryFolder, InventoryItem, InventoryManager,
InventoryObjectClass, Simulator,
};
use libremetaverse_compat_tests::block_on;
use libremetaverse_types::UUID;
use libremetaverse_types::compat::{
CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use std::time::Duration;
const CAP: &str = "http://test.invalid/request-task-inventory";
const SEED: &str = "http://test.invalid/seed";
#[derive(Clone)]
struct Recording(Arc<Mutex<Vec<HttpRequest>>>);
impl Recording {
fn requests(&self) -> Vec<HttpRequest> {
self.0.lock().unwrap().clone()
}
}
fn client_with_cap(response: HttpResponse, expose_capability: bool) -> (GridClient, Recording) {
let requests = Arc::new(Mutex::new(Vec::new()));
let recorded = Arc::clone(&requests);
let response = Mutex::new(Some(response));
let seed = if expose_capability {
format!("<llsd><map><key>RequestTaskInventory</key><uri>{CAP}</uri></map></llsd>")
} else {
"<llsd><map></map></llsd>".into()
};
let release_seed = Arc::new(AtomicBool::new(false));
let handler_release_seed = Arc::clone(&release_seed);
let handler = HttpMessageHandler::new(move |request, _| {
let is_seed = request.uri.0 == SEED;
let result = if is_seed {
HttpResponse {
status_code: 200,
headers: BTreeMap::new(),
content_type: Some("application/llsd+xml".into()),
body: seed.as_bytes().to_vec(),
}
} else {
recorded.lock().unwrap().push(request);
response.lock().unwrap().take().unwrap_or(HttpResponse {
status_code: 404,
headers: BTreeMap::new(),
content_type: None,
body: Vec::new(),
})
};
let handler_release_seed = Arc::clone(&handler_release_seed);
async move {
if is_seed {
while !handler_release_seed.load(Ordering::Acquire) {
tokio::task::yield_now().await;
}
}
result
}
});
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:0".parse().unwrap(),
0,
None,
None,
)
.unwrap();
simulator
.set_seed_caps(Some(Uri(SEED.into())), Some(true))
.unwrap();
let caps = simulator.clone().caps.expect("seed capability client");
let (seed_complete, completion) = mpsc::channel();
let _subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |_| {
seed_complete.send(()).unwrap();
})));
release_seed.store(true, Ordering::Release);
completion
.recv_timeout(Duration::from_secs(2))
.expect("seed capability request completed");
let mut network = client.network();
network.set_current_sim(Some(simulator));
client.set_network(network);
(client, Recording(requests))
}
fn response(status: u16, body: String) -> HttpResponse {
HttpResponse {
status_code: status,
headers: BTreeMap::new(),
content_type: Some("application/json".into()),
body: body.into_bytes(),
}
}
fn uuid() -> UUID {
UUID::random().expect("UUID Random")
}
fn item_json(
item_id: UUID,
parent_id: UUID,
asset_or_shadow_key: &str,
asset_or_shadow_value: UUID,
) -> String {
format!(
concat!(
"{{\"item_id\":\"{}\",\"parent_id\":\"{}\",",
"\"name\":\"Test Item\",\"desc\":\"A test item\",",
"\"type\":7,\"inv_type\":7,\"flags\":0,\"created_at\":1700000000,",
"\"agent_id\":\"11111111-1111-1111-1111-111111111111\",",
"\"{}\":\"{}\",\"permissions\":{{",
"\"creator_id\":\"11111111-1111-1111-1111-111111111111\",",
"\"owner_id\":\"11111111-1111-1111-1111-111111111111\",",
"\"last_owner_id\":\"11111111-1111-1111-1111-111111111111\",",
"\"group_id\":\"00000000-0000-0000-0000-000000000000\",",
"\"base_mask\":2147483647,\"owner_mask\":2147483647,",
"\"group_mask\":0,\"everyone_mask\":0,\"next_owner_mask\":2147483647,",
"\"is_owner_group\":false}},\"sale_info\":{{\"sale_price\":0,\"sale_type\":0}}}}"
),
item_id.to_string(),
parent_id.to_string(),
asset_or_shadow_key,
asset_or_shadow_value.to_string(),
)
}
fn single<T: 'static>(entries: &[Box<dyn InventoryObjectClass>]) -> &T {
let mut matching = entries
.iter()
.filter_map(|entry| entry.as_any().downcast_ref::<T>());
let value = matching.next().expect("matching inventory object");
assert!(matching.next().is_none(), "expected exactly one object");
value
}
// parity-case: LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs::RequestTaskInventoryCapTests.GetTaskInventoryAsync_CapAvailable_ReturnsContentsFolderAndDecryptsShadowId::test c9bfc5526260f9e2dffa8a22938999911fdd80be4b2b6d6487bf6171f8dbb56a translated
#[test]
fn cap_available_returns_contents_folder_and_decrypts_shadow_id() {
let object_id = uuid();
let item_id = uuid();
let asset_id = uuid();
let shadow_id = InventoryManager::encrypt_asset_id(asset_id).unwrap();
let body = format!(
"{{\"inventory_serial\":3,\"contents\":[{}]}}",
item_json(item_id, object_id, "shadow_id", shadow_id)
);
let (client, recording) = client_with_cap(response(200, body), true);
let result = block_on(
client
.inventory()
.get_task_inventory(object_id, 12_345, None, None),
)
.expect("GetTaskInventoryAsync");
let requests = recording.requests();
assert_eq!(requests.len(), 1);
assert!(
requests[0]
.uri
.0
.contains(&format!("task_id={}", object_id.to_string()))
);
assert_eq!(result.len(), 2);
let folder = single::<InventoryFolder>(&result);
assert_eq!(folder.base.name(), "Contents");
assert_eq!(folder.base.uuid(), object_id);
let item = single::<InventoryItem>(&result);
assert_eq!(item.base.uuid(), item_id);
assert_eq!(item.base.parent_uuid(), object_id);
assert_eq!(item.base.name(), "Test Item");
assert_eq!(item.asset_uuid(), asset_id);
}
// parity-case: LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs::RequestTaskInventoryCapTests.GetTaskInventoryAsync_PlainAssetId_ParsesWithoutDecryption::test 95f39928dae61bca86b7d0a0702307db6c5fdc754e51f73a24bd5ee131e6de3f translated
#[test]
fn plain_asset_id_parses_without_decryption() {
let object_id = uuid();
let item_id = uuid();
let asset_id = uuid();
let body = format!(
"{{\"inventory_serial\":1,\"contents\":[{}]}}",
item_json(item_id, object_id, "asset_id", asset_id)
);
let (client, _) = client_with_cap(response(200, body), true);
let result = block_on(
client
.inventory()
.get_task_inventory(object_id, 12_345, None, None),
)
.expect("GetTaskInventoryAsync");
assert_eq!(single::<InventoryItem>(&result).asset_uuid(), asset_id);
}
// parity-case: LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs::RequestTaskInventoryCapTests.GetTaskInventoryAsync_NonSuccessStatus_ReturnsContentsFolderOnly::test b8a25891064e470ac8d77ecd8a4847cf773b043de5ebeae275a9dd6669518585 translated
#[test]
fn non_success_status_returns_contents_folder_only() {
let object_id = uuid();
let (client, _) = client_with_cap(response(500, String::new()), true);
let result = block_on(
client
.inventory()
.get_task_inventory(object_id, 12_345, None, None),
)
.expect("GetTaskInventoryAsync");
assert_eq!(result.len(), 1);
assert!(result[0].as_any().is::<InventoryFolder>());
}
// parity-case: LibreMetaverse.Tests/RequestTaskInventoryCapTests.cs::RequestTaskInventoryCapTests.GetTaskInventoryAsync_NoCapability_DoesNotMakeHttpRequest::test c0c43ebcf781fb81b1fef7ce1dd2774e350d92ef882f49876f1a16d23a338592 translated
#[test]
fn no_capability_does_not_make_http_request() {
let (client, recording) = client_with_cap(response(404, String::new()), false);
let cancellation = CancellationTokenSource::new();
let token = cancellation.token();
let inventory = client.inventory();
let (result, ()) = block_on(async move {
tokio::join!(
inventory.get_task_inventory(uuid(), 12_345, None, Some(token)),
async move {
// `join!` polls the inventory branch before this branch can
// resume after yielding, so cancellation releases the legacy
// callback wait instead of racing seed discovery or entry.
tokio::task::yield_now().await;
cancellation.cancel();
}
)
});
let result = result.expect("GetTaskInventoryAsync");
assert!(result.is_empty());
assert!(recording.requests().is_empty());
}