use std::fs;
use std::path::PathBuf;
use hydracache_client_protocol::java_migration::{
java_exception_mapping, JavaClientRuntimeConfig, JavaClientTopology, JavaCodecDescriptor,
JavaCodecKind, JavaCodecRegistryContract, JavaExceptionKind, JavaMapOperation,
JavaMapProtocolFamily, JavaMigrationContractError, SpringCacheMode,
UnsupportedHazelcastApiManifest, JAVA_MIGRATION_CONTRACT_VERSION,
SUPPORTED_SPRING_BOOT_GENERATIONS,
};
use hydracache_client_protocol::{ClientErrorCode, ClientErrorEnvelope};
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.canonicalize()
.expect("repo root")
}
mod java_migration_contract {
use super::*;
#[test]
fn protocol_errors_map_to_documented_java_exception_kinds() {
let cases = [
(
ClientErrorCode::IncompatibleVersion,
false,
JavaExceptionKind::Protocol,
"HydraCacheProtocolException",
),
(
ClientErrorCode::Unauthenticated,
false,
JavaExceptionKind::Authentication,
"HydraCacheAuthenticationException",
),
(
ClientErrorCode::Unauthorized,
false,
JavaExceptionKind::Authorization,
"HydraCacheAuthorizationException",
),
(
ClientErrorCode::TenantQuota,
true,
JavaExceptionKind::QuotaExceeded,
"HydraCacheQuotaExceededException",
),
(
ClientErrorCode::RateLimited,
true,
JavaExceptionKind::RateLimited,
"HydraCacheRateLimitedException",
),
(
ClientErrorCode::ResidencyDenied,
false,
JavaExceptionKind::ResidencyDenied,
"HydraCacheResidencyDeniedException",
),
(
ClientErrorCode::TooLarge,
false,
JavaExceptionKind::PayloadTooLarge,
"HydraCachePayloadTooLargeException",
),
(
ClientErrorCode::DeadlineExceeded,
true,
JavaExceptionKind::Timeout,
"HydraCacheTimeoutException",
),
(
ClientErrorCode::Conflict,
false,
JavaExceptionKind::Conflict,
"HydraCacheConflictException",
),
(
ClientErrorCode::BackendUnavailable,
true,
JavaExceptionKind::BackendUnavailable,
"HydraCacheBackendUnavailableException",
),
(
ClientErrorCode::MalformedFrame,
false,
JavaExceptionKind::MalformedFrame,
"HydraCacheMalformedFrameException",
),
];
for (code, retryable, kind, class_name) in cases {
let error =
ClientErrorEnvelope::new(code, retryable, "redacted").with_retry_after_ms(25);
let mapping = java_exception_mapping(&error);
assert_eq!(mapping.kind, kind, "{code:?}");
assert_eq!(mapping.class_name, class_name, "{code:?}");
assert_eq!(mapping.retryable, retryable, "{code:?}");
assert!(mapping.preserves_request_id, "{code:?}");
assert!(mapping.preserves_retry_after, "{code:?}");
}
}
#[test]
fn codec_registry_contract_rejects_ambiguous_or_reflective_serializer() {
let user_codec =
JavaCodecDescriptor::explicit("user-profile-v1", "com.acme.UserProfile", 1).unwrap();
let mut registry = JavaCodecRegistryContract::new();
registry.register(user_codec.clone()).unwrap();
assert_eq!(registry.len(), 1);
assert_eq!(registry.get("user-profile-v1"), Some(&user_codec));
let duplicate =
JavaCodecDescriptor::explicit("user-profile-v1", "com.acme.OtherProfile", 1).unwrap();
let duplicate_error = registry.register(duplicate).unwrap_err();
assert_eq!(
duplicate_error,
JavaMigrationContractError::AmbiguousCodec {
codec_id: "user-profile-v1".to_owned()
}
);
let reflective = JavaCodecDescriptor::new(
"profile-reflective",
"com.acme.UserProfile",
1,
JavaCodecKind::ReflectiveFallback,
)
.unwrap();
assert_eq!(
JavaCodecRegistryContract::new()
.register(reflective)
.unwrap_err(),
JavaMigrationContractError::UnsupportedSerializer("reflective-fallback")
);
let native = JavaCodecDescriptor::new(
"profile-native",
"com.acme.UserProfile",
1,
JavaCodecKind::JavaNativeSerialization,
)
.unwrap();
assert_eq!(
JavaCodecRegistryContract::new()
.register(native)
.unwrap_err(),
JavaMigrationContractError::UnsupportedSerializer("java-native-serialization")
);
let bridge = JavaCodecDescriptor::new(
"legacy-user-profile",
"com.acme.LegacyUserProfile",
1,
JavaCodecKind::LegacySerializerBridge,
)
.unwrap();
assert_eq!(
JavaCodecRegistryContract::new()
.register(bridge.clone())
.unwrap_err(),
JavaMigrationContractError::LegacySerializerBridgeDisabled(
"legacy-user-profile".to_owned()
)
);
let mut migration_registry =
JavaCodecRegistryContract::new().with_legacy_serializer_bridge_enabled();
migration_registry.register(bridge).unwrap();
assert_eq!(migration_registry.len(), 1);
}
#[test]
fn unsupported_hazelcast_api_surface_is_a_checked_in_manifest() {
let manifest = UnsupportedHazelcastApiManifest::checked_in().unwrap();
assert_eq!(manifest.version, JAVA_MIGRATION_CONTRACT_VERSION);
assert!(manifest.entries.len() >= 8);
for api in [
"HazelcastInstance.getCPSubsystem",
"HazelcastInstance.getExecutorService",
"HazelcastInstance.getSql",
"IMap.lock",
"IMap.executeOnKey",
"ReplicatedMap",
"ReliableTopic",
"FencedLock",
] {
let entry = manifest.find(api).expect("manifest entry");
assert!(
!entry.migration_hint.trim().is_empty(),
"{api} should have a migration hint"
);
}
let future_error =
UnsupportedHazelcastApiManifest::parse("version=2\nIMap.lock|hint").unwrap_err();
assert_eq!(
future_error,
JavaMigrationContractError::UnsupportedManifestVersion {
actual: 2,
supported: JAVA_MIGRATION_CONTRACT_VERSION
}
);
let root = repo_root();
let manifest_file = fs::read_to_string(
root.join("crates/hydracache-client-protocol/manifests/unsupported_hazelcast_apis.txt"),
)
.expect("unsupported Hazelcast manifest");
let docs = fs::read_to_string(root.join("docs/integrations/java-migration.md"))
.expect("java migration docs");
let compat = fs::read_to_string(root.join("docs/COMPAT.md")).expect("compat");
assert!(manifest_file.contains("IMap.lock|"));
assert!(docs.contains("Java/Spring Migration Contract"));
assert!(docs.contains("HydraCacheMap<String, UserProfile>"));
assert!(docs.contains("mode: native"));
assert!(compat.contains("Java migration toolkit contract"));
assert!(compat.contains("unsupported_hazelcast_apis.txt"));
}
#[test]
fn java_runtime_config_modes_and_facade_contract_are_reviewable() {
assert_eq!(SUPPORTED_SPRING_BOOT_GENERATIONS, &[2, 3, 4]);
let config = JavaClientRuntimeConfig::client_first(
[
"https://cache-a.internal:8443",
"https://cache-b.internal:8443",
],
"core",
"gameservice",
);
config.validate().unwrap();
assert!(config.smart_routing);
assert!(config.customizer_hooks_enabled);
assert_eq!(config.deadline_ms, 5_000);
let mut member = config.clone();
member.topology = JavaClientTopology::Member;
assert_eq!(
member.validate().unwrap_err(),
JavaMigrationContractError::UnsupportedClientTopology("member")
);
assert!(SpringCacheMode::Native.lazy_dynamic_cache_names());
assert!(SpringCacheMode::JCache.requires_jcache_provider());
assert!(!SpringCacheMode::None.lazy_dynamic_cache_names());
assert_eq!(
JavaMapOperation::Get.protocol_family(),
JavaMapProtocolFamily::Get
);
assert_eq!(
JavaMapOperation::ContainsKey.protocol_family(),
JavaMapProtocolFamily::Get
);
assert_eq!(
JavaMapOperation::PutIfAbsent.protocol_family(),
JavaMapProtocolFamily::ConditionalPutIfAbsent
);
assert_eq!(
JavaMapOperation::Remove.protocol_family(),
JavaMapProtocolFamily::Invalidate
);
assert_eq!(
JavaMapOperation::GetAll.protocol_family(),
JavaMapProtocolFamily::BatchGet
);
assert_eq!(
JavaMapOperation::PutAll.protocol_family(),
JavaMapProtocolFamily::BatchPut
);
assert_eq!(
JavaMapOperation::EvictRegion.protocol_family(),
JavaMapProtocolFamily::EvictRegion
);
}
}