client tests now have a proper structure
This commit is contained in:
@@ -14,7 +14,10 @@ use common::proto::multieko2::table_script::{
|
|||||||
use common::proto::multieko2::tables_data::{
|
use common::proto::multieko2::tables_data::{
|
||||||
tables_data_client::TablesDataClient,
|
tables_data_client::TablesDataClient,
|
||||||
GetTableDataByPositionRequest,
|
GetTableDataByPositionRequest,
|
||||||
|
GetTableDataRequest, // ADD THIS
|
||||||
GetTableDataResponse,
|
GetTableDataResponse,
|
||||||
|
DeleteTableDataRequest, // ADD THIS
|
||||||
|
DeleteTableDataResponse, // ADD THIS
|
||||||
GetTableDataCountRequest,
|
GetTableDataCountRequest,
|
||||||
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
|
PostTableDataRequest, PostTableDataResponse, PutTableDataRequest,
|
||||||
PutTableDataResponse,
|
PutTableDataResponse,
|
||||||
@@ -116,7 +119,7 @@ impl GrpcClient {
|
|||||||
Ok(response.into_inner())
|
Ok(response.into_inner())
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW Methods for TablesData service
|
// Existing TablesData methods
|
||||||
pub async fn get_table_data_count(
|
pub async fn get_table_data_count(
|
||||||
&mut self,
|
&mut self,
|
||||||
profile_name: String,
|
profile_name: String,
|
||||||
@@ -135,7 +138,7 @@ impl GrpcClient {
|
|||||||
Ok(response.into_inner().count as u64)
|
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,
|
&mut self,
|
||||||
profile_name: String,
|
profile_name: String,
|
||||||
table_name: String,
|
table_name: String,
|
||||||
@@ -155,18 +158,58 @@ pub async fn get_table_data_by_position(
|
|||||||
Ok(response.into_inner())
|
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(
|
pub async fn post_table_data(
|
||||||
&mut self,
|
&mut self,
|
||||||
profile_name: String,
|
profile_name: String,
|
||||||
table_name: String,
|
table_name: String,
|
||||||
// CHANGE THIS: Accept the pre-converted data
|
|
||||||
data: HashMap<String, Value>,
|
data: HashMap<String, Value>,
|
||||||
) -> Result<PostTableDataResponse> {
|
) -> Result<PostTableDataResponse> {
|
||||||
// The conversion logic is now gone from here.
|
|
||||||
let grpc_request = PostTableDataRequest {
|
let grpc_request = PostTableDataRequest {
|
||||||
profile_name,
|
profile_name,
|
||||||
table_name,
|
table_name,
|
||||||
data, // This is now the correct type
|
data,
|
||||||
};
|
};
|
||||||
let request = tonic::Request::new(grpc_request);
|
let request = tonic::Request::new(grpc_request);
|
||||||
let response = self
|
let response = self
|
||||||
@@ -182,15 +225,13 @@ pub async fn get_table_data_by_position(
|
|||||||
profile_name: String,
|
profile_name: String,
|
||||||
table_name: String,
|
table_name: String,
|
||||||
id: i64,
|
id: i64,
|
||||||
// CHANGE THIS: Accept the pre-converted data
|
|
||||||
data: HashMap<String, Value>,
|
data: HashMap<String, Value>,
|
||||||
) -> Result<PutTableDataResponse> {
|
) -> Result<PutTableDataResponse> {
|
||||||
// The conversion logic is now gone from here.
|
|
||||||
let grpc_request = PutTableDataRequest {
|
let grpc_request = PutTableDataRequest {
|
||||||
profile_name,
|
profile_name,
|
||||||
table_name,
|
table_name,
|
||||||
id,
|
id,
|
||||||
data, // This is now the correct type
|
data,
|
||||||
};
|
};
|
||||||
let request = tonic::Request::new(grpc_request);
|
let request = tonic::Request::new(grpc_request);
|
||||||
let response = self
|
let response = self
|
||||||
|
|||||||
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
|
// tests/mod.rs
|
||||||
|
|
||||||
pub mod form_tests;
|
pub mod form;
|
||||||
|
|||||||
Reference in New Issue
Block a user