diff --git a/packages/infra/engine/tests/actors_create.rs b/packages/infra/engine/tests/actors_create.rs index ac81ade503..4ae329f433 100644 --- a/packages/infra/engine/tests/actors_create.rs +++ b/packages/infra/engine/tests/actors_create.rs @@ -1,7 +1,10 @@ mod common; +use base64::Engine; use serde_json::json; +const MAX_INPUT_SIZE: usize = rivet_util::file_size::mebibytes(4) as usize; + // MARK: Basic #[test] fn create_actor_valid_namespace() { @@ -169,13 +172,14 @@ fn create_actor_specific_datacenter() { let actor_id = common::create_actor_with_options( common::CreateActorOptions { namespace: namespace.clone(), - datacenter: Some("dc-2".to_string()), ..Default::default() }, - ctx.leader_dc().guard_port(), + ctx.get_dc(2).guard_port(), ) .await; + common::wait_for_actor_propagation(&"foo", 1).await; + assert!(!actor_id.is_empty(), "Actor ID should not be empty"); let actor = @@ -196,7 +200,6 @@ fn create_actor_current_datacenter() { let actor_id = common::create_actor_with_options( common::CreateActorOptions { namespace: namespace.clone(), - datacenter: None, ..Default::default() }, ctx.leader_dc().guard_port(), @@ -223,42 +226,31 @@ fn create_actor_non_existent_namespace() { }); } -#[test] -#[should_panic(expected = "Failed to create actor")] -fn create_actor_invalid_datacenter() { - common::run(common::TestOpts::new(1), |ctx| async move { - let (namespace, _, _runner) = - common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - - common::create_actor_with_options( - common::CreateActorOptions { - namespace: namespace.clone(), - datacenter: Some("invalid-dc".to_string()), - ..Default::default() - }, - ctx.leader_dc().guard_port(), - ) - .await; - }); -} - #[test] fn create_actor_malformed_input() { common::run(common::TestOpts::new(1), |ctx| async move { let (namespace, _, _runner) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - let actor_id = common::create_actor_with_options( - common::CreateActorOptions { - namespace: namespace.clone(), - input: Some("not-valid-base64!@#$%".to_string()), - ..Default::default() - }, - ctx.leader_dc().guard_port(), - ) - .await; + let client = reqwest::Client::new(); + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&serde_json::json!({ + "name": "test", + "input": "not-valid-base64!@#$%", + })) + .send() + .await + .expect("Failed to send request"); - assert!(!actor_id.is_empty(), "Actor ID should not be empty"); + assert!( + !response.status().is_success(), + "Should fail with invalid base64 input" + ); }); } @@ -269,17 +261,17 @@ fn create_actor_remote_datacenter_verify() { let (namespace, _, _runner) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + // common::wait_for_actor_propagation(&"", 1).await; let actor_id = common::create_actor_with_options( common::CreateActorOptions { namespace: namespace.clone(), - datacenter: Some("dc-2".to_string()), ..Default::default() }, - ctx.leader_dc().guard_port(), + ctx.get_dc(2).guard_port(), ) .await; - common::wait_for_actor_propagation(&actor_id, 1).await; + // common::wait_for_actor_propagation(&actor_id, 1).await; let actor = common::assert_actor_exists(&actor_id, &namespace, ctx.get_dc(2).guard_port()).await; @@ -290,6 +282,34 @@ fn create_actor_remote_datacenter_verify() { }); } +// MARK: Namespace validation +#[test] +fn create_actor_namespace_validation() { + common::run(common::TestOpts::new(1), |ctx| async move { + let non_existent_ns = "non-existent-namespace"; + let api_port = ctx.leader_dc().guard_port(); + let client = reqwest::Client::new(); + + // POST /actors + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + api_port, non_existent_ns + )) + .json(&json!({ + "name": "test", + "key": "key", + })) + .send() + .await + .expect("Failed to send request"); + assert!( + !response.status().is_success(), + "POST /actors should fail with non-existent namespace" + ); + }); +} + // MARK: Edge cases #[test] @@ -353,15 +373,70 @@ fn empty_strings_for_required_parameters() { }); } + +#[test] +fn test_long_strings_for_input() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test different base64 encoded inputs + let large_string = "A".repeat(MAX_INPUT_SIZE + 1); + let base64_tests = vec![ + ("normal", "AAAA", true), + ("very-large", rivet_util::safe_slice(&large_string, 0, MAX_INPUT_SIZE-1), true), // Within bounds + ("too-large", &large_string, false), // Out of bounds base64 string + ]; + + for (name, base64_input, should_work) in base64_tests { + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&json!({ + "name": format!("base64-{}", name), + "input": base64_input, + "runner_name_selector": "foo", + "crash_policy": "destroy", + })) + .send() + .await + .expect(&format!("Failed to send request for {}", name)); + + if should_work && base64::engine::general_purpose::STANDARD.decode(base64_input).is_ok() { + // Valid base64 should work + assert!( + response.status().is_success(), + "Valid base64 '{}' should succeed, but instead got {}", + name, + response.text().await.unwrap() + ); + } else { + // Invalid base64 should fail + assert!( + !response.status().is_success(), + "Invalid base64 '{}' should fail", + name + ); + } + } + }); +} + + #[test] fn very_long_strings_for_names_and_key() { common::run(common::TestOpts::new(1), |ctx| async move { let (namespace, _, _runner) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - // Create very long name and key (should work up to reasonable limits) - let long_name = "a".repeat(255); // 255 chars should be acceptable - let long_key = "k".repeat(255); + // Create name and key with exactly 32 chars (should work) + let long_name = "a".repeat(32); // 32 chars should be acceptable + let long_key = "k".repeat(32); let actor_id = common::create_actor_with_options( common::CreateActorOptions { @@ -380,8 +455,8 @@ fn very_long_strings_for_names_and_key() { assert_eq!(actor["actor"]["name"], long_name); assert_eq!(actor["actor"]["key"], long_key); - // Try extremely long name (should fail) - let too_long_name = "a".repeat(1000); + // Try name with 33 chars (should fail) + let too_long_name = "a".repeat(33); let client = reqwest::Client::new(); let response = client .post(&format!( @@ -398,12 +473,33 @@ fn very_long_strings_for_names_and_key() { .expect("Failed to send request"); assert!( !response.status().is_success(), - "Should fail with extremely long name" + "Should fail with 33-character name" + ); + + // Try key with 33 chars (should fail) + let too_long_key = "k".repeat(33); + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&json!({ + "name": "test", + "key": too_long_key, + })) + .send() + .await + .expect("Failed to send request"); + assert!( + !response.status().is_success(), + "Should fail with 33-character key" ); }); } #[test] +#[ignore] fn special_characters_in_names_and_keys() { common::run(common::TestOpts::new(1), |ctx| async move { let (namespace, _, _runner) = @@ -522,3 +618,120 @@ fn maximum_limits_32_actor_ids_in_list() { ); }); } + +// MARK: Key collision tests + +#[test] +fn create_destroy_create_destroy_same_key_single_dc() { + common::run(common::TestOpts::new(2), |ctx| async move { + create_destroy_create_destroy_same_key_inner(ctx.leader_dc(), ctx.leader_dc()).await; + }); +} + +#[test] +#[ignore] +fn create_destroy_create_destroy_same_key_multi_dc() { + common::run(common::TestOpts::new(2), |ctx| async move { + create_destroy_create_destroy_same_key_inner(ctx.get_dc(2), ctx.get_dc(2)).await; + }); +} + +#[test] +#[ignore] +fn create_destroy_create_destroy_same_key_different_dc() { + common::run(common::TestOpts::new(2), |ctx| async move { + create_destroy_create_destroy_same_key_inner(ctx.leader_dc(), ctx.get_dc(2)).await; + }); +} + +async fn create_destroy_create_destroy_same_key_inner( + target_dc1: &common::TestDatacenter, + target_dc2: &common::TestDatacenter, +) { + let (namespace, _, runner) = common::setup_test_namespace_with_runner(target_dc1).await; + let key = rand::random::().to_string(); + + // First create/destroy cycle + let actor_id1 = common::create_actor_with_options( + common::CreateActorOptions { + namespace: namespace.clone(), + key: Some(key.clone()), + ..Default::default() + }, + target_dc1.guard_port(), + ) + .await; + + common::assert_actor_in_dc(&actor_id1, target_dc1.config.dc_label()).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + // Destroy first actor + tracing::info!(?actor_id1, "destroying first actor"); + common::destroy_actor(&actor_id1, &namespace, target_dc1.guard_port()).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + // Second create/destroy cycle with same key + let actor_id2 = common::create_actor_with_options( + common::CreateActorOptions { + namespace: namespace.clone(), + key: Some(key.clone()), + ..Default::default() + }, + target_dc2.guard_port(), + ) + .await; + + assert_ne!(actor_id1, actor_id2, "same actor id after first cycle"); + common::assert_actor_in_dc(&actor_id2, target_dc1.config.dc_label()).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + // Destroy second actor + tracing::info!(?actor_id2, "destroying second actor"); + common::destroy_actor(&actor_id2, &namespace, target_dc2.guard_port()).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + // Third create/destroy cycle with same key + let actor_id3 = common::create_actor_with_options( + common::CreateActorOptions { + namespace: namespace.clone(), + key: Some(key.clone()), + ..Default::default() + }, + target_dc1.guard_port(), + ) + .await; + + assert_ne!(actor_id1, actor_id3, "same actor id after second cycle (vs first)"); + assert_ne!(actor_id2, actor_id3, "same actor id after second cycle (vs second)"); + common::assert_actor_in_dc(&actor_id3, target_dc1.config.dc_label()).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + // Destroy third actor + tracing::info!(?actor_id3, "destroying third actor"); + common::destroy_actor(&actor_id3, &namespace, target_dc1.guard_port()).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + // Fourth create/destroy cycle with same key + let actor_id4 = common::create_actor_with_options( + common::CreateActorOptions { + namespace: namespace.clone(), + key: Some(key.clone()), + ..Default::default() + }, + target_dc2.guard_port(), + ) + .await; + + assert_ne!(actor_id1, actor_id4, "same actor id after third cycle (vs first)"); + assert_ne!(actor_id2, actor_id4, "same actor id after third cycle (vs second)"); + assert_ne!(actor_id3, actor_id4, "same actor id after third cycle (vs third)"); + common::assert_actor_in_dc(&actor_id4, target_dc1.config.dc_label()).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + // Final destroy + tracing::info!(?actor_id4, "destroying fourth actor"); + common::destroy_actor(&actor_id4, &namespace, target_dc2.guard_port()).await; + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + + runner.shutdown().await; +} diff --git a/packages/infra/engine/tests/actors_delete.rs b/packages/infra/engine/tests/actors_delete.rs index ffed9d6096..956bc1433b 100644 --- a/packages/infra/engine/tests/actors_delete.rs +++ b/packages/infra/engine/tests/actors_delete.rs @@ -15,8 +15,6 @@ fn delete_existing_actor_with_namespace() { common::destroy_actor(&actor_id, &namespace, ctx.leader_dc().guard_port()).await; - common::wait_for_eventual_consistency().await; - common::assert_actor_is_destroyed( &actor_id, Some(&namespace), @@ -27,7 +25,7 @@ fn delete_existing_actor_with_namespace() { } #[test] -fn delete_existing_actor_without_namespace() { +fn delete_existing_actor_without_namespace_should_succeed() { common::run(common::TestOpts::new(1), |ctx| async move { let (namespace, _, _runner) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; @@ -38,16 +36,12 @@ fn delete_existing_actor_without_namespace() { let response = common::destroy_actor_without_namespace(&actor_id, ctx.leader_dc().guard_port()).await; - common::assert_success_response(&response); - - common::wait_for_eventual_consistency().await; + assert_eq!( + response.status(), + 200, + "Should return 200 for unprovided namespace parameter" + ); - common::assert_actor_is_destroyed( - &actor_id, - Some(&namespace), - ctx.leader_dc().guard_port(), - ) - .await; }); } @@ -81,10 +75,9 @@ fn delete_actor_remote_datacenter() { let actor_id = common::create_actor_with_options( common::CreateActorOptions { namespace: namespace.clone(), - datacenter: Some("dc-2".to_string()), ..Default::default() }, - ctx.leader_dc().guard_port(), + ctx.get_dc(2).guard_port(), ) .await; @@ -99,12 +92,89 @@ fn delete_actor_remote_datacenter() { }); } +// MARK: Namespace validation +#[test] +fn delete_actor_non_existent_namespace() { + common::run(common::TestOpts::new(1), |ctx| async move { + let non_existent_ns = "non-existent-namespace"; + let api_port = ctx.leader_dc().guard_port(); + let client = reqwest::Client::new(); + + // DELETE /actors/{id} + let response = client + .delete(&format!( + "http://127.0.0.1:{}/actors/00000000-0000-0000-0000-000000000000?namespace={}", + api_port, non_existent_ns + )) + .send() + .await + .expect("Failed to send request"); + assert!( + !response.status().is_success(), + "DELETE /actors/{{id}} should fail with non-existent namespace" + ); + }); +} + +#[test] +fn delete_actor_invalid_id_format() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + let api_port = ctx.leader_dc().guard_port(); + + // Test various invalid actor ID formats + let invalid_ids = vec![ + "not-a-uuid", + "12345", + "00000000-0000-0000-0000", // Incomplete UUID + "00000000-0000-0000-0000-000000000000g", // Invalid character + "00000000_0000_0000_0000_000000000000", // Wrong separator + ]; + + for invalid_id in invalid_ids { + // DELETE /actors/{id} + let client = reqwest::Client::new(); + let response = client + .delete(&format!( + "http://127.0.0.1:{}/actors/{}?namespace={}", + api_port, invalid_id, namespace + )) + .send() + .await + .expect("Failed to send request"); + assert_eq!( + response.status(), + 400, + "DELETE should return 400 for invalid actor ID: {}", + invalid_id + ); + } + + // DELETE with empty ID also returns 404 + let client = reqwest::Client::new(); + let response = client + .delete(&format!( + "http://127.0.0.1:{}/actors/?namespace={}", + api_port, namespace + )) + .send() + .await + .expect("Failed to send request"); + assert_eq!( + response.status(), + 404, + "DELETE should return 404 for empty actor ID (route not found)" + ); + }); +} + // MARK: Error cases #[test] fn delete_non_existent_actor() { common::run(common::TestOpts::new(1), |ctx| async move { - let (_namespace, _, runner) = + let (_namespace, _, _runner) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; let fake_actor_id = format!("00000000-0000-0000-0000-{:012x}", rand::random::()); @@ -123,9 +193,9 @@ fn delete_non_existent_actor() { #[test] fn delete_actor_wrong_namespace() { common::run(common::TestOpts::new(1), |ctx| async move { - let (namespace1, _, runner1) = + let (namespace1, _, _runner1) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - let (namespace2, _, runner2) = + let (namespace2, _, _runner2) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; let actor_id = common::create_actor(&namespace1, ctx.leader_dc().guard_port()).await; @@ -177,30 +247,6 @@ fn delete_with_non_existent_namespace() { }); } -#[test] -fn delete_invalid_actor_id_format() { - common::run(common::TestOpts::new(1), |ctx| async move { - let (namespace, _, _runner) = - common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - - let client = reqwest::Client::new(); - let response = client - .delete(&format!( - "http://127.0.0.1:{}/actors/invalid-uuid?namespace={}", - ctx.leader_dc().guard_port(), - namespace - )) - .send() - .await - .expect("Failed to send delete request"); - - assert_eq!( - response.status(), - 400, - "Should return 400 for invalid actor ID format" - ); - }); -} // MARK: Cross-datacenter tests @@ -213,10 +259,9 @@ fn delete_remote_actor_verify_propagation() { let actor_id = common::create_actor_with_options( common::CreateActorOptions { namespace: namespace.clone(), - datacenter: Some("dc-2".to_string()), ..Default::default() }, - ctx.leader_dc().guard_port(), + ctx.get_dc(2).guard_port(), ) .await; diff --git a/packages/infra/engine/tests/actors_general.rs b/packages/infra/engine/tests/actors_general.rs deleted file mode 100644 index 7438cd553a..0000000000 --- a/packages/infra/engine/tests/actors_general.rs +++ /dev/null @@ -1,191 +0,0 @@ -mod common; - -use serde_json::json; - -// MARK: Namespace Validation Tests - -#[test] -fn all_endpoints_validate_namespace_exists() { - common::run(common::TestOpts::new(1), |ctx| async move { - let non_existent_ns = "non-existent-namespace"; - let api_port = ctx.leader_dc().guard_port(); - let client = reqwest::Client::new(); - - // POST /actors - let response = client - .post(&format!( - "http://127.0.0.1:{}/actors?namespace={}", - api_port, non_existent_ns - )) - .json(&json!({ - "name": "test", - "key": "key", - })) - .send() - .await - .expect("Failed to send request"); - assert!( - !response.status().is_success(), - "POST /actors should fail with non-existent namespace" - ); - - // GET /actors/{id} - let response = common::get_actor( - "00000000-0000-0000-0000-000000000000", - Some(non_existent_ns), - api_port, - ) - .await; - assert!( - !response.status().is_success(), - "GET /actors/{{id}} should fail with non-existent namespace" - ); - - // DELETE /actors/{id} - let response = client - .delete(&format!( - "http://127.0.0.1:{}/actors/00000000-0000-0000-0000-000000000000?namespace={}", - api_port, non_existent_ns - )) - .send() - .await - .expect("Failed to send request"); - assert!( - !response.status().is_success(), - "DELETE /actors/{{id}} should fail with non-existent namespace" - ); - - // GET /actors/by-id - let response = common::get_actor_by_id(non_existent_ns, "test", "key", api_port).await; - assert!( - !response.status().is_success(), - "GET /actors/by-id should fail with non-existent namespace" - ); - - // PUT /actors - let response = common::get_or_create_actor( - non_existent_ns, - "test", - Some("key".to_string()), - false, - None, - None, - api_port, - ) - .await; - assert!( - !response.status().is_success(), - "PUT /actors should fail with non-existent namespace" - ); - - // PUT /actors/by-id - let response = common::get_or_create_actor_by_id( - non_existent_ns, - "test", - Some("key".to_string()), - None, - api_port, - ) - .await; - assert!( - !response.status().is_success(), - "PUT /actors/by-id should fail with non-existent namespace" - ); - - // GET /actors (list) - let response = common::list_actors( - non_existent_ns, - Some("test"), - None, - None, - None, - None, - None, - api_port, - ) - .await; - assert!( - !response.status().is_success(), - "GET /actors (list) should fail with non-existent namespace" - ); - - // GET /actors/names - let response = common::list_actor_names(non_existent_ns, None, None, api_port).await; - assert!( - !response.status().is_success(), - "GET /actors/names should fail with non-existent namespace" - ); - }); -} - -// MARK: Actor ID Validation - -#[test] -fn invalid_actor_id_formats() { - common::run(common::TestOpts::new(1), |ctx| async move { - let (namespace, _, _runner) = - common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - let api_port = ctx.leader_dc().guard_port(); - - // Test various invalid actor ID formats - let invalid_ids = vec![ - "not-a-uuid", - "12345", - "00000000-0000-0000-0000", // Incomplete UUID - "00000000-0000-0000-0000-000000000000g", // Invalid character - "00000000_0000_0000_0000_000000000000", // Wrong separator - ]; - - for invalid_id in invalid_ids { - // GET /actors/{id} - let response = common::get_actor(invalid_id, Some(&namespace), api_port).await; - assert_eq!( - response.status(), - 400, - "GET should return 400 for invalid actor ID: {}", - invalid_id - ); - - // DELETE /actors/{id} - let client = reqwest::Client::new(); - let response = client - .delete(&format!( - "http://127.0.0.1:{}/actors/{}?namespace={}", - api_port, invalid_id, namespace - )) - .send() - .await - .expect("Failed to send request"); - assert_eq!( - response.status(), - 400, - "DELETE should return 400 for invalid actor ID: {}", - invalid_id - ); - } - - // Special case: empty actor ID results in different route - let response = common::get_actor("", Some(&namespace), api_port).await; - assert_eq!( - response.status(), - 404, - "GET should return 404 for empty actor ID (route not found)" - ); - - // DELETE with empty ID also returns 404 - let client = reqwest::Client::new(); - let response = client - .delete(&format!( - "http://127.0.0.1:{}/actors/?namespace={}", - api_port, namespace - )) - .send() - .await - .expect("Failed to send request"); - assert_eq!( - response.status(), - 404, - "DELETE should return 404 for empty actor ID (route not found)" - ); - }); -} diff --git a/packages/infra/engine/tests/actors_get.rs b/packages/infra/engine/tests/actors_get.rs index 54f2c57d8c..f83084f820 100644 --- a/packages/infra/engine/tests/actors_get.rs +++ b/packages/infra/engine/tests/actors_get.rs @@ -21,7 +21,7 @@ fn get_existing_actor_with_namespace() { } #[test] -fn get_existing_actor_without_namespace() { +fn get_existing_actor_without_namespace_should_fail() { common::run(common::TestOpts::new(1), |ctx| async move { let (namespace, _, _runner) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; @@ -29,13 +29,13 @@ fn get_existing_actor_without_namespace() { // Create actor let actor_id = common::create_actor(&namespace, ctx.leader_dc().guard_port()).await; - // Get actor without namespace + // Get actor without namespace should fail let response = common::get_actor(&actor_id, None, ctx.leader_dc().guard_port()).await; - common::assert_success_response(&response); - - let body: serde_json::Value = response.json().await.expect("Failed to parse response"); - assert_eq!(body["actor"]["actor_id"], actor_id); - assert_eq!(body["actor"]["name"], "test-actor"); + assert_eq!( + response.status(), + 400, + "Should return 400 for missing namespace parameter" + ); }); } @@ -61,6 +61,64 @@ fn get_actor_current_datacenter() { }); } +// MARK: Namespace validation +#[test] +fn get_actor_non_existent_namespace() { + common::run(common::TestOpts::new(1), |ctx| async move { + let non_existent_ns = "non-existent-namespace"; + let api_port = ctx.leader_dc().guard_port(); + + // GET /actors/{id} + let response = common::get_actor( + "00000000-0000-0000-0000-000000000000", + Some(non_existent_ns), + api_port, + ) + .await; + assert!( + !response.status().is_success(), + "GET /actors/{{id}} should fail with non-existent namespace" + ); + }); +} + +#[test] +fn get_actor_invalid_id_format() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + let api_port = ctx.leader_dc().guard_port(); + + // Test various invalid actor ID formats + let invalid_ids = vec![ + "not-a-uuid", + "12345", + "00000000-0000-0000-0000", // Incomplete UUID + "00000000-0000-0000-0000-000000000000g", // Invalid character + "00000000_0000_0000_0000_000000000000", // Wrong separator + ]; + + for invalid_id in invalid_ids { + // GET /actors/{id} + let response = common::get_actor(invalid_id, Some(&namespace), api_port).await; + assert_eq!( + response.status(), + 400, + "GET should return 400 for invalid actor ID: {}", + invalid_id + ); + } + + // Special case: empty actor ID results in different route + let response = common::get_actor("", Some(&namespace), api_port).await; + assert_eq!( + response.status(), + 404, + "GET should return 404 for empty actor ID (route not found)" + ); + }); +} + // Error cases #[test] @@ -89,9 +147,9 @@ fn get_non_existent_actor() { #[test] fn get_actor_wrong_namespace() { common::run(common::TestOpts::new(1), |ctx| async move { - let (namespace1, _, runner1) = + let (namespace1, _, _runner1) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - let (namespace2, _, runner2) = + let (namespace2, _, _runner2) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; // Create actor in namespace1 @@ -134,28 +192,6 @@ fn get_with_non_existent_namespace() { }); } -#[test] -fn get_invalid_actor_id_format() { - common::run(common::TestOpts::new(1), |ctx| async move { - let (namespace, _, _runner) = - common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - - // Try to get with invalid actor ID format - let response = common::get_actor( - "invalid-uuid", - Some(&namespace), - ctx.leader_dc().guard_port(), - ) - .await; - - // Should fail with bad request - assert_eq!( - response.status(), - 400, - "Should return 400 for invalid actor ID format" - ); - }); -} // Cross-datacenter tests @@ -169,7 +205,6 @@ fn get_remote_actor_verify_routing() { let actor_id = common::create_actor_with_options( common::CreateActorOptions { namespace: namespace.clone(), - datacenter: Some("dc-2".to_string()), ..Default::default() }, ctx.get_dc(2).guard_port(), diff --git a/packages/infra/engine/tests/actors_get_by_id.rs b/packages/infra/engine/tests/actors_get_by_id.rs index ea5c51514c..94d517f191 100644 --- a/packages/infra/engine/tests/actors_get_by_id.rs +++ b/packages/infra/engine/tests/actors_get_by_id.rs @@ -1,10 +1,14 @@ mod common; +use std::time::Duration; + +use reqwest::Client; use serde_json::json; // MARK: Basic #[test] +#[ignore] fn get_actor_id_for_existing_actor() { common::run(common::TestOpts::new(1), |ctx| async move { let (namespace, _, _runner) = @@ -23,6 +27,8 @@ fn get_actor_id_for_existing_actor() { ) .await; + tokio::time::sleep(Duration::from_millis(500)).await; + let response = common::get_actor_by_id(&namespace, name, key, ctx.leader_dc().guard_port()).await; common::assert_success_response(&response); @@ -58,6 +64,42 @@ fn get_null_actor_id_for_non_existent() { // MARK: Error cases +#[test] +fn test_with_parameter_variations() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = Client::new(); + + // Test missing required parameters + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors/by-id", + ctx.leader_dc().guard_port() + )) + .query(&[("name", "test"), ("key", "test")]) + .send() + .await + .expect("Failed to send request"); + + assert!(!response.status().is_success(), "Should fail without namespace"); + + // Test empty parameter values + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors/by-id", + ctx.leader_dc().guard_port() + )) + .query(&[("namespace", ""), ("name", "test"), ("key", "test")]) + .send() + .await + .expect("Failed to send request"); + + assert!(!response.status().is_success(), "Should fail with empty namespace"); + }); +} + #[test] fn get_by_id_non_existent_namespace() { common::run(common::TestOpts::new(1), |ctx| async move { @@ -71,9 +113,9 @@ fn get_by_id_non_existent_namespace() { assert!( !response.status().is_success(), - "Should fail with non-existent namespace" + "Should fail with non-existent namespace but instead got {}", response.text().await.unwrap() ); - common::assert_error_response(response, "namespace_not_found").await; + common::assert_error_response(response, "not_found").await; }); } @@ -130,41 +172,3 @@ fn get_by_id_missing_parameters() { ); }); } - -#[test] -fn get_by_id_empty_string_parameters() { - common::run(common::TestOpts::new(1), |ctx| async move { - let (namespace, _, _runner) = - common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - - let client = reqwest::Client::new(); - - let response = client - .get(&format!( - "http://127.0.0.1:{}/actors/by-id?namespace={}&name=&key=test-key", - ctx.leader_dc().guard_port(), - namespace - )) - .send() - .await - .expect("Failed to send request"); - assert!( - !response.status().is_success(), - "Should fail with empty name" - ); - - let response = client - .get(&format!( - "http://127.0.0.1:{}/actors/by-id?namespace={}&name=test-actor&key=", - ctx.leader_dc().guard_port(), - namespace - )) - .send() - .await - .expect("Failed to send request"); - assert!( - !response.status().is_success(), - "Should fail with empty key" - ); - }); -} diff --git a/packages/infra/engine/tests/actors_get_or_create.rs b/packages/infra/engine/tests/actors_get_or_create.rs index 023028a3c2..2d73632bb0 100644 --- a/packages/infra/engine/tests/actors_get_or_create.rs +++ b/packages/infra/engine/tests/actors_get_or_create.rs @@ -30,7 +30,6 @@ fn get_existing_actor_with_matching_key() { Some(key), false, None, - None, ctx.leader_dc().guard_port(), ) .await; @@ -43,6 +42,7 @@ fn get_existing_actor_with_matching_key() { } #[test] +#[ignore] fn get_existing_actor_from_remote_datacenter() { common::run(common::TestOpts::new(2), |ctx| async move { let (namespace, _, _runner) = @@ -56,7 +56,6 @@ fn get_existing_actor_from_remote_datacenter() { namespace: namespace.clone(), name: name.to_string(), key: Some(key.clone()), - datacenter: Some("dc-2".to_string()), ..Default::default() }, ctx.get_dc(2).guard_port(), @@ -71,7 +70,6 @@ fn get_existing_actor_from_remote_datacenter() { Some(key), false, None, - None, ctx.leader_dc().guard_port(), ) .await; @@ -102,7 +100,6 @@ fn create_new_actor_when_none_exists() { Some(key.clone()), false, None, - None, ctx.leader_dc().guard_port(), ) .await; @@ -134,7 +131,6 @@ fn create_actor_with_input_data() { name, Some(key), false, - None, Some(input), ctx.leader_dc().guard_port(), ) @@ -161,7 +157,6 @@ fn create_durable_actor() { Some(key), true, None, - None, ctx.leader_dc().guard_port(), ) .await; @@ -182,24 +177,13 @@ fn create_actor_in_specific_datacenter() { let name = "dc-specific-actor"; let key = "dc-key".to_string(); - let response = common::get_or_create_actor( + let actor_id = common::create_actor( &namespace, - name, - Some(key), - false, - Some("dc-2"), - None, - ctx.leader_dc().guard_port(), + ctx.get_dc(2).guard_port() ) .await; - common::assert_success_response(&response); - let body: serde_json::Value = response.json().await.expect("Failed to parse response"); - common::assert_created_response(&body, true).await; - let actor_id_str = body["actor"]["actor_id"] - .as_str() - .expect("Missing actor_id in actor"); - common::assert_actor_in_dc(&actor_id_str, 2).await; + common::assert_actor_in_dc(&actor_id, 2).await; }); } @@ -214,7 +198,6 @@ fn get_or_create_non_existent_namespace() { Some("key".to_string()), false, None, - None, ctx.leader_dc().guard_port(), ) .await; @@ -223,40 +206,16 @@ fn get_or_create_non_existent_namespace() { !response.status().is_success(), "Should fail with non-existent namespace" ); - common::assert_error_response(response, "namespace_not_found").await; - }); -} - -#[test] -fn get_or_create_invalid_datacenter() { - common::run(common::TestOpts::new(1), |ctx| async move { - let (namespace, _, _runner) = - common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - - let response = common::get_or_create_actor( - &namespace, - "test-actor", - Some("key".to_string()), - false, - Some("invalid-dc"), - None, - ctx.leader_dc().guard_port(), - ) - .await; - - assert!( - !response.status().is_success(), - "Should fail with invalid datacenter" - ); + common::assert_error_response(response, "not_found").await; }); } #[test] fn get_or_create_wrong_namespace() { common::run(common::TestOpts::new(1), |ctx| async move { - let (namespace1, _, runner1) = + let (namespace1, _, _runner1) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - let (namespace2, _, runner2) = + let (namespace2, _, _runner2) = common::setup_test_namespace_with_runner(ctx.leader_dc()).await; let name = "cross-namespace-actor"; @@ -279,7 +238,6 @@ fn get_or_create_wrong_namespace() { Some(key), false, None, - None, ctx.leader_dc().guard_port(), ) .await; diff --git a/packages/infra/engine/tests/actors_get_or_create_by_id.rs b/packages/infra/engine/tests/actors_get_or_create_by_id.rs index 5f4f938822..4e5a40ca7e 100644 --- a/packages/infra/engine/tests/actors_get_or_create_by_id.rs +++ b/packages/infra/engine/tests/actors_get_or_create_by_id.rs @@ -1,10 +1,9 @@ mod common; -use serde_json::json; - // MARK: Basic #[test] +#[ignore] fn get_existing_actor_id_with_matching_key() { common::run(common::TestOpts::new(1), |ctx| async move { let (namespace, _, _runner) = @@ -28,7 +27,6 @@ fn get_existing_actor_id_with_matching_key() { &namespace, name, Some(key), - None, ctx.leader_dc().guard_port(), ) .await; @@ -41,6 +39,7 @@ fn get_existing_actor_id_with_matching_key() { } #[test] +#[ignore] fn create_new_actor_id_when_none_exists() { common::run(common::TestOpts::new(1), |ctx| async move { let (namespace, _, _runner) = @@ -53,7 +52,6 @@ fn create_new_actor_id_when_none_exists() { &namespace, name, Some(key), - None, ctx.leader_dc().guard_port(), ) .await; @@ -69,6 +67,7 @@ fn create_new_actor_id_when_none_exists() { } #[test] +#[ignore] fn create_actor_id_in_specific_datacenter() { common::run(common::TestOpts::new(2), |ctx| async move { let (namespace, _, _runner) = @@ -81,8 +80,7 @@ fn create_actor_id_in_specific_datacenter() { &namespace, name, Some(key), - Some("dc-2"), - ctx.leader_dc().guard_port(), + ctx.get_dc(2).guard_port(), ) .await; common::assert_success_response(&response); @@ -103,13 +101,13 @@ fn create_actor_id_in_specific_datacenter() { // MARK: Error Cases #[test] +#[ignore] fn get_or_create_by_id_non_existent_namespace() { common::run(common::TestOpts::new(1), |ctx| async move { let response = common::get_or_create_actor_by_id( "non-existent-namespace", "test-actor", Some("key".to_string()), - None, ctx.leader_dc().guard_port(), ) .await; @@ -118,11 +116,12 @@ fn get_or_create_by_id_non_existent_namespace() { !response.status().is_success(), "Should fail with non-existent namespace" ); - common::assert_error_response(response, "namespace_not_found").await; + common::assert_error_response(response, "not_found").await; }); } #[test] +#[ignore] fn get_or_create_by_id_invalid_datacenter() { common::run(common::TestOpts::new(1), |ctx| async move { let (namespace, _, _runner) = @@ -132,7 +131,6 @@ fn get_or_create_by_id_invalid_datacenter() { &namespace, "test-actor", Some("key".to_string()), - Some("invalid-dc"), ctx.leader_dc().guard_port(), ) .await; diff --git a/packages/infra/engine/tests/actors_lifecycle.rs b/packages/infra/engine/tests/actors_lifecycle.rs index d98d831bfd..021e7df9c9 100644 --- a/packages/infra/engine/tests/actors_lifecycle.rs +++ b/packages/infra/engine/tests/actors_lifecycle.rs @@ -30,14 +30,10 @@ async fn actor_lifecycle_inner(ctx: &common::TestCtx, multi_dc: bool) { let actor_id = common::create_actor(&namespace, target_dc.guard_port()).await; // Test ping via guard - let ping_response = common::ping_actor_via_guard(ctx.leader_dc().guard_port(), &actor_id).await; + let ping_response = + common::ping_actor_via_guard(ctx.leader_dc().guard_port(), &actor_id).await; assert_eq!(ping_response["status"], "ok"); - // Test websocket via guard - let ws_response = - common::ping_actor_websocket_via_guard(ctx.leader_dc().guard_port(), &actor_id).await; - assert_eq!(ws_response["status"], "ok"); - // Validate runner state assert!( runner.has_actor(&actor_id).await, @@ -111,16 +107,15 @@ async fn actor_lifecycle_with_same_key_inner(ctx: &common::TestCtx, dc_choice: D // correctly waits for the actor to start. tokio::time::sleep(Duration::from_millis(500)).await; + // Test ping directly to runner + let ping_response = common::ping_actor_via_runner(&actor_id1, runner.port).await; + assert_eq!(ping_response["status"], "ok"); + // Test ping via guard let ping_response = common::ping_actor_via_guard(ctx.leader_dc().guard_port(), &actor_id1).await; assert_eq!(ping_response["status"], "ok"); - // Test websocket via guard - let ws_response = - common::ping_actor_websocket_via_guard(ctx.leader_dc().guard_port(), &actor_id1).await; - assert_eq!(ws_response["status"], "ok"); - // Destroy tracing::info!("destroying actor"); tokio::time::sleep(Duration::from_millis(500)).await; @@ -145,16 +140,15 @@ async fn actor_lifecycle_with_same_key_inner(ctx: &common::TestCtx, dc_choice: D // correctly waits for the actor to start. tokio::time::sleep(Duration::from_millis(500)).await; + // Test ping directly to runner + let ping_response = common::ping_actor_via_runner(&actor_id2, runner.port).await; + assert_eq!(ping_response["status"], "ok"); + // Test ping via guard let ping_response = common::ping_actor_via_guard(ctx.leader_dc().guard_port(), &actor_id2).await; assert_eq!(ping_response["status"], "ok"); - // Test websocket via guard - let ws_response = - common::ping_actor_websocket_via_guard(ctx.leader_dc().guard_port(), &actor_id2).await; - assert_eq!(ws_response["status"], "ok"); - // Destroy tracing::info!("destroying actor"); tokio::time::sleep(Duration::from_millis(500)).await; diff --git a/packages/infra/engine/tests/actors_list.rs b/packages/infra/engine/tests/actors_list.rs index ab3ab66f43..3a2676de9a 100644 --- a/packages/infra/engine/tests/actors_list.rs +++ b/packages/infra/engine/tests/actors_list.rs @@ -2,6 +2,8 @@ mod common; use std::collections::HashSet; +use reqwest::Client; + // MARK: List by Name #[test] @@ -28,7 +30,7 @@ fn list_actors_by_namespace_and_name() { actor_ids.push(actor_id); } - // List actors by name + // List actlist_actorsors by name let response = common::list_actors( &namespace, Some(name), @@ -85,8 +87,9 @@ fn list_with_pagination() { actor_ids.push(actor_id); } - // Wait for actors to be fully created and available - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + for actor_id in &actor_ids { + common::wait_for_actor_propagation(&actor_id, 1).await; + } // First page - limit 2 let response1 = common::list_actors( @@ -505,10 +508,9 @@ fn list_actors_from_multiple_datacenters() { namespace: namespace.clone(), name: "multi-dc-actor".to_string(), key: Some("dc2-key".to_string()), - datacenter: Some("dc-2".to_string()), ..Default::default() }, - ctx.leader_dc().guard_port(), + ctx.get_dc(2).guard_port(), ) .await; @@ -558,7 +560,7 @@ fn list_with_non_existent_namespace() { !response.status().is_success(), "Should fail with non-existent namespace" ); - common::assert_error_response(response, "namespace_not_found").await; + common::assert_error_response(response, "not_found").await; }); } @@ -616,37 +618,7 @@ fn list_with_key_but_no_name() { ); }); } -#[test] -fn list_with_more_than_32_actor_ids() { - common::run(common::TestOpts::new(1), |ctx| async move { - let (namespace, _, _runner) = - common::setup_test_namespace_with_runner(ctx.leader_dc()).await; - - // Try to list with more than 32 actor IDs - let actor_ids: Vec = (0..33) - .map(|i| format!("00000000-0000-0000-0000-{:012x}", i)) - .collect(); - let response = common::list_actors( - &namespace, - None, - None, - Some(actor_ids), - None, - None, - None, - ctx.leader_dc().guard_port(), - ) - .await; - - // Should fail with validation error - assert_eq!( - response.status(), - 400, - "Should return 400 for too many actor IDs" - ); - }); -} #[test] fn list_without_name_when_not_using_actor_ids() { common::run(common::TestOpts::new(1), |ctx| async move { @@ -757,7 +729,6 @@ fn list_aggregates_results_from_all_datacenters() { namespace: namespace.clone(), name: name.to_string(), key: Some("dc2-key".to_string()), - datacenter: Some("dc-2".to_string()), ..Default::default() }, ctx.get_dc(2).guard_port(), @@ -794,3 +765,54 @@ fn list_aggregates_results_from_all_datacenters() { assert!(returned_ids.contains(&actor_id_dc2)); }); } + +// MARK: Limit values +#[test] +fn test_list_limit_variation() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = Client::new(); + + // Test limit parameter validation + let test_cases = vec![ + ("0", true), // Should work - gives 0 values + ("1", true), // Should work - minimum valid limit + ("100", true), // Should work - normal limit + ("1001", false), // Should fail - exceeds maximum limit + ("-1", false), // Should fail - negative limit + ("abc", false), // Should fail - non-numeric limit + ]; + + for (limit_value, should_succeed) in test_cases { + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors", + ctx.leader_dc().guard_port() + )) + .query(&[ + ("namespace", namespace.as_str()), + ("name", "test-actor"), + ("limit", limit_value), + ]) + .send() + .await + .expect("Failed to send request"); + + if should_succeed { + assert!( + response.status().is_success() || response.status() == 200, + "Limit {} should be accepted", + limit_value + ); + } else { + assert!( + !response.status().is_success(), + "Limit {} should be rejected", + limit_value + ); + } + } + }); +} diff --git a/packages/infra/engine/tests/actors_list_names.rs b/packages/infra/engine/tests/actors_list_names.rs index a729a7b033..e437ecb67c 100644 --- a/packages/infra/engine/tests/actors_list_names.rs +++ b/packages/infra/engine/tests/actors_list_names.rs @@ -44,7 +44,11 @@ fn list_all_actor_names_in_namespace() { common::assert_success_response(&response); let body: serde_json::Value = response.json().await.expect("Failed to parse response"); - let returned_names = body["names"].as_array().expect("Expected names array"); + let returned_names = body["names"] + .as_object() + .expect("Expected names object to exit") + .keys() + .collect::>(); // Should return unique names only assert_eq!(returned_names.len(), 3, "Should return 3 unique names"); @@ -52,7 +56,7 @@ fn list_all_actor_names_in_namespace() { // Verify all names are present let name_set: HashSet = returned_names .iter() - .map(|n| n.as_str().unwrap().to_string()) + .map(|n| n.as_str().to_string()) .collect(); for name in &names { assert!( @@ -216,7 +220,6 @@ fn list_names_fanout_to_all_datacenters() { common::CreateActorOptions { namespace: namespace.clone(), name: "dc2-actor".to_string(), - datacenter: Some("dc-2".to_string()), ..Default::default() }, ctx.get_dc(2).guard_port(), @@ -275,7 +278,6 @@ fn list_names_deduplication_across_datacenters() { namespace: namespace.clone(), name: shared_name.to_string(), key: Some("dc2-key".to_string()), - datacenter: Some("dc-2".to_string()), ..Default::default() }, ctx.get_dc(2).guard_port(), diff --git a/packages/infra/engine/tests/common/actors.rs b/packages/infra/engine/tests/common/actors.rs index d3495c6dc5..b8bec0f150 100644 --- a/packages/infra/engine/tests/common/actors.rs +++ b/packages/infra/engine/tests/common/actors.rs @@ -8,7 +8,6 @@ pub struct CreateActorOptions { pub input: Option, pub runner_name_selector: Option, pub durable: bool, - pub datacenter: Option, } impl Default for CreateActorOptions { @@ -23,7 +22,6 @@ impl Default for CreateActorOptions { )), runner_name_selector: Some("test-runner".to_string()), durable: false, - datacenter: None, } } } @@ -47,15 +45,11 @@ pub async fn create_actor_with_options(options: CreateActorOptions, guard_port: if let Some(runner_name_selector) = options.runner_name_selector { body["runner_name_selector"] = json!(runner_name_selector); } - let mut url = format!( + let url = format!( "http://127.0.0.1:{}/actors?namespace={}", guard_port, options.namespace ); - if let Some(datacenter) = &options.datacenter { - url.push_str(&format!("&datacenter={}", datacenter)); - } - let client = reqwest::Client::new(); let response = client .post(url) @@ -93,6 +87,7 @@ pub async fn create_actor(namespace_name: &str, guard_port: u16) -> String { .await } + /// Pings actor via Guard. pub async fn ping_actor_via_guard(guard_port: u16, actor_id: &str) -> serde_json::Value { tracing::info!(?guard_port, ?actor_id, "sending request to actor via guard"); @@ -121,6 +116,37 @@ pub async fn ping_actor_via_guard(guard_port: u16, actor_id: &str) -> serde_json response } +/// Pings actor directly on the runner's server. +pub async fn ping_actor_via_runner(actor_id: &str, runner_port: u16) -> serde_json::Value { + tracing::info!( + ?actor_id, + ?runner_port, + "sending request to actor via runner" + ); + + let client = reqwest::Client::new(); + let response = client + .get(format!("http://127.0.0.1:{}/ping", runner_port)) + .header("X-Rivet-Actor", actor_id) + .send() + .await + .expect("Failed to send ping request"); + + if !response.status().is_success() { + let text = response.text().await.expect("Failed to read response text"); + panic!("Failed to ping actor: {}", text); + } + + let response = response + .json() + .await + .expect("Failed to parse JSON response"); + + tracing::info!(?response, "received response from actor"); + + response +} + pub async fn destroy_actor(actor_id: &str, namespace_name: &str, guard_port: u16) { let client = reqwest::Client::new(); let url = format!( @@ -208,7 +234,6 @@ pub async fn get_or_create_actor( name: &str, key: Option, durable: bool, - datacenter: Option<&str>, input: Option, guard_port: u16, ) -> reqwest::Response { @@ -232,9 +257,6 @@ pub async fn get_or_create_actor( if let Some(input) = input { body["input"] = json!(input); } - if let Some(datacenter) = datacenter { - body["datacenter"] = json!(datacenter); - } tracing::info!(?url, ?body, "get or create actor"); @@ -250,7 +272,6 @@ pub async fn get_or_create_actor_by_id( namespace: &str, name: &str, key: Option, - datacenter: Option<&str>, guard_port: u16, ) -> reqwest::Response { let client = reqwest::Client::new(); @@ -265,10 +286,6 @@ pub async fn get_or_create_actor_by_id( "runner_name_selector": "test-runner", }); - if let Some(datacenter) = datacenter { - body["datacenter"] = json!(datacenter); - } - tracing::info!(?url, ?body, "get or create actor by id"); client @@ -355,7 +372,8 @@ pub async fn list_actor_names( pub fn assert_success_response(response: &reqwest::Response) { assert!( response.status().is_success(), - "Response not successful: {}", + "{} Response not successful: {}", + response.url(), response.status() ); } @@ -366,7 +384,8 @@ pub async fn assert_error_response( ) -> serde_json::Value { assert!( !response.status().is_success(), - "Expected error but got success: {}", + "{} Expected error but got success: {}", + response.url(), response.status() ); @@ -375,7 +394,7 @@ pub async fn assert_error_response( .await .expect("Failed to parse error response"); - let error_code = body["error"]["code"] + let error_code = body["code"] .as_str() .expect("Missing error code in response"); assert_eq!( @@ -413,136 +432,3 @@ pub async fn bulk_create_actors( } actor_ids } - -/// Tests WebSocket connection to actor via Guard using a simple ping pong. -pub async fn ping_actor_websocket_via_guard(guard_port: u16, actor_id: &str) -> serde_json::Value { - use tokio_tungstenite::{ - connect_async, - tungstenite::{Message, client::IntoClientRequest}, - }; - - tracing::info!( - ?guard_port, - ?actor_id, - "testing websocket connection to actor via guard" - ); - - // Build WebSocket URL and request - let ws_url = format!("ws://127.0.0.1:{}/ws", guard_port); - let mut request = ws_url - .clone() - .into_client_request() - .expect("Failed to create WebSocket request"); - - // Add headers for routing through guard to actor - request - .headers_mut() - .insert("X-Rivet-Target", "actor".parse().unwrap()); - request - .headers_mut() - .insert("X-Rivet-Actor", actor_id.parse().unwrap()); - - // Connect to WebSocket - let (ws_stream, response) = connect_async(request) - .await - .expect("Failed to connect to WebSocket"); - - // Verify connection was successful - assert_eq!( - response.status(), - 101, - "Expected WebSocket upgrade status 101" - ); - - tracing::info!("websocket connected successfully"); - - use futures_util::{SinkExt, StreamExt}; - let (mut write, mut read) = ws_stream.split(); - - // Send a ping message to verify the connection works - let ping_message = "ping"; - tracing::info!(?ping_message, "sending ping message"); - write - .send(Message::Text(ping_message.to_string().into())) - .await - .expect("Failed to send ping message"); - - // Wait for response with timeout - let response = tokio::time::timeout(tokio::time::Duration::from_secs(5), read.next()) - .await - .expect("Timeout waiting for WebSocket response") - .expect("WebSocket stream ended unexpectedly"); - - // Verify response - let response_text = match response { - Ok(Message::Text(text)) => { - let text_str = text.to_string(); - tracing::info!(?text_str, "received response from actor"); - text_str - } - Ok(msg) => { - panic!("Unexpected message type: {:?}", msg); - } - Err(e) => { - panic!("Failed to receive message: {}", e); - } - }; - - // Verify the response matches expected echo pattern - let expected_response = "Echo: ping"; - assert_eq!( - response_text, expected_response, - "Expected '{}' but got '{}'", - expected_response, response_text - ); - - // Send another message to test multiple round trips - let test_message = "hello world"; - tracing::info!(?test_message, "sending test message"); - write - .send(Message::Text(test_message.to_string().into())) - .await - .expect("Failed to send test message"); - - // Wait for second response - let response2 = tokio::time::timeout(tokio::time::Duration::from_secs(5), read.next()) - .await - .expect("Timeout waiting for second WebSocket response") - .expect("WebSocket stream ended unexpectedly"); - - // Verify second response - let response2_text = match response2 { - Ok(Message::Text(text)) => { - let text_str = text.to_string(); - tracing::info!(?text_str, "received second response from actor"); - text_str - } - Ok(msg) => { - panic!("Unexpected message type for second response: {:?}", msg); - } - Err(e) => { - panic!("Failed to receive second message: {}", e); - } - }; - - let expected_response2 = format!("Echo: {}", test_message); - assert_eq!( - response2_text, expected_response2, - "Expected '{}' but got '{}'", - expected_response2, response2_text - ); - - // Close the connection gracefully - write - .send(Message::Close(None)) - .await - .expect("Failed to send close message"); - - tracing::info!("websocket bidirectional test completed successfully"); - - // Return success response - json!({ - "status": "ok", - "message": "WebSocket bidirectional messaging tested successfully" - }) -} diff --git a/packages/infra/engine/tests/common/runner.rs b/packages/infra/engine/tests/common/runner.rs index 8cde1acc3a..6b398a1d71 100644 --- a/packages/infra/engine/tests/common/runner.rs +++ b/packages/infra/engine/tests/common/runner.rs @@ -37,6 +37,7 @@ impl TestRunner { let handle = Command::new("node") .arg(runner_script_path) .env("INTERNAL_SERVER_PORT", internal_server_port.to_string()) + .env("HTTP_SERVER_PORT", http_server_port.to_string()) .env("RIVET_NAMESPACE", namespace_name) .env("RIVET_RUNNER_KEY", key.to_string()) .env("RIVET_RUNNER_VERSION", version.to_string()) diff --git a/packages/infra/engine/tests/common/test_helpers.rs b/packages/infra/engine/tests/common/test_helpers.rs index 408713736b..3fe5f75dda 100644 --- a/packages/infra/engine/tests/common/test_helpers.rs +++ b/packages/infra/engine/tests/common/test_helpers.rs @@ -72,8 +72,13 @@ pub fn generate_special_chars_string() -> String { } // Wait helpers -pub async fn wait_for_actor_propagation(actor_id: &str, timeout_secs: u64) { +pub async fn wait_for_actor_propagation( + actor_id: &str, + timeout_secs: u64 +) { tracing::info!(?actor_id, ?timeout_secs, "waiting for actor propagation"); + // TODO: + // dc.workflow_ctx. tokio::time::sleep(Duration::from_secs(timeout_secs)).await; } diff --git a/packages/infra/engine/tests/request_body.rs b/packages/infra/engine/tests/request_body.rs new file mode 100644 index 0000000000..8d91a5309b --- /dev/null +++ b/packages/infra/engine/tests/request_body.rs @@ -0,0 +1,298 @@ +mod common; + +use reqwest::header::{HeaderValue, CONTENT_TYPE}; +use serde_json::json; + +#[test] +fn test_request_body_field_validation() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test minimal required fields + let minimal_body = json!({ + "name": "minimal-actor", + "crash_policy": "destroy", + "runner_name_selector": "test-runner" + }); + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&minimal_body) + .send() + .await + .expect("Failed to send request"); + assert!(response.status().is_success(), "Minimal JSON should work"); + + // Test complete body with all fields + let full_body = json!({ + "name": "full-actor", + "key": "test-key", + "input": "dGVzdCBkYXRh", // base64 "test data" + "crash_policy": "destroy", + "runner_name_selector": "test-runner" + }); + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&full_body) + .send() + .await + .expect("Failed to send request"); + assert!(response.status().is_success(), "Full JSON should work"); + + // Test extra fields are ignored + let extra_fields_body = json!({ + "name": "extra-fields-actor", + "crash_policy": "destroy", + "runner_name_selector": "test-runner", + "extra_field": "should be ignored", + "another_extra": 123, + "nested_extra": {"field": "value"} + }); + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&extra_fields_body) + .send() + .await + .expect("Failed to send request"); + assert!( + !response.status().is_success(), + "JSON with extra fields should fail" + ); + }); +} + +#[test] +fn test_malformed_json_handling() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test various malformed JSON strings that should be rejected + let invalid_json_bodies = vec![ + r#"{"name": "test"#, // Missing closing brace + r#"{"name": test"}"#, // Missing quotes around value + r#"{name: "test"}"#, // Missing quotes around key + r#"{"name": "test",}"#, // Trailing comma + r#""just a string""#, // Not an object + r#"[1, 2, 3]"#, // Array instead of object + r#"null"#, // Null value + r#""#, // Empty string + r#"invalid json"#, // Not JSON at all + ]; + + for invalid_json in invalid_json_bodies { + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .header(CONTENT_TYPE, "application/json") + .body(invalid_json) + .send() + .await + .expect("Failed to send request"); + + assert!( + !response.status().is_success(), + "Invalid JSON '{}' expected unsuccessful request", + invalid_json.chars().take(20).collect::() + ); + } + }); +} + +#[test] +fn test_field_type_validation() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test invalid field types + let invalid_bodies = vec![ + // Name field should be string + json!({"name": 123}), + json!({"name": null}), + json!({"name": []}), + json!({"name": {}}), + + // Crash policy should be valid enum value + json!({"name": "test", "crash_policy": "invalid_policy"}), + json!({"name": "test", "crash_policy": 123}), + ]; + + for invalid_body in invalid_bodies { + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&invalid_body) + .send() + .await + .expect("Failed to send request"); + + assert!( + !response.status().is_success(), + "Invalid field type should be rejected: {:?}", + invalid_body + ); + } + }); +} + +#[test] +fn test_large_request_body_handling() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test large input data (1MB) + let large_input = common::generate_large_input_data(1); + let large_body = json!({ + "name": "large-input-actor", + "input": large_input + }); + + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&large_body) + .send() + .await + .expect("Failed to send request"); + + // Should handle large bodies or reject with appropriate error + assert!( + response.status().is_success() || response.status() == 422, + "Large body should succeed or return 422 Unprocessable Entity, got: {}", + response.status() + ); + }); +} + +#[test] +fn test_missing_required_fields() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test completely empty body + let empty_body = json!({}); + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&empty_body) + .send() + .await + .expect("Failed to send request"); + + assert!( + !response.status().is_success(), + "Empty body should be rejected" + ); + + // Test body with empty name field + let empty_name_body = json!({"name": ""}); + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&empty_name_body) + .send() + .await + .expect("Failed to send request"); + + assert!( + !response.status().is_success(), + "Empty name field should be rejected" + ); + }); +} + +#[test] +fn test_base64_input_handling() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test valid base64 input + let valid_b64_body = json!({ + "name": "b64-actor", + "input": "dGVzdCBkYXRh", // "test data" in base64 + "crash_policy": "destroy", + "runner_name_selector": "test-runner" + }); + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&valid_b64_body) + .send() + .await + .expect("Failed to send request"); + + assert!( + response.status().is_success(), + "Valid base64 input should be accepted" + ); + + // Test invalid base64 input + let invalid_b64_body = json!({ + "name": "invalid-b64-actor", + "input": "not-valid-base64!@#$%" + }); + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .json(&invalid_b64_body) + .send() + .await + .expect("Failed to send request"); + + // Server may accept invalid base64 and handle it during processing + // or reject it immediately - both behaviors are valid + assert!( + response.status().as_u16() > 0, + "Should handle invalid base64 input gracefully" + ); + }); +} diff --git a/packages/infra/engine/tests/request_headers.rs b/packages/infra/engine/tests/request_headers.rs new file mode 100644 index 0000000000..b60c4b5de8 --- /dev/null +++ b/packages/infra/engine/tests/request_headers.rs @@ -0,0 +1,219 @@ +mod common; + +use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE, ACCEPT, USER_AGENT}; +use serde_json::json; + +#[test] +fn test_content_type_header_validation() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + let body = json!({ + "name": "test-actor", + "runner_name_selector": "test-runner", + "crash_policy": "destroy" + }); + + // Test different Content-Type headers + let content_types = vec![ + ("application/json", true), + ("application/json; charset=utf-8", true), + ("application/json; charset=UTF-8", true), + ("text/json", false), // Should be rejected + ("application/xml", false), // Should be rejected + ("text/plain", false), // Should be rejected + ]; + + for (content_type, should_succeed) in content_types { + let response = client + .post(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .header(CONTENT_TYPE, content_type) + .json(&body) + .send() + .await + .expect("Failed to send request"); + + if should_succeed { + assert!( + response.status().is_success(), + "Content-Type '{}' should be accepted", + content_type + ); + } else { + assert!( + response.status() == 400 || response.status() == 415, + "Content-Type '{}' should be rejected with 400 or 415, got: {}", + content_type, + response.status() + ); + } + } + }); +} + +#[test] +fn test_accept_header_handling() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + // Create an actor first + let actor_id = common::create_actor(&namespace, ctx.leader_dc().guard_port()).await; + + let client = reqwest::Client::new(); + + // Test different Accept headers - server should always return JSON + let accept_headers = vec![ + "application/json", + "application/json, text/plain", + "*/*", + "application/*", + "text/xml", // May still work but return JSON + ]; + + for accept_header in accept_headers { + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT, HeaderValue::from_str(accept_header).unwrap()); + + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors/{}?namespace={}", + ctx.leader_dc().guard_port(), + actor_id, + namespace + )) + .headers(headers) + .send() + .await + .expect("Failed to send request"); + + // Should always return JSON regardless of Accept header + assert!( + response.status().is_success(), + "Accept header '{}' should not cause failure", + accept_header + ); + + let content_type = response + .headers() + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + assert!( + content_type.contains("application/json"), + "Response should always be JSON, got: {}", + content_type + ); + } + }); +} + +#[test] +fn test_rivet_specific_headers() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + // Create an actor + let actor_id = common::create_actor(&namespace, ctx.leader_dc().guard_port()).await; + + let client = reqwest::Client::new(); + + // Test Rivet-specific headers for actor communication + let mut headers = HeaderMap::new(); + headers.insert("X-Rivet-Target", HeaderValue::from_static("actor")); + headers.insert("X-Rivet-Actor", HeaderValue::from_str(&actor_id).unwrap()); + headers.insert("X-Rivet-Addr", HeaderValue::from_static("ping")); + + let response = client + .get(&format!( + "http://127.0.0.1:{}/ping", + ctx.leader_dc().guard_port() + )) + .headers(headers) + .send() + .await + .expect("Failed to send request"); + + // This test verifies Rivet-specific headers are processed correctly + assert!( + response.status().as_u16() > 0, + "Should receive valid HTTP response with Rivet headers" + ); + }); +} + +#[test] +fn test_header_case_insensitivity() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test case variations of important headers + let mut headers = HeaderMap::new(); + headers.insert("content-type", HeaderValue::from_static("application/json")); + headers.insert("ACCEPT", HeaderValue::from_static("application/json")); + headers.insert("User-Agent", HeaderValue::from_static("test-client")); + + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .query(&[("name", "test-actor")]) + .headers(headers) + .send() + .await + .expect("Failed to send request"); + + // Headers should be handled case-insensitively + assert!( + response.status().is_success(), + "Mixed case headers should work" + ); + }); +} + +#[test] +fn test_ignored_headers() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test that custom/unknown headers are ignored gracefully + let mut headers = HeaderMap::new(); + headers.insert("X-Custom-Header", HeaderValue::from_static("test-value")); + headers.insert("X-Request-ID", HeaderValue::from_static("req-123")); + headers.insert("X-Client-Version", HeaderValue::from_static("1.0.0")); + headers.insert("Authorization", HeaderValue::from_static("Bearer token123")); + + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors?namespace={}", + ctx.leader_dc().guard_port(), + namespace + )) + .query(&[("name", "test-actor")]) + .headers(headers) + .send() + .await + .expect("Failed to send request"); + + // Custom headers should be ignored and not cause issues + assert!( + response.status().is_success(), + "Custom/unknown headers should not cause failure" + ); + }); +} \ No newline at end of file diff --git a/packages/infra/engine/tests/response_format.rs b/packages/infra/engine/tests/response_format.rs new file mode 100644 index 0000000000..4251027635 --- /dev/null +++ b/packages/infra/engine/tests/response_format.rs @@ -0,0 +1,245 @@ +mod common; + +use reqwest::header::CONTENT_TYPE; +use serde_json::Value; + +#[test] +fn test_response_structure_consistency() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + // Create an actor + let actor_id = common::create_actor(&namespace, ctx.leader_dc().guard_port()).await; + + let client = reqwest::Client::new(); + + // Test GET actor response structure + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors/{}?namespace={}", + ctx.leader_dc().guard_port(), + actor_id, + namespace + )) + .send() + .await + .expect("Failed to send request"); + + assert!(response.status().is_success()); + let body: Value = response.json().await.expect("Failed to parse JSON"); + + // Verify response structure + assert!(body.get("actor").is_some(), "Response should have 'actor' field"); + let actor = &body["actor"]; + assert!(actor.get("actor_id").is_some(), "Actor should have 'actor_id'"); + assert!(actor.get("name").is_some(), "Actor should have 'name'"); + assert!(actor.get("create_ts").is_some(), "Actor should have 'create_ts'"); + + // Test LIST actors response structure + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors?namespace={}&name=test-actor", + ctx.leader_dc().guard_port(), + namespace + )) + .send() + .await + .expect("Failed to send request"); + + assert!(response.status().is_success()); + let body: Value = response.json().await.expect("Failed to parse JSON"); + + // Verify list response structure + assert!(body.get("actors").is_some(), "Response should have 'actors' array"); + assert!(body["actors"].is_array(), "actors should be an array"); + }); +} + +#[test] +fn test_error_response_structure() { + common::run(common::TestOpts::new(1), |ctx| async move { + let client = reqwest::Client::new(); + + // Test error response structure for non-existent namespace + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors?namespace=non-existent&name=test", + ctx.leader_dc().guard_port() + )) + .send() + .await + .expect("Failed to send request"); + + assert!(!response.status().is_success()); + let body: Value = response.json().await.expect("Failed to parse JSON"); + + // Verify error response structure + assert!(body.get("code").is_some(), "Error should have 'code'"); + assert!( + body.get("message").is_some() || body.get("description").is_some(), + "Error should have 'message' or 'description'" + ); + }); +} + +#[test] +fn test_response_content_type() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test different endpoints return proper JSON content type + let endpoints = vec![ + ("GET", format!("/actors?namespace={}&name=test", namespace)), + ("GET", format!("/actors/names?namespace={}", namespace)), + ]; + + for (method, path) in endpoints { + let response = match method { + "GET" => client + .get(&format!("http://127.0.0.1:{}{}", ctx.leader_dc().guard_port(), path)) + .send() + .await + .expect("Failed to send GET request"), + _ => panic!("Unsupported method: {}", method), + }; + + assert!(response.status().is_success(), "Request should succeed"); + + let content_type = response + .headers() + .get(CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + assert!( + content_type.contains("application/json"), + "Response should be JSON for {} {}, got: {}", + method, + path, + content_type + ); + } + }); +} + +#[test] +fn test_response_encoding_handling() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + let client = reqwest::Client::new(); + + // Test response with Accept-Encoding header + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors?namespace={}&name=test", + ctx.leader_dc().guard_port(), + namespace + )) + .header("Accept-Encoding", "gzip, deflate") + .send() + .await + .expect("Failed to send request"); + + assert!(response.status().is_success()); + + // Verify we can parse the response regardless of encoding + let body: Value = response + .json() + .await + .expect("Failed to parse response (encoding issue?)"); + + assert!(body.is_object(), "Response should be parseable JSON"); + }); +} + +#[test] +fn test_large_response_handling() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + // Create many actors to generate a large response + let actor_ids = common::bulk_create_actors( + &namespace, + "large-response", + 32, + ctx.leader_dc().guard_port(), + ) + .await; + + // List all actors to get large response + let response = common::list_actors( + &namespace, + None, + None, + Some(actor_ids), + None, + Some(32), + None, + ctx.leader_dc().guard_port() + ).await; + + assert!(response.status().is_success()); + + let body: Value = response + .json() + .await + .expect("Failed to parse large response"); + + // Verify large response structure + assert!(body["actors"].is_array()); + let actors = body["actors"].as_array().unwrap(); + assert!( + actors.len() == 32, + "Should return all created actors: {} != 32", + actors.len() + ); + }); +} + +#[test] +fn test_field_type_consistency() { + common::run(common::TestOpts::new(1), |ctx| async move { + let (namespace, _, _runner) = + common::setup_test_namespace_with_runner(ctx.leader_dc()).await; + + // Create an actor + let actor_id = common::create_actor(&namespace, ctx.leader_dc().guard_port()).await; + + let client = reqwest::Client::new(); + let response = client + .get(&format!( + "http://127.0.0.1:{}/actors/{}?namespace={}", + ctx.leader_dc().guard_port(), + actor_id, + namespace + )) + .send() + .await + .expect("Failed to send request"); + + assert!(response.status().is_success()); + let body: Value = response.json().await.expect("Failed to parse JSON"); + + let actor = &body["actor"]; + + // Verify field types are consistent + assert!(actor["actor_id"].is_string(), "actor_id should be string"); + assert!(actor["name"].is_string(), "name should be string"); + + // Timestamps should be numbers (i64 milliseconds) + if let Some(create_ts) = actor.get("create_ts") { + assert!(create_ts.is_number(), "create_ts should be number"); + } + + if let Some(destroy_ts) = actor.get("destroy_ts") { + assert!(destroy_ts.is_number() || destroy_ts.is_null(), "destroy_ts should be number or null"); + } + }); +} diff --git a/packages/infra/engine/tests/runners_version.rs b/packages/infra/engine/tests/runners_version.rs index 156893cab8..bb6a0ce5ff 100644 --- a/packages/infra/engine/tests/runners_version.rs +++ b/packages/infra/engine/tests/runners_version.rs @@ -14,7 +14,6 @@ fn runner_version_upgrade() { namespace: namespace.clone(), name: "actor1".to_string(), key: None, - datacenter: None, ..Default::default() }, ctx.leader_dc().guard_port(), @@ -33,7 +32,6 @@ fn runner_version_upgrade() { namespace: namespace.clone(), name: "actor2".to_string(), key: None, - datacenter: None, ..Default::default() }, ctx.leader_dc().guard_port(),