put working now doing post general test

This commit is contained in:
filipriec
2025-03-03 23:43:43 +01:00
parent eef5efccd4
commit 74f43bdc52
12 changed files with 557 additions and 3 deletions

View File

@@ -6,6 +6,7 @@ import "common.proto";
service TablesData {
rpc PostTableData (PostTableDataRequest) returns (PostTableDataResponse);
rpc PutTableData (PutTableDataRequest) returns (PutTableDataResponse);
}
message PostTableDataRequest {
@@ -19,3 +20,16 @@ message PostTableDataResponse {
string message = 2;
int64 inserted_id = 3;
}
message PutTableDataRequest {
string profile_name = 1;
string table_name = 2;
int64 id = 3;
map<string, string> data = 4;
}
message PutTableDataResponse {
bool success = 1;
string message = 2;
int64 updated_id = 3;
}

Binary file not shown.

View File

@@ -20,6 +20,29 @@ pub struct PostTableDataResponse {
#[prost(int64, tag = "3")]
pub inserted_id: i64,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PutTableDataRequest {
#[prost(string, tag = "1")]
pub profile_name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub table_name: ::prost::alloc::string::String,
#[prost(int64, tag = "3")]
pub id: i64,
#[prost(map = "string, string", tag = "4")]
pub data: ::std::collections::HashMap<
::prost::alloc::string::String,
::prost::alloc::string::String,
>,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PutTableDataResponse {
#[prost(bool, tag = "1")]
pub success: bool,
#[prost(string, tag = "2")]
pub message: ::prost::alloc::string::String,
#[prost(int64, tag = "3")]
pub updated_id: i64,
}
/// Generated client implementations.
pub mod tables_data_client {
#![allow(
@@ -137,6 +160,32 @@ pub mod tables_data_client {
);
self.inner.unary(req, path, codec).await
}
pub async fn put_table_data(
&mut self,
request: impl tonic::IntoRequest<super::PutTableDataRequest>,
) -> std::result::Result<
tonic::Response<super::PutTableDataResponse>,
tonic::Status,
> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/multieko2.tables_data.TablesData/PutTableData",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(
GrpcMethod::new("multieko2.tables_data.TablesData", "PutTableData"),
);
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
@@ -159,6 +208,13 @@ pub mod tables_data_server {
tonic::Response<super::PostTableDataResponse>,
tonic::Status,
>;
async fn put_table_data(
&self,
request: tonic::Request<super::PutTableDataRequest>,
) -> std::result::Result<
tonic::Response<super::PutTableDataResponse>,
tonic::Status,
>;
}
#[derive(Debug)]
pub struct TablesDataServer<T> {
@@ -281,6 +337,51 @@ pub mod tables_data_server {
};
Box::pin(fut)
}
"/multieko2.tables_data.TablesData/PutTableData" => {
#[allow(non_camel_case_types)]
struct PutTableDataSvc<T: TablesData>(pub Arc<T>);
impl<
T: TablesData,
> tonic::server::UnaryService<super::PutTableDataRequest>
for PutTableDataSvc<T> {
type Response = super::PutTableDataResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::PutTableDataRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as TablesData>::put_table_data(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = PutTableDataSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(empty_body());

View File

@@ -1,8 +1,11 @@
// src/server/services/tables_data_service.rs
use tonic::{Request, Response, Status};
use common::proto::multieko2::tables_data::tables_data_server::TablesData;
use common::proto::multieko2::tables_data::{PostTableDataRequest, PostTableDataResponse};
use crate::tables_data::handlers::post_table_data;
use common::proto::multieko2::tables_data::{
PostTableDataRequest, PostTableDataResponse,
PutTableDataRequest, PutTableDataResponse // Add this import
};
use crate::tables_data::handlers::{post_table_data, put_table_data}; // Add put_table_data
use sqlx::PgPool;
#[derive(Debug)]
@@ -18,6 +21,16 @@ impl TablesData for TablesDataService {
) -> Result<Response<PostTableDataResponse>, Status> {
let request = request.into_inner();
let response = post_table_data(&self.db_pool, request).await?;
Ok(Response::new(response)) // Wrap the response in a Response
Ok(Response::new(response))
}
// Add the new method implementation
async fn put_table_data(
&self,
request: Request<PutTableDataRequest>,
) -> Result<Response<PutTableDataResponse>, Status> {
let request = request.into_inner();
let response = put_table_data(&self.db_pool, request).await?;
Ok(Response::new(response))
}
}

View File

@@ -0,0 +1,36 @@
grpcurl -plaintext -d '{
"profile_name": "default",
"table_name": "2025_company_data1",
"id": 1,
"data": {
"firma": "ACME Corporation Updated",
"company_name": "ACME Corp Updated",
"textfield": "Updated sample text",
"textfield2": "Updated additional information",
"textfield3": "Updated more details",
"headquarters_psc": "54321",
"contact_phone": "987-654-3210",
"office_address": "456 Updated St, Springfield",
"support_email": "updated-support@acmecorp.com",
"is_active": "false"
}
}' localhost:50051 multieko2.tables_data.TablesData/PutTableData
{
"success": true,
"message": "Data updated successfully",
"updatedId": "1"
}
grpcurl -plaintext -d '{
"profile_name": "default",
"table_name": "2025_company_data1",
"id": 2,
"data": {
"firma": "1",
"is_active": "true"
}
}' localhost:50051 multieko2.tables_data.TablesData/PutTableData
{
"success": true,
"message": "Data updated successfully",
"updatedId": "2"
}

View File

@@ -1,4 +1,6 @@
// server/src/tables_data/handlers.rs
pub mod post_table_data;
pub mod put_table_data;
pub use post_table_data::post_table_data;
pub use put_table_data::put_table_data;

View File

@@ -0,0 +1,140 @@
// src/tables_data/handlers/put_table_data.rs
use tonic::Status;
use sqlx::{PgPool, Arguments, Postgres};
use sqlx::postgres::PgArguments;
use chrono::{DateTime, Utc};
use common::proto::multieko2::tables_data::{PutTableDataRequest, PutTableDataResponse};
pub async fn put_table_data(
db_pool: &PgPool,
request: PutTableDataRequest,
) -> Result<PutTableDataResponse, Status> {
let profile_name = request.profile_name;
let table_name = request.table_name;
let record_id = request.id;
let data = request.data;
// Lookup profile (same as POST)
let profile = sqlx::query!(
"SELECT id FROM profiles WHERE name = $1",
profile_name
)
.fetch_optional(db_pool)
.await
.map_err(|e| Status::internal(format!("Profile lookup error: {}", e)))?;
let profile_id = profile.ok_or_else(|| Status::not_found("Profile not found"))?.id;
// Lookup table_definition (same as POST)
let table_def = sqlx::query!(
r#"SELECT id, columns FROM table_definitions
WHERE profile_id = $1 AND table_name = $2"#,
profile_id,
table_name
)
.fetch_optional(db_pool)
.await
.map_err(|e| Status::internal(format!("Table lookup error: {}", e)))?;
let table_def = table_def.ok_or_else(|| Status::not_found("Table not found"))?;
// Parse columns from JSON (same as POST)
let columns_json: Vec<String> = serde_json::from_value(table_def.columns.clone())
.map_err(|e| Status::internal(format!("Column parsing error: {}", e)))?;
let mut columns = Vec::new();
for col_def in columns_json {
let parts: Vec<&str> = col_def.splitn(2, ' ').collect();
if parts.len() != 2 {
return Err(Status::internal("Invalid column format"));
}
let name = parts[0].trim_matches('"').to_string();
let sql_type = parts[1].to_string();
columns.push((name, sql_type));
}
// Validate system columns
let system_columns = ["firma", "deleted"];
let user_columns: Vec<&String> = columns.iter().map(|(name, _)| name).collect();
// Validate input columns
for key in data.keys() {
if !system_columns.contains(&key.as_str()) && !user_columns.contains(&key) {
return Err(Status::invalid_argument(format!("Invalid column: {}", key)));
}
}
// Prepare SQL parameters
let mut params = PgArguments::default();
let mut set_clauses = Vec::new();
let mut param_idx = 1;
// Add data parameters
for (col, value) in &data {
let sql_type = if system_columns.contains(&col.as_str()) {
match col.as_str() {
"firma" => "TEXT",
"deleted" => "BOOLEAN",
_ => return Err(Status::invalid_argument("Invalid system column")),
}
} else {
columns.iter()
.find(|(name, _)| name == col)
.map(|(_, sql_type)| sql_type.as_str())
.ok_or_else(|| Status::invalid_argument(format!("Column not found: {}", col)))?
};
match sql_type {
"TEXT" | "VARCHAR(15)" | "VARCHAR(255)" => {
if let Some(max_len) = sql_type.strip_prefix("VARCHAR(")
.and_then(|s| s.strip_suffix(')'))
.and_then(|s| s.parse::<usize>().ok())
{
if value.len() > max_len {
return Err(Status::invalid_argument(format!("Value too long for {}", col)));
}
}
params.add(value);
},
"BOOLEAN" => {
let val = value.parse::<bool>()
.map_err(|_| Status::invalid_argument(format!("Invalid boolean for {}", col)))?;
params.add(val);
},
"TIMESTAMPTZ" => {
let dt = DateTime::parse_from_rfc3339(value)
.map_err(|_| Status::invalid_argument(format!("Invalid timestamp for {}", col)))?;
params.add(dt.with_timezone(&Utc));
},
_ => return Err(Status::invalid_argument(format!("Unsupported type {}", sql_type))),
}
set_clauses.push(format!("\"{}\" = ${}", col, param_idx));
param_idx += 1;
}
// Add ID parameter at the end
params.add(record_id);
let set_clause = set_clauses.join(", ");
let sql = format!(
"UPDATE \"{}\" SET {} WHERE id = ${} AND deleted = FALSE RETURNING id",
table_name,
set_clause,
param_idx
);
let result = sqlx::query_scalar_with::<Postgres, i64, _>(&sql, params)
.fetch_optional(db_pool)
.await
.map_err(|e| Status::internal(format!("Update failed: {}", e)))?;
match result {
Some(updated_id) => Ok(PutTableDataResponse {
success: true,
message: "Data updated successfully".into(),
updated_id,
}),
None => Err(Status::not_found("Record not found or already deleted")),
}
}

View File

@@ -1,3 +1,5 @@
// tests/mod.rs
pub mod adresar;
pub mod tables_data;
pub mod common;

View File

@@ -0,0 +1,3 @@
// tests/tables_data/mod.rs
pub mod post_table_data_test;

View File

@@ -0,0 +1,241 @@
// tests/tables_data/handlers/post_table_data_test.rs
use rstest::{fixture, rstest};
use sqlx::PgPool;
use std::collections::HashMap;
use common::proto::multieko2::tables_data::{PostTableDataRequest, PostTableDataResponse};
use server::tables_data::handlers::post_table_data;
use crate::common::setup_test_db;
use tonic;
use chrono::Utc;
// Fixtures
#[fixture]
async fn pool() -> PgPool {
setup_test_db().await
}
#[fixture]
async fn closed_pool(#[future] pool: PgPool) -> PgPool {
let pool = pool.await;
pool.close().await;
pool
}
#[fixture]
fn valid_request() -> HashMap<String, String> {
let mut map = HashMap::new();
map.insert("firma".into(), "Test Company".into());
map.insert("kz".into(), "KZ123".into());
map.insert("drc".into(), "DRC456".into());
map.insert("ulica".into(), "Test Street".into());
map.insert("psc".into(), "12345".into());
map.insert("mesto".into(), "Test City".into());
map.insert("stat".into(), "Test Country".into());
map.insert("banka".into(), "Test Bank".into());
map.insert("ucet".into(), "123456789".into());
map.insert("skladm".into(), "Warehouse M".into());
map.insert("ico".into(), "12345678".into());
map.insert("kontakt".into(), "John Doe".into());
map.insert("telefon".into(), "+421123456789".into());
map.insert("skladu".into(), "Warehouse U".into());
map.insert("fax".into(), "+421123456700".into());
map
}
#[fixture]
fn minimal_request() -> HashMap<String, String> {
let mut map = HashMap::new();
map.insert("firma".into(), "Required Only".into());
map
}
fn create_table_request(data: HashMap<String, String>) -> PostTableDataRequest {
PostTableDataRequest {
profile_name: "default".into(),
table_name: "2025_adresar".into(),
data,
}
}
async fn assert_table_response(pool: &PgPool, response: &PostTableDataResponse, expected: &HashMap<String, String>) {
let row = sqlx::query!(r#"SELECT * FROM "2025_adresar" WHERE id = $1"#, response.inserted_id)
.fetch_one(pool)
.await
.unwrap();
assert_eq!(row.firma, expected["firma"]);
assert!(!row.deleted);
// Check optional fields using direct struct access
let check_field = |field: &str, value: &str| {
let db_value = match field {
"kz" => row.kz.as_deref().unwrap_or_default(),
"drc" => row.drc.as_deref().unwrap_or_default(),
"ulica" => row.ulica.as_deref().unwrap_or_default(),
"psc" => row.psc.as_deref().unwrap_or_default(),
"mesto" => row.mesto.as_deref().unwrap_or_default(),
"stat" => row.stat.as_deref().unwrap_or_default(),
"banka" => row.banka.as_deref().unwrap_or_default(),
"ucet" => row.ucet.as_deref().unwrap_or_default(),
"skladm" => row.skladm.as_deref().unwrap_or_default(),
"ico" => row.ico.as_deref().unwrap_or_default(),
"kontakt" => row.kontakt.as_deref().unwrap_or_default(),
"telefon" => row.telefon.as_deref().unwrap_or_default(),
"skladu" => row.skladu.as_deref().unwrap_or_default(),
"fax" => row.fax.as_deref().unwrap_or_default(),
_ => panic!("Unexpected field: {}", field),
};
assert_eq!(db_value, value);
};
check_field("kz", expected.get("kz").unwrap_or(&String::new()));
check_field("drc", expected.get("drc").unwrap_or(&String::new()));
check_field("ulica", expected.get("ulica").unwrap_or(&String::new()));
check_field("psc", expected.get("psc").unwrap_or(&String::new()));
check_field("mesto", expected.get("mesto").unwrap_or(&String::new()));
check_field("stat", expected.get("stat").unwrap_or(&String::new()));
check_field("banka", expected.get("banka").unwrap_or(&String::new()));
check_field("ucet", expected.get("ucet").unwrap_or(&String::new()));
check_field("skladm", expected.get("skladm").unwrap_or(&String::new()));
check_field("ico", expected.get("ico").unwrap_or(&String::new()));
check_field("kontakt", expected.get("kontakt").unwrap_or(&String::new()));
check_field("telefon", expected.get("telefon").unwrap_or(&String::new()));
check_field("skladu", expected.get("skladu").unwrap_or(&String::new()));
check_field("fax", expected.get("fax").unwrap_or(&String::new()));
// Handle timestamp conversion
let created_at: chrono::DateTime<Utc> = row.created_at.unwrap().into();
assert!(created_at <= Utc::now());
}
#[rstest]
#[tokio::test]
async fn test_create_table_data_success(
#[future] pool: PgPool,
valid_request: HashMap<String, String>,
) {
let pool = pool.await;
let request = create_table_request(valid_request.clone());
let response = post_table_data(&pool, request).await.unwrap();
assert!(response.inserted_id > 0);
assert!(response.success);
assert_eq!(response.message, "Data inserted successfully");
assert_table_response(&pool, &response, &valid_request).await;
}
// Remaining tests follow the same pattern with fixed parameter declarations
#[rstest]
#[tokio::test]
async fn test_create_table_data_whitespace_trimming(
#[future] pool: PgPool,
valid_request: HashMap<String, String>,
) {
let pool = pool.await;
let mut request = valid_request;
request.insert("firma".into(), " Test Company ".into());
request.insert("telefon".into(), " +421123456789 ".into());
let response = post_table_data(&pool, create_table_request(request)).await.unwrap();
let row = sqlx::query!(r#"SELECT firma, telefon FROM "2025_adresar" WHERE id = $1"#, response.inserted_id)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(row.firma, "Test Company");
assert_eq!(row.telefon.unwrap(), "+421123456789");
}
#[rstest]
#[tokio::test]
async fn test_create_table_data_empty_optional_fields(
#[future] pool: PgPool,
valid_request: HashMap<String, String>,
) {
let pool = pool.await;
let mut request = valid_request;
request.insert("telefon".into(), " ".into());
let response = post_table_data(&pool, create_table_request(request)).await.unwrap();
let telefon: Option<String> = sqlx::query_scalar!(r#"SELECT telefon FROM "2025_adresar" WHERE id = $1"#, response.inserted_id)
.fetch_one(&pool)
.await
.unwrap();
assert!(telefon.is_none());
}
// Fixed parameter declarations for remaining tests
#[rstest]
#[tokio::test]
async fn test_create_table_data_invalid_firma(
#[future] pool: PgPool,
valid_request: HashMap<String, String>,
) {
let pool = pool.await;
let mut request = valid_request;
request.insert("firma".into(), " ".into());
let result = post_table_data(&pool, create_table_request(request)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().code(), tonic::Code::InvalidArgument);
}
#[rstest]
#[tokio::test]
async fn test_create_table_data_minimal_request(
#[future] pool: PgPool,
minimal_request: HashMap<String, String>,
) {
let pool = pool.await;
let response = post_table_data(&pool, create_table_request(minimal_request.clone())).await.unwrap();
assert!(response.inserted_id > 0);
assert_table_response(&pool, &response, &minimal_request).await;
}
#[rstest]
#[tokio::test]
async fn test_create_table_data_telefon_length_limit(
#[future] pool: PgPool,
valid_request: HashMap<String, String>,
) {
let pool = pool.await;
let mut request = valid_request;
request.insert("telefon".into(), "1".repeat(16));
let result = post_table_data(&pool, create_table_request(request)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().code(), tonic::Code::Internal);
}
#[rstest]
#[tokio::test]
async fn test_create_table_data_special_characters(
#[future] pool: PgPool,
valid_request: HashMap<String, String>,
) {
let pool = pool.await;
let mut request = valid_request;
request.insert("ulica".into(), "Náměstí 28. října 123/456".into());
let response = post_table_data(&pool, create_table_request(request)).await.unwrap();
let row = sqlx::query!(r#"SELECT ulica FROM "2025_adresar" WHERE id = $1"#, response.inserted_id)
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(row.ulica.unwrap(), "Náměstí 28. října 123/456");
}
#[rstest]
#[tokio::test]
async fn test_create_table_data_database_error(
#[future] closed_pool: PgPool,
minimal_request: HashMap<String, String>,
) {
let closed_pool = closed_pool.await;
let result = post_table_data(&closed_pool, create_table_request(minimal_request)).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().code(), tonic::Code::Internal);
}

View File

@@ -0,0 +1,2 @@
// tests/tables_data/mod.rs
pub mod handlers;