use uri_register::{ConfigurationError, PostgresUriRegister, UriService};
mod common;
use common::{get_database_url, get_table_name};
async fn setup() -> PostgresUriRegister {
let db_url = get_database_url();
let table_name = get_table_name();
PostgresUriRegister::new(&db_url, &table_name, 2, 10_000)
.await
.expect(
"Failed to connect to database. Ensure PostgreSQL is running and schema is initialized.",
)
}
#[tokio::test]
async fn test_register_uri_basic() {
let register = setup().await;
let uri = format!("http://example.org/test/{}", uuid::Uuid::new_v4());
let id1 = register
.register_uri(&uri)
.await
.expect("Failed to register URI");
let id2 = register
.register_uri(&uri)
.await
.expect("Failed to register URI again");
assert_eq!(id1, id2, "Same URI should return same ID");
}
#[tokio::test]
async fn test_register_uri_multiple_different() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uri1 = format!("http://example.org/test/{}/1", uuid);
let uri2 = format!("http://example.org/test/{}/2", uuid);
let id1 = register
.register_uri(&uri1)
.await
.expect("Failed to register URI 1");
let id2 = register
.register_uri(&uri2)
.await
.expect("Failed to register URI 2");
assert_ne!(id1, id2, "Different URIs should have different IDs");
}
#[tokio::test]
async fn test_register_uri_batch_basic() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uris = vec![
format!("http://example.org/batch/{}/1", uuid),
format!("http://example.org/batch/{}/2", uuid),
format!("http://example.org/batch/{}/3", uuid),
];
let ids = register
.register_uri_batch(&uris)
.await
.expect("Failed to batch register URIs");
assert_eq!(ids.len(), 3, "Should return 3 IDs");
let unique_ids: std::collections::HashSet<_> = ids.iter().copied().collect();
assert_eq!(unique_ids.len(), 3, "All IDs should be unique");
}
#[tokio::test]
async fn test_register_uri_batch_order_preservation() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uris = vec![
format!("http://example.org/order/{}/1", uuid),
format!("http://example.org/order/{}/2", uuid),
format!("http://example.org/order/{}/3", uuid),
];
let batch_ids = register
.register_uri_batch(&uris)
.await
.expect("Failed to batch register");
for (i, uri) in uris.iter().enumerate() {
let individual_id = register
.register_uri(uri)
.await
.expect("Failed to register individually");
assert_eq!(
batch_ids[i], individual_id,
"Batch ID at index {} should match individual registration",
i
);
}
}
#[tokio::test]
async fn test_register_uri_batch_with_existing() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uris1 = vec![
format!("http://example.org/existing/{}/1", uuid),
format!("http://example.org/existing/{}/2", uuid),
];
let ids1 = register
.register_uri_batch(&uris1)
.await
.expect("Failed to register first batch");
let uris2 = vec![
format!("http://example.org/existing/{}/2", uuid), format!("http://example.org/existing/{}/3", uuid), ];
let ids2 = register
.register_uri_batch(&uris2)
.await
.expect("Failed to register second batch");
assert_eq!(ids1[1], ids2[0], "Existing URI should return same ID");
}
#[tokio::test]
async fn test_register_uri_batch_empty() {
let register = setup().await;
let ids = register
.register_uri_batch(&[])
.await
.expect("Failed to handle empty batch");
assert_eq!(ids.len(), 0, "Empty batch should return empty result");
}
#[tokio::test]
async fn test_register_uri_batch_large() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let batch_size = 1000;
let uris: Vec<String> = (0..batch_size)
.map(|i| format!("http://example.org/large/{}/{}", uuid, i))
.collect();
let ids = register
.register_uri_batch(&uris)
.await
.expect("Failed to process large batch");
assert_eq!(ids.len(), batch_size, "Should process all URIs");
let unique_ids: std::collections::HashSet<_> = ids.iter().copied().collect();
assert_eq!(unique_ids.len(), batch_size, "All IDs should be unique");
for (i, uri) in uris.iter().enumerate() {
let individual_id = register
.register_uri(uri)
.await
.expect("Failed to register");
assert_eq!(
ids[i], individual_id,
"Order should be preserved for URI at index {}",
i
);
}
}
#[tokio::test]
async fn test_special_characters() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let special_uris = vec![
format!("http://example.org/special/{}/with spaces", uuid),
format!(
"http://example.org/special/{}/with?query=params&foo=bar",
uuid
),
format!("http://example.org/special/{}/with#fragment", uuid),
format!("http://example.org/special/{}/with/slash/", uuid),
format!("http://example.org/special/{}/emoji-😀-test", uuid),
];
let ids = register
.register_uri_batch(&special_uris)
.await
.expect("Failed to handle special characters");
assert_eq!(
ids.len(),
special_uris.len(),
"Should handle all special characters"
);
for (i, uri) in special_uris.iter().enumerate() {
let retrieved_id = register
.register_uri(uri)
.await
.expect("Failed to retrieve");
assert_eq!(
ids[i], retrieved_id,
"Special character URI should round-trip correctly"
);
}
}
#[tokio::test]
async fn test_very_long_uri() {
let register = setup().await;
let long_path = "a".repeat(2000);
let uri = format!("http://example.org/{}/{}", uuid::Uuid::new_v4(), long_path);
let id1 = register
.register_uri(&uri)
.await
.expect("Failed to handle long URI");
let id2 = register
.register_uri(&uri)
.await
.expect("Failed to retrieve long URI");
assert_eq!(id1, id2, "Long URI should round-trip correctly");
}
#[tokio::test]
async fn test_concurrent_access() {
let _register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uri = format!("http://example.org/concurrent/{}", uuid);
let mut handles = vec![];
for _ in 0..10 {
let register_clone =
PostgresUriRegister::new(&get_database_url(), &get_table_name(), 20, 10_000)
.await
.expect("Failed to create register");
let uri_clone = uri.clone();
let handle = tokio::spawn(async move {
register_clone
.register_uri(&uri_clone)
.await
.expect("Failed to register URI")
});
handles.push(handle);
}
let mut ids = vec![];
for handle in handles {
let id = handle.await.expect("Task panicked");
ids.push(id);
}
let unique_ids: std::collections::HashSet<u64> = ids.into_iter().collect();
assert_eq!(
unique_ids.len(),
1,
"Concurrent access should return same ID"
);
}
#[tokio::test]
async fn test_concurrent_batch_operations() {
let _register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uris: Vec<String> = (0..100)
.map(|i| format!("http://example.org/concurrent-batch/{}/{}", uuid, i))
.collect();
let mut handles = vec![];
for _ in 0..5 {
let register_clone =
PostgresUriRegister::new(&get_database_url(), &get_table_name(), 20, 10_000)
.await
.expect("Failed to create register");
let uris_clone = uris.clone();
let handle = tokio::spawn(async move {
register_clone
.register_uri_batch(&uris_clone)
.await
.expect("Failed to batch register")
});
handles.push(handle);
}
let mut results = vec![];
for handle in handles {
let result = handle.await.expect("Task panicked");
results.push(result);
}
let first_result = &results[0];
for result in &results[1..] {
assert_eq!(
result, first_result,
"Concurrent batches should produce consistent results"
);
}
}
#[tokio::test]
async fn test_cache_behavior() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uri = format!("http://example.org/cache/{}", uuid);
let id1 = register
.register_uri(&uri)
.await
.expect("Failed to register URI");
let id2 = register
.register_uri(&uri)
.await
.expect("Failed to get cached URI");
assert_eq!(id1, id2, "Cache should return same ID");
let batch_result = register
.register_uri_batch(std::slice::from_ref(&uri))
.await
.expect("Failed to batch register cached URI");
assert_eq!(
batch_result[0], id1,
"Batch operation should return cached ID"
);
}
#[tokio::test]
async fn test_stats() {
let register = setup().await;
let stats = register.stats().await.expect("Failed to get stats");
assert!(stats.size_bytes > 0, "Size should be positive");
}
#[tokio::test]
async fn test_register_uri_batch_with_duplicates_in_input() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uri1 = format!("http://example.org/dup/{}/1", uuid);
let uri2 = format!("http://example.org/dup/{}/2", uuid);
let uris = vec![uri1.clone(), uri2.clone(), uri1.clone()];
let ids = register
.register_uri_batch(&uris)
.await
.expect("Failed to batch register with duplicates");
assert_eq!(ids.len(), 3, "Should return ID for each input");
assert_eq!(ids[0], ids[2], "Duplicate URIs should have same ID");
assert_ne!(ids[0], ids[1], "Different URIs should have different IDs");
}
#[tokio::test]
async fn test_register_uri_batch_hashmap_basic() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uris = vec![
format!("http://example.org/hashmap/{}/1", uuid),
format!("http://example.org/hashmap/{}/2", uuid),
format!("http://example.org/hashmap/{}/3", uuid),
];
let map = register
.register_uri_batch_hashmap(&uris)
.await
.expect("Failed to batch register hashmap");
assert_eq!(map.len(), 3, "Should return 3 mappings");
for uri in &uris {
assert!(map.contains_key(uri), "Map should contain URI: {}", uri);
}
let unique_ids: std::collections::HashSet<_> = map.values().copied().collect();
assert_eq!(unique_ids.len(), 3, "All IDs should be unique");
}
#[tokio::test]
async fn test_register_uri_batch_hashmap_with_duplicates() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uri1 = format!("http://example.org/hashmap-dup/{}/1", uuid);
let uri2 = format!("http://example.org/hashmap-dup/{}/2", uuid);
let uris = vec![uri1.clone(), uri2.clone(), uri1.clone()];
let map = register
.register_uri_batch_hashmap(&uris)
.await
.expect("Failed to batch register hashmap with duplicates");
assert_eq!(map.len(), 2, "Duplicates should be removed");
assert!(map.contains_key(&uri1));
assert!(map.contains_key(&uri2));
}
#[tokio::test]
async fn test_register_uri_batch_hashmap_with_existing() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uri1 = format!("http://example.org/hashmap-existing/{}/1", uuid);
let uri2 = format!("http://example.org/hashmap-existing/{}/2", uuid);
let uri3 = format!("http://example.org/hashmap-existing/{}/3", uuid);
let uris1 = vec![uri1.clone(), uri2.clone()];
let map1 = register
.register_uri_batch_hashmap(&uris1)
.await
.expect("Failed to register first batch");
let uris2 = vec![uri2.clone(), uri3.clone()];
let map2 = register
.register_uri_batch_hashmap(&uris2)
.await
.expect("Failed to register second batch");
assert_eq!(
map1.get(&uri2),
map2.get(&uri2),
"Existing URI should return same ID"
);
let single_id = register
.register_uri(&uri2)
.await
.expect("Failed to register URI");
assert_eq!(
map1.get(&uri2),
Some(&single_id),
"ID should match single registration"
);
}
#[tokio::test]
async fn test_register_uri_batch_hashmap_empty() {
let register = setup().await;
let map = register
.register_uri_batch_hashmap(&[])
.await
.expect("Failed to handle empty hashmap batch");
assert_eq!(map.len(), 0, "Empty batch should return empty map");
}
#[tokio::test]
async fn test_register_uri_batch_hashmap_large() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let batch_size = 1000;
let uris: Vec<String> = (0..batch_size)
.map(|i| format!("http://example.org/hashmap-large/{}/{}", uuid, i))
.collect();
let map = register
.register_uri_batch_hashmap(&uris)
.await
.expect("Failed to process large hashmap batch");
assert_eq!(map.len(), batch_size, "Should process all URIs");
for uri in &uris {
assert!(map.contains_key(uri), "Map should contain all URIs");
}
let unique_ids: std::collections::HashSet<_> = map.values().copied().collect();
assert_eq!(unique_ids.len(), batch_size, "All IDs should be unique");
}
#[tokio::test]
async fn test_register_uri_batch_order_with_sql_result_reordering() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uris = vec![
format!("http://example.org/order-test/{}/z-last", uuid),
format!("http://example.org/order-test/{}/a-first", uuid),
format!("http://example.org/order-test/{}/m-middle", uuid),
format!("http://example.org/order-test/{}/z-last", uuid), format!("http://example.org/order-test/{}/a-first", uuid), ];
let ids = register
.register_uri_batch(&uris)
.await
.expect("Failed to batch register");
assert_eq!(ids.len(), 5, "Should return ID for each input position");
for (idx, uri) in uris.iter().enumerate() {
let individual_id = register
.register_uri(uri)
.await
.expect("Failed to register individually");
assert_eq!(
ids[idx], individual_id,
"ID at position {} should match individual registration for URI: {}",
idx, uri
);
}
assert_eq!(
ids[0], ids[3],
"Duplicate URIs at positions 0 and 3 should have same ID"
);
assert_eq!(
ids[1], ids[4],
"Duplicate URIs at positions 1 and 4 should have same ID"
);
assert_ne!(ids[0], ids[1], "Different URIs should have different IDs");
assert_ne!(ids[0], ids[2], "Different URIs should have different IDs");
assert_ne!(ids[1], ids[2], "Different URIs should have different IDs");
}
#[tokio::test]
async fn test_register_uri_batch_hashmap_correctness() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uris = vec![
format!("http://example.org/hashmap-correct/{}/uri1", uuid),
format!("http://example.org/hashmap-correct/{}/uri2", uuid),
format!("http://example.org/hashmap-correct/{}/uri3", uuid),
];
let map = register
.register_uri_batch_hashmap(&uris)
.await
.expect("Failed to batch register hashmap");
for uri in &uris {
let individual_id = register
.register_uri(uri)
.await
.expect("Failed to register individually");
assert_eq!(
map.get(uri),
Some(&individual_id),
"HashMap should map URI '{}' to correct ID {}",
uri,
individual_id
);
}
}
#[tokio::test]
async fn test_invalid_uri_validation() {
let register = setup().await;
let invalid_uris = vec![
"not a uri",
"://missing-scheme",
"http://",
"",
"just-a-string",
"ftp://[invalid",
];
for invalid_uri in invalid_uris {
let result = register.register_uri(invalid_uri).await;
assert!(
result.is_err(),
"Invalid URI '{}' should be rejected",
invalid_uri
);
if let Err(e) = result {
assert!(
matches!(e, uri_register::Error::InvalidUri(_)),
"Error should be InvalidUri, got: {:?}",
e
);
}
}
}
#[tokio::test]
async fn test_valid_uri_validation() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let valid_uris = vec![
format!("http://example.org/{}", uuid),
format!("https://example.org/path/{}", uuid),
format!("ftp://ftp.example.org/file-{}.txt", uuid),
format!("http://example.org:8080/path?query={}", uuid),
format!("https://user:pass@example.org/path/{}#fragment", uuid),
format!("file:///path/to/file-{}", uuid),
];
for valid_uri in &valid_uris {
let result = register.register_uri(valid_uri).await;
assert!(
result.is_ok(),
"Valid URI '{}' should be accepted, got error: {:?}",
valid_uri,
result.err()
);
}
}
#[tokio::test]
async fn test_invalid_uri_batch_validation() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uris = vec![
format!("http://valid.org/{}", uuid),
"invalid uri".to_string(), format!("http://another-valid.org/{}", uuid),
];
let result = register.register_uri_batch(&uris).await;
assert!(result.is_err(), "Batch with invalid URI should fail");
if let Err(e) = result {
assert!(
matches!(e, uri_register::Error::InvalidUri(_)),
"Error should be InvalidUri, got: {:?}",
e
);
}
}
#[tokio::test]
async fn test_invalid_uri_batch_hashmap_validation() {
let register = setup().await;
let uuid = uuid::Uuid::new_v4();
let uris = vec![
format!("http://valid.org/{}", uuid),
"not-a-valid-uri".to_string(), ];
let result = register.register_uri_batch_hashmap(&uris).await;
assert!(
result.is_err(),
"Batch hashmap with invalid URI should fail"
);
if let Err(e) = result {
assert!(
matches!(e, uri_register::Error::InvalidUri(_)),
"Error should be InvalidUri, got: {:?}",
e
);
}
}
#[tokio::test]
async fn test_configuration_validation_zero_cache_size() {
let db_url = get_database_url();
let table_name = get_table_name();
let result = PostgresUriRegister::new(&db_url, &table_name, 20, 0).await;
assert!(
result.is_err(),
"Creating register with cache_size=0 should fail"
);
if let Err(e) = result {
assert!(
matches!(
e,
uri_register::Error::Configuration(ConfigurationError::InvalidCacheSize(0))
),
"Error should be Configuration::InvalidCacheSize(0), got: {:?}",
e
);
}
}
#[tokio::test]
async fn test_configuration_validation_zero_max_connections() {
let db_url = get_database_url();
let table_name = get_table_name();
let result = PostgresUriRegister::new(&db_url, &table_name, 0, 10_000).await;
assert!(
result.is_err(),
"Creating register with max_connections=0 should fail"
);
if let Err(e) = result {
assert!(
matches!(
e,
uri_register::Error::Configuration(ConfigurationError::InvalidMaxConnections(0))
),
"Error should be Configuration::InvalidMaxConnections(0), got: {:?}",
e
);
}
}
#[tokio::test]
async fn test_configuration_validation_valid_parameters() {
let db_url = get_database_url();
let table_name = get_table_name();
let test_cases = vec![
(1, 1), (10, 1000), (50, 10_000), (100, 100_000), ];
for (max_conn, cache) in test_cases {
let result = PostgresUriRegister::new(&db_url, &table_name, max_conn, cache).await;
assert!(
result.is_ok(),
"Valid parameters (max_connections={}, cache_size={}) should succeed, got: {:?}",
max_conn,
cache,
result.err()
);
}
}
#[tokio::test]
async fn test_table_name_validation() {
let db_url = get_database_url();
let result = PostgresUriRegister::new(&db_url, "", 20, 10_000).await;
assert!(result.is_err(), "Empty table name should fail");
if let Err(e) = result {
assert!(
matches!(
e,
uri_register::Error::Configuration(ConfigurationError::InvalidTableName(_))
),
"Should be InvalidTableName error"
);
}
let result = PostgresUriRegister::new(&db_url, "1_invalid", 20, 10_000).await;
assert!(
result.is_err(),
"Table name starting with digit should fail"
);
let result = PostgresUriRegister::new(&db_url, "invalid-name", 20, 10_000).await;
assert!(result.is_err(), "Table name with hyphens should fail");
let result = PostgresUriRegister::new(&db_url, "invalid.name", 20, 10_000).await;
assert!(result.is_err(), "Table name with dots should fail");
let result = PostgresUriRegister::new(&db_url, "invalid name", 20, 10_000).await;
assert!(result.is_err(), "Table name with spaces should fail");
let long_name = "a".repeat(64);
let result = PostgresUriRegister::new(&db_url, &long_name, 20, 10_000).await;
assert!(result.is_err(), "Table name > 63 characters should fail");
let valid_names = vec![
"uri_register",
"_private_table",
"Table123",
"UPPERCASE",
"MixedCase_123",
];
for table_name in valid_names {
let result = PostgresUriRegister::new(&db_url, table_name, 20, 10_000).await;
assert!(
result.is_ok() || matches!(result, Err(uri_register::Error::Database(_))),
"Valid table name '{}' should pass validation",
table_name
);
}
}