client tests now have a proper structure
This commit is contained in:
1
client/tests/form/gui/mod.rs
Normal file
1
client/tests/form/gui/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod form_tests;
|
||||
2
client/tests/form/mod.rs
Normal file
2
client/tests/form/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod gui;
|
||||
pub mod requests;
|
||||
159
client/tests/form/requests/form_request_tests.rs
Normal file
159
client/tests/form/requests/form_request_tests.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
// client/tests/form_request_tests.rs
|
||||
use rstest::{fixture, rstest};
|
||||
use client::services::grpc_client::GrpcClient;
|
||||
use client::state::pages::form::FormState;
|
||||
use client::state::pages::canvas_state::CanvasState;
|
||||
use prost_types::Value;
|
||||
use prost_types::value::Kind;
|
||||
use std::collections::HashMap;
|
||||
use tonic::Status;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
// ========================================================================
|
||||
// HELPER FUNCTIONS AND UTILITIES
|
||||
// ========================================================================
|
||||
|
||||
/// Generate unique identifiers for test isolation using timestamp
|
||||
fn generate_unique_id() -> String {
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
// Ensure we always get a 12-character hex string by padding with zeros
|
||||
format!("{:012x}", timestamp % 1_000_000_000_000u128)
|
||||
}
|
||||
|
||||
/// Helper function to create string value
|
||||
fn create_string_value(s: &str) -> Value {
|
||||
Value {
|
||||
kind: Some(Kind::StringValue(s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to create number value
|
||||
fn create_number_value(n: f64) -> Value {
|
||||
Value {
|
||||
kind: Some(Kind::NumberValue(n))
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to create boolean value
|
||||
fn create_bool_value(b: bool) -> Value {
|
||||
Value {
|
||||
kind: Some(Kind::BoolValue(b))
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to create null value
|
||||
fn create_null_value() -> Value {
|
||||
Value {
|
||||
kind: Some(Kind::NullValue(0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if backend is available
|
||||
async fn is_backend_available() -> bool {
|
||||
if std::env::var("SKIP_BACKEND_TESTS").is_ok() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match GrpcClient::new().await {
|
||||
Ok(_) => true,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip test if backend is not available
|
||||
macro_rules! skip_if_backend_unavailable {
|
||||
() => {
|
||||
if !is_backend_available().await {
|
||||
println!("Backend unavailable - skipping test");
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TEST CONTEXT AND FIXTURES
|
||||
// ========================================================================
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FormTestContext {
|
||||
client: GrpcClient,
|
||||
profile_name: String,
|
||||
table_name: String,
|
||||
}
|
||||
|
||||
impl FormTestContext {
|
||||
/// Create test form data for insertion
|
||||
fn create_test_form_data(&self) -> HashMap<String, Value> {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value("Test Company Ltd"));
|
||||
data.insert("telefon".to_string(), create_string_value("+421123456789"));
|
||||
data.insert("email".to_string(), create_string_value("test@company.com"));
|
||||
data.insert("kz".to_string(), create_string_value("KZ123"));
|
||||
data.insert("ulica".to_string(), create_string_value("Test Street 123"));
|
||||
data.insert("mesto".to_string(), create_string_value("Test City"));
|
||||
data
|
||||
}
|
||||
|
||||
/// Create minimal valid form data
|
||||
fn create_minimal_form_data(&self) -> HashMap<String, Value> {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value("Minimal Company"));
|
||||
data
|
||||
}
|
||||
|
||||
/// Create form data with invalid fields
|
||||
fn create_invalid_form_data(&self) -> HashMap<String, Value> {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value("Test Company"));
|
||||
data.insert("nonexistent_field".to_string(), create_string_value("Invalid"));
|
||||
data
|
||||
}
|
||||
|
||||
/// Create form data with type mismatches
|
||||
fn create_type_mismatch_data(&self) -> HashMap<String, Value> {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value("Test Company"));
|
||||
data.insert("age".to_string(), create_string_value("thirty")); // String for number field
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
async fn form_test_context() -> FormTestContext {
|
||||
let client = GrpcClient::new()
|
||||
.await
|
||||
.expect("Failed to create gRPC client for test");
|
||||
|
||||
let unique_id = generate_unique_id();
|
||||
let profile_name = format!("test_profile_{}", unique_id);
|
||||
let table_name = format!("test_table_{}", unique_id);
|
||||
|
||||
FormTestContext {
|
||||
client,
|
||||
profile_name,
|
||||
table_name,
|
||||
}
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
async fn populated_test_context() -> FormTestContext {
|
||||
let mut context = form_test_context().await;
|
||||
|
||||
// Pre-populate with test data if backend is available
|
||||
if is_backend_available().await {
|
||||
let test_data = context.create_test_form_data();
|
||||
let _ = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
test_data,
|
||||
).await;
|
||||
}
|
||||
|
||||
context
|
||||
}
|
||||
|
||||
include!("form_request_tests2.rs");
|
||||
859
client/tests/form/requests/form_request_tests2.rs
Normal file
859
client/tests/form/requests/form_request_tests2.rs
Normal file
@@ -0,0 +1,859 @@
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_post_table_data_success(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let test_data = context.create_test_form_data();
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
test_data.clone(),
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
assert!(response.success, "POST operation should succeed");
|
||||
assert!(response.inserted_id > 0, "Should return valid inserted ID");
|
||||
assert!(!response.message.is_empty(), "Should return non-empty message");
|
||||
println!("POST successful: ID {}, Message: {}", response.inserted_id, response.message);
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(status) = e.downcast_ref::<Status>() {
|
||||
if status.code() == tonic::Code::Unavailable {
|
||||
println!("Backend unavailable - test cannot run");
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("POST request failed unexpectedly: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_post_table_data_minimal_data(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let minimal_data = context.create_minimal_form_data();
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
minimal_data,
|
||||
).await;
|
||||
|
||||
assert!(result.is_ok(), "POST with minimal data should succeed");
|
||||
let response = result.unwrap();
|
||||
assert!(response.success);
|
||||
assert!(response.inserted_id > 0);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_get_table_data_count_success(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let result = context.client.get_table_data_count(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(count) => {
|
||||
assert!(count >= 0, "Count should be non-negative");
|
||||
println!("GET count successful: {}", count);
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(status) = e.downcast_ref::<Status>() {
|
||||
if status.code() == tonic::Code::Unavailable {
|
||||
println!("Backend unavailable - test cannot run");
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("GET count request failed unexpectedly: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_get_table_data_by_id_with_existing_record(#[future] populated_test_context: FormTestContext) {
|
||||
let mut context = populated_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// First create a record
|
||||
let test_data = context.create_test_form_data();
|
||||
let post_result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
test_data.clone(),
|
||||
).await;
|
||||
|
||||
if let Ok(post_response) = post_result {
|
||||
let created_id = post_response.inserted_id;
|
||||
|
||||
// Now try to get it
|
||||
let get_result = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
).await;
|
||||
|
||||
match get_result {
|
||||
Ok(response) => {
|
||||
assert!(!response.data.is_empty(), "Should return data fields");
|
||||
println!("GET by ID successful: {} fields", response.data.len());
|
||||
|
||||
// Verify some data matches
|
||||
if let Some(firma_value) = response.data.get("firma") {
|
||||
assert_eq!(firma_value, "Test Company Ltd");
|
||||
}
|
||||
}
|
||||
Err(e) => panic!("GET by ID failed unexpectedly: {}", e),
|
||||
}
|
||||
} else {
|
||||
println!("Could not create test record, skipping GET test");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_get_table_data_by_nonexistent_id(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let nonexistent_id = 99999;
|
||||
let result = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
nonexistent_id,
|
||||
).await;
|
||||
|
||||
assert!(result.is_err(), "GET should fail for nonexistent ID");
|
||||
if let Some(status) = result.unwrap_err().downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_put_table_data_success(#[future] populated_test_context: FormTestContext) {
|
||||
let mut context = populated_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// First create a record
|
||||
let test_data = context.create_test_form_data();
|
||||
let post_result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
test_data,
|
||||
).await;
|
||||
|
||||
if let Ok(post_response) = post_result {
|
||||
let created_id = post_response.inserted_id;
|
||||
|
||||
// Update the record
|
||||
let mut update_data = HashMap::new();
|
||||
update_data.insert("firma".to_string(), create_string_value("Updated Company Name"));
|
||||
update_data.insert("telefon".to_string(), create_string_value("+421987654321"));
|
||||
|
||||
let put_result = context.client.put_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
update_data,
|
||||
).await;
|
||||
|
||||
match put_result {
|
||||
Ok(response) => {
|
||||
assert!(response.success, "PUT operation should succeed");
|
||||
assert_eq!(response.updated_id, created_id, "Should return correct updated ID");
|
||||
println!("PUT successful: ID {}, Message: {}", response.updated_id, response.message);
|
||||
}
|
||||
Err(e) => panic!("PUT request failed unexpectedly: {}", e),
|
||||
}
|
||||
} else {
|
||||
println!("Could not create test record, skipping PUT test");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_put_table_data_nonexistent_id(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let nonexistent_id = 99999;
|
||||
let mut update_data = HashMap::new();
|
||||
update_data.insert("firma".to_string(), create_string_value("Updated Company"));
|
||||
|
||||
let result = context.client.put_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
nonexistent_id,
|
||||
update_data,
|
||||
).await;
|
||||
|
||||
assert!(result.is_err(), "PUT should fail for nonexistent ID");
|
||||
if let Some(status) = result.unwrap_err().downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::NotFound);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_delete_table_data_success(#[future] populated_test_context: FormTestContext) {
|
||||
let mut context = populated_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// First create a record
|
||||
let test_data = context.create_test_form_data();
|
||||
let post_result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
test_data,
|
||||
).await;
|
||||
|
||||
if let Ok(post_response) = post_result {
|
||||
let created_id = post_response.inserted_id;
|
||||
|
||||
let delete_result = context.client.delete_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
).await;
|
||||
|
||||
match delete_result {
|
||||
Ok(response) => {
|
||||
assert!(response.success, "DELETE operation should succeed");
|
||||
println!("DELETE successful for ID {}", created_id);
|
||||
}
|
||||
Err(e) => panic!("DELETE request failed unexpectedly: {}", e),
|
||||
}
|
||||
} else {
|
||||
println!("Could not create test record, skipping DELETE test");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_delete_table_data_nonexistent_id(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let nonexistent_id = 99999;
|
||||
let result = context.client.delete_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
nonexistent_id,
|
||||
).await;
|
||||
|
||||
// DELETE should succeed even for nonexistent IDs (idempotent operation)
|
||||
assert!(result.is_ok(), "DELETE should not fail for nonexistent ID");
|
||||
let response = result.unwrap();
|
||||
assert!(response.success, "DELETE should report success even for nonexistent ID");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// ERROR HANDLING AND VALIDATION TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_invalid_profile_and_table_errors(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let bogus_profile = "profile_does_not_exist".to_string();
|
||||
let bogus_table = "table_does_not_exist".to_string();
|
||||
|
||||
let result = context.client.get_table_data_count(bogus_profile, bogus_table).await;
|
||||
|
||||
assert!(result.is_err(), "Expected error for non-existent profile/table");
|
||||
if let Some(status) = result.unwrap_err().downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::NotFound, "Expected NotFound for non-existent profile");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_invalid_column_validation(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let invalid_data = context.create_invalid_form_data();
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
invalid_data,
|
||||
).await;
|
||||
|
||||
assert!(result.is_err(), "Expected error for undefined column");
|
||||
if let Some(status) = result.unwrap_err().downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::InvalidArgument);
|
||||
assert!(status.message().contains("Invalid column") ||
|
||||
status.message().contains("nonexistent"));
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_data_type_validation(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let type_mismatch_data = context.create_type_mismatch_data();
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
type_mismatch_data,
|
||||
).await;
|
||||
|
||||
assert!(result.is_err(), "Expected error for wrong data type");
|
||||
if let Some(status) = result.unwrap_err().downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::InvalidArgument);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_empty_data_validation(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let empty_data = HashMap::new();
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
empty_data,
|
||||
).await;
|
||||
|
||||
assert!(result.is_err(), "Expected error for empty data");
|
||||
if let Some(status) = result.unwrap_err().downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::InvalidArgument);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// SOFT DELETE BEHAVIOR TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_soft_delete_behavior_comprehensive(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// 1. Create a record
|
||||
let test_data = context.create_test_form_data();
|
||||
let post_result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
test_data,
|
||||
).await;
|
||||
|
||||
if let Ok(post_response) = post_result {
|
||||
let record_id = post_response.inserted_id;
|
||||
println!("Created record with ID {}", record_id);
|
||||
|
||||
// 2. Verify count before deletion
|
||||
let count_before = context.client.get_table_data_count(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone()
|
||||
).await.unwrap_or(0);
|
||||
assert!(count_before >= 1, "Count should be at least 1 after creation");
|
||||
|
||||
// 3. Soft-delete the record
|
||||
let delete_result = context.client.delete_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
record_id,
|
||||
).await;
|
||||
|
||||
assert!(delete_result.is_ok(), "Delete operation should succeed");
|
||||
println!("Soft-deleted record {}", record_id);
|
||||
|
||||
// 4. Verify count decreased after deletion
|
||||
let count_after = context.client.get_table_data_count(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone()
|
||||
).await.unwrap_or(0);
|
||||
assert_eq!(count_after, count_before - 1, "Count should decrease by 1 after soft delete");
|
||||
|
||||
// 5. Try to GET the soft-deleted record
|
||||
let get_result = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
record_id,
|
||||
).await;
|
||||
|
||||
assert!(get_result.is_err(), "Should not be able to GET a soft-deleted record");
|
||||
if let Some(status) = get_result.unwrap_err().downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::NotFound);
|
||||
}
|
||||
println!("Correctly failed to GET soft-deleted record");
|
||||
} else {
|
||||
println!("Could not create test record for soft delete test");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// POSITIONAL RETRIEVAL TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_positional_retrieval_comprehensive(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// 1. Create multiple records
|
||||
let test_names = ["Alice Corp", "Bob Industries", "Charlie Ltd"];
|
||||
let mut created_ids = Vec::new();
|
||||
|
||||
for (i, name) in test_names.iter().enumerate() {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value(name));
|
||||
data.insert("kz".to_string(), create_string_value(&format!("KZ{}", i + 1)));
|
||||
|
||||
if let Ok(response) = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await {
|
||||
created_ids.push(response.inserted_id);
|
||||
}
|
||||
}
|
||||
|
||||
if created_ids.len() < 3 {
|
||||
println!("Could not create enough test records for positional test");
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Created {} records with IDs: {:?}", created_ids.len(), created_ids);
|
||||
|
||||
// 2. Test valid positional retrieval
|
||||
for i in 0..3 {
|
||||
let position = (i + 1) as i32;
|
||||
let result = context.client.get_table_data_by_position(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
position,
|
||||
).await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
assert!(!response.data.is_empty(), "Position {} should return data", position);
|
||||
if let Some(firma_value) = response.data.get("firma") {
|
||||
assert!(test_names.contains(&firma_value.as_str()),
|
||||
"Returned firma '{}' should be one of our test names", firma_value);
|
||||
}
|
||||
println!("Successfully retrieved record at position {}", position);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to get record at position {}: {}", position, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Test out-of-bounds position
|
||||
let oob_position = 100;
|
||||
let result_oob = context.client.get_table_data_by_position(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
oob_position,
|
||||
).await;
|
||||
|
||||
assert!(result_oob.is_err(), "Should fail for out-of-bounds position");
|
||||
if let Some(status) = result_oob.unwrap_err().downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::NotFound);
|
||||
}
|
||||
|
||||
// 4. Test invalid position (≤ 0)
|
||||
let invalid_positions = [0, -1, -5];
|
||||
for invalid_pos in invalid_positions {
|
||||
let result_invalid = context.client.get_table_data_by_position(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
invalid_pos,
|
||||
).await;
|
||||
|
||||
assert!(result_invalid.is_err(), "Should fail for invalid position {}", invalid_pos);
|
||||
if let Some(status) = result_invalid.unwrap_err().downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::InvalidArgument);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// WORKFLOW AND INTEGRATION TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_complete_crud_workflow(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let test_data = context.create_test_form_data();
|
||||
|
||||
// 1. CREATE - Post data
|
||||
let post_result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
test_data.clone(),
|
||||
).await;
|
||||
|
||||
let created_id = match post_result {
|
||||
Ok(response) => {
|
||||
assert!(response.success, "POST should succeed");
|
||||
println!("Workflow: Created record with ID {}", response.inserted_id);
|
||||
response.inserted_id
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some(status) = e.downcast_ref::<Status>() {
|
||||
if status.code() == tonic::Code::Unavailable {
|
||||
println!("Workflow test skipped - backend not available");
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("Workflow POST failed unexpectedly: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
// 2. READ - Get the created data
|
||||
let get_result = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
).await;
|
||||
|
||||
let get_response = get_result.expect("Workflow GET should succeed");
|
||||
if let Some(firma_value) = get_response.data.get("firma") {
|
||||
assert_eq!(firma_value, "Test Company Ltd", "Retrieved data should match created data");
|
||||
}
|
||||
println!("Workflow: Verified created data");
|
||||
|
||||
// 3. UPDATE - Modify the data
|
||||
let mut update_data = HashMap::new();
|
||||
update_data.insert("firma".to_string(), create_string_value("Updated in Workflow"));
|
||||
update_data.insert("telefon".to_string(), create_string_value("+421999888777"));
|
||||
|
||||
let put_result = context.client.put_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
update_data,
|
||||
).await;
|
||||
|
||||
let put_response = put_result.expect("Workflow PUT should succeed");
|
||||
assert!(put_response.success, "PUT should succeed");
|
||||
assert_eq!(put_response.updated_id, created_id, "PUT should return correct ID");
|
||||
println!("Workflow: Updated record");
|
||||
|
||||
// 4. VERIFY UPDATE - Get updated data
|
||||
let get_updated_result = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
).await;
|
||||
|
||||
let get_updated_response = get_updated_result.expect("Workflow GET after update should succeed");
|
||||
if let Some(firma_value) = get_updated_response.data.get("firma") {
|
||||
assert_eq!(firma_value, "Updated in Workflow", "Data should be updated");
|
||||
}
|
||||
println!("Workflow: Verified updated data");
|
||||
|
||||
// 5. DELETE - Remove the data
|
||||
let delete_result = context.client.delete_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
).await;
|
||||
|
||||
let delete_response = delete_result.expect("Workflow DELETE should succeed");
|
||||
assert!(delete_response.success, "DELETE should succeed");
|
||||
println!("Workflow: Deleted record");
|
||||
|
||||
// 6. VERIFY DELETE - Ensure data is gone
|
||||
let get_deleted_result = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
).await;
|
||||
|
||||
assert!(get_deleted_result.is_err(), "Should not be able to GET deleted record");
|
||||
if let Some(status) = get_deleted_result.unwrap_err().downcast_ref::<Status>() {
|
||||
assert_eq!(status.code(), tonic::Code::NotFound);
|
||||
}
|
||||
println!("Workflow: Verified record deletion");
|
||||
|
||||
println!("Complete CRUD workflow test successful for ID {}", created_id);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// FORM STATE INTEGRATION TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_form_state_integration(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Create a form state
|
||||
let mut form_state = FormState::new(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
vec![], // columns would be populated in real use
|
||||
);
|
||||
|
||||
// Test count update
|
||||
let count_result = context.client.get_table_data_count(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
).await;
|
||||
|
||||
if let Ok(count) = count_result {
|
||||
form_state.total_count = count;
|
||||
assert_eq!(form_state.total_count, count, "Form state count should match backend");
|
||||
println!("Form state updated with count: {}", form_state.total_count);
|
||||
}
|
||||
|
||||
// Create a test record for form state testing
|
||||
let test_data = context.create_test_form_data();
|
||||
if let Ok(post_response) = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
test_data,
|
||||
).await {
|
||||
let created_id = post_response.inserted_id;
|
||||
|
||||
// Test form state update from backend response
|
||||
if let Ok(get_response) = context.client.get_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
created_id,
|
||||
).await {
|
||||
form_state.update_from_response(&get_response.data, created_id as u64);
|
||||
assert_eq!(form_state.current_position, created_id as u64, "Form state position should match");
|
||||
assert!(!form_state.has_unsaved_changes(), "Form state should not have unsaved changes after update");
|
||||
println!("Form state successfully updated from backend data");
|
||||
}
|
||||
} else {
|
||||
println!("Could not create test record for form state test");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// CONCURRENT OPERATIONS TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_concurrent_post_operations(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Create multiple concurrent POST operations using tokio::spawn
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for i in 0..5 {
|
||||
let context_clone = context.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut data = context_clone.create_test_form_data();
|
||||
data.insert("firma".to_string(), create_string_value(&format!("Concurrent Company {}", i)));
|
||||
data.insert("kz".to_string(), create_string_value(&format!("CONC{}", i)));
|
||||
|
||||
let mut client = context_clone.client;
|
||||
client.post_table_data(
|
||||
context_clone.profile_name.clone(),
|
||||
context_clone.table_name.clone(),
|
||||
data,
|
||||
).await
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
let mut success_count = 0;
|
||||
for (i, handle) in handles.into_iter().enumerate() {
|
||||
match handle.await {
|
||||
Ok(Ok(response)) => {
|
||||
assert!(response.success, "Concurrent POST {} should succeed", i);
|
||||
success_count += 1;
|
||||
}
|
||||
Ok(Err(_)) => {
|
||||
println!("Concurrent POST {} failed (may be expected if backend issues)", i);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Concurrent task {} panicked: {}", i, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Concurrent operations: {}/{} succeeded", success_count, 5);
|
||||
assert!(success_count > 0, "At least some concurrent operations should succeed");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// PERFORMANCE AND STRESS TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_rapid_sequential_operations(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let operation_count = 10;
|
||||
let mut successful_operations = 0;
|
||||
|
||||
for i in 0..operation_count {
|
||||
let mut data = context.create_test_form_data();
|
||||
data.insert("firma".to_string(), create_string_value(&format!("Rapid Company {}", i)));
|
||||
data.insert("kz".to_string(), create_string_value(&format!("RAP{}", i)));
|
||||
|
||||
if let Ok(response) = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await {
|
||||
assert!(response.success, "Rapid operation {} should succeed", i);
|
||||
successful_operations += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let duration = start_time.elapsed();
|
||||
println!("{} rapid operations took: {:?}", operation_count, duration);
|
||||
println!("Success rate: {}/{}", successful_operations, operation_count);
|
||||
|
||||
assert!(successful_operations > 0, "At least some rapid operations should succeed");
|
||||
assert!(duration.as_secs() < 30, "Rapid operations should complete in reasonable time");
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// CONNECTION AND CLIENT TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_grpc_client_connection() {
|
||||
if std::env::var("SKIP_BACKEND_TESTS").is_ok() {
|
||||
println!("Connection test skipped due to SKIP_BACKEND_TESTS");
|
||||
return;
|
||||
}
|
||||
|
||||
let client_result = GrpcClient::new().await;
|
||||
match client_result {
|
||||
Ok(_) => println!("gRPC client connection test passed"),
|
||||
Err(e) => {
|
||||
println!("gRPC client connection failed (expected if backend not running): {}", e);
|
||||
// Don't panic - this is expected when backend is not available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_client_timeout_handling(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
// Test that operations complete within reasonable timeouts
|
||||
let timeout_duration = Duration::from_secs(10);
|
||||
|
||||
let count_result = timeout(
|
||||
timeout_duration,
|
||||
context.client.get_table_data_count(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
)
|
||||
).await;
|
||||
|
||||
match count_result {
|
||||
Ok(Ok(count)) => {
|
||||
println!("Count operation completed within timeout: {}", count);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("Count operation failed: {}", e);
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Count operation timed out after {:?}", timeout_duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// DATA EDGE CASES TESTS
|
||||
// ========================================================================
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_special_characters_and_unicode(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let special_strings = vec![
|
||||
"José María González",
|
||||
"Москва",
|
||||
"北京市",
|
||||
"🚀 Tech Company 🌟",
|
||||
"Quote\"Test'Apostrophe",
|
||||
"Price: $1,000.50 (50% off!)",
|
||||
];
|
||||
|
||||
for (i, test_string) in special_strings.iter().enumerate() {
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value(test_string));
|
||||
data.insert("kz".to_string(), create_string_value(&format!("UNI{}", i)));
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await;
|
||||
|
||||
if let Ok(response) = result {
|
||||
assert!(response.success, "Should handle special characters: '{}'", test_string);
|
||||
println!("Successfully handled special string: '{}'", test_string);
|
||||
} else {
|
||||
println!("Failed to handle special string: '{}' (may be expected)", test_string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_null_and_empty_values(#[future] form_test_context: FormTestContext) {
|
||||
let mut context = form_test_context.await;
|
||||
skip_if_backend_unavailable!();
|
||||
|
||||
let mut data = HashMap::new();
|
||||
data.insert("firma".to_string(), create_string_value("Null Test Company"));
|
||||
data.insert("telefon".to_string(), create_null_value());
|
||||
data.insert("email".to_string(), create_string_value(""));
|
||||
data.insert("ulica".to_string(), create_string_value(" ")); // Whitespace only
|
||||
|
||||
let result = context.client.post_table_data(
|
||||
context.profile_name.clone(),
|
||||
context.table_name.clone(),
|
||||
data,
|
||||
).await;
|
||||
|
||||
if let Ok(response) = result {
|
||||
assert!(response.success, "Should handle null and empty values");
|
||||
println!("Successfully handled null and empty values");
|
||||
} else {
|
||||
println!("Failed to handle null and empty values (may be expected based on validation)");
|
||||
}
|
||||
}
|
||||
|
||||
1
client/tests/form/requests/mod.rs
Normal file
1
client/tests/form/requests/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod form_request_tests;
|
||||
@@ -1,3 +1,3 @@
|
||||
// tests/mod.rs
|
||||
|
||||
pub mod form_tests;
|
||||
pub mod form;
|
||||
|
||||
Reference in New Issue
Block a user