client tests now have a proper structure

This commit is contained in:
filipriec
2025-06-26 11:56:18 +02:00
parent a7457f5749
commit 5cd324b6ae
8 changed files with 1072 additions and 9 deletions

View File

@@ -14,7 +14,10 @@ use common::proto::multieko2::table_script::{
use common::proto::multieko2::tables_data::{
tables_data_client::TablesDataClient,
GetTableDataByPositionRequest,
GetTableDataRequest, // ADD THIS
GetTableDataResponse,
DeleteTableDataRequest, // ADD THIS
DeleteTableDataResponse, // ADD THIS
GetTableDataCountRequest,
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
PutTableDataResponse,
@@ -116,7 +119,7 @@ impl GrpcClient {
Ok(response.into_inner())
}
// NEW Methods for TablesData service
// Existing TablesData methods
pub async fn get_table_data_count(
&mut self,
profile_name: String,
@@ -135,7 +138,7 @@ impl GrpcClient {
Ok(response.into_inner().count as u64)
}
pub async fn get_table_data_by_position(
pub async fn get_table_data_by_position(
&mut self,
profile_name: String,
table_name: String,
@@ -155,18 +158,58 @@ pub async fn get_table_data_by_position(
Ok(response.into_inner())
}
// ADD THIS: Missing get_table_data method
pub async fn get_table_data(
&mut self,
profile_name: String,
table_name: String,
id: i64,
) -> Result<GetTableDataResponse> {
let grpc_request = GetTableDataRequest {
profile_name,
table_name,
id,
};
let request = tonic::Request::new(grpc_request);
let response = self
.tables_data_client
.get_table_data(request)
.await
.context("gRPC GetTableData call failed")?;
Ok(response.into_inner())
}
// ADD THIS: Missing delete_table_data method
pub async fn delete_table_data(
&mut self,
profile_name: String,
table_name: String,
record_id: i64,
) -> Result<DeleteTableDataResponse> {
let grpc_request = DeleteTableDataRequest {
profile_name,
table_name,
record_id,
};
let request = tonic::Request::new(grpc_request);
let response = self
.tables_data_client
.delete_table_data(request)
.await
.context("gRPC DeleteTableData call failed")?;
Ok(response.into_inner())
}
pub async fn post_table_data(
&mut self,
profile_name: String,
table_name: String,
// CHANGE THIS: Accept the pre-converted data
data: HashMap<String, Value>,
) -> Result<PostTableDataResponse> {
// The conversion logic is now gone from here.
let grpc_request = PostTableDataRequest {
profile_name,
table_name,
data, // This is now the correct type
data,
};
let request = tonic::Request::new(grpc_request);
let response = self
@@ -182,15 +225,13 @@ pub async fn get_table_data_by_position(
profile_name: String,
table_name: String,
id: i64,
// CHANGE THIS: Accept the pre-converted data
data: HashMap<String, Value>,
) -> Result<PutTableDataResponse> {
// The conversion logic is now gone from here.
let grpc_request = PutTableDataRequest {
profile_name,
table_name,
id,
data, // This is now the correct type
data,
};
let request = tonic::Request::new(grpc_request);
let response = self

View File

@@ -0,0 +1 @@
pub mod form_tests;

2
client/tests/form/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod gui;
pub mod requests;

View 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");

View 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)");
}
}

View File

@@ -0,0 +1 @@
pub mod form_request_tests;

View File

@@ -1,3 +1,3 @@
// tests/mod.rs
pub mod form_tests;
pub mod form;