Files
windmill/backend/windmill-trigger-postgres/src/relation.rs
Ruben Fiszel 2eafe6df36 test: add comprehensive test coverage for extracted backend crates
Add 296 tests across unit and integration test suites to cover
the newly extracted crates from the recent refactor commits.

Unit tests (270):
- windmill-trigger-postgres (96): hex codec, bool parsing, type
  conversion, relation tracking, replication message parsing,
  publication data validation
- windmill-trigger-http (92): HMAC signature verification for
  GitHub/Slack/Stripe/TikTok/Twitch/Zoom webhooks, API key auth,
  Basic Auth, route validation, HTTP method/request type serde
- windmill-api-jobs (39): SQL query builder for job listing/counting
  with filters, pagination, label handling
- windmill-trigger (31): TriggerMode serde, query pagination,
  BaseTriggerData backward compat, HandlerAction, ServerState
- windmill-common webhook (7): WebhookMessage serialization tags
- worker nativets/postgresql (5): nativets job execution with
  args/objects/datetime, postgresql query execution

Integration tests (26):
- backend/tests/triggers.rs: capture config CRUD, capture payload
  operations, capture API endpoints, HTTP trigger CRUD with mode
  filtering, all trigger types DB schema validation (websocket,
  kafka, postgres, nats, sqs), schedule operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 08:58:24 +00:00

189 lines
5.9 KiB
Rust

use core::str;
use serde_json::{Map, Value};
use std::{collections::HashMap, str::Utf8Error};
use super::{
converter::{Converter, ConverterError},
replication_message::{Columns, RelationBody, TupleData},
};
use rust_postgres::types::Oid;
#[derive(Debug, thiserror::Error)]
pub enum RelationConversionError {
#[error("Could not find matching table")]
FailToFindMatchingTable,
#[error("Binary data not supported")]
BinaryFormatNotSupported,
#[error("decode error: {0}")]
FromBytes(#[from] ConverterError),
#[error("invalid string value")]
InvalidStr(#[from] Utf8Error),
}
pub struct RelationConverter(HashMap<Oid, RelationBody>);
impl RelationConverter {
pub fn new() -> Self {
Self(HashMap::new())
}
pub fn add_relation(&mut self, relation: RelationBody) {
self.0.insert(relation.o_id, relation);
}
pub fn get_columns(&self, o_id: Oid) -> Result<&Columns, RelationConversionError> {
self.0
.get(&o_id)
.map(|relation_body| &relation_body.columns)
.ok_or(RelationConversionError::FailToFindMatchingTable)
}
pub fn get_relation(&self, o_id: Oid) -> Result<&RelationBody, RelationConversionError> {
self.0
.get(&o_id)
.ok_or(RelationConversionError::FailToFindMatchingTable)
}
pub fn row_to_json(
&self,
to_decode: (Oid, Vec<TupleData>),
) -> Result<Map<String, Value>, RelationConversionError> {
let (o_id, tuple_data) = to_decode;
let mut object: Map<String, Value> = Map::new();
let columns = self.get_columns(o_id)?;
for (i, column) in columns.iter().enumerate() {
let value = match &tuple_data[i] {
TupleData::Null | TupleData::UnchangedToast => Value::Null,
TupleData::Binary(_) => {
return Err(RelationConversionError::BinaryFormatNotSupported)
}
TupleData::Text(bytes) => {
let str = str::from_utf8(&bytes[..])?;
Converter::try_from_str(column.type_o_id.clone(), str)?
}
};
object.insert(column.name.clone(), value);
}
Ok(object)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use rust_postgres::types::Type;
use serde_json::json;
use super::super::replication_message::{Column, ReplicaIdentity, RelationBody};
fn make_relation(o_id: Oid, columns: Vec<Column>) -> RelationBody {
RelationBody::new(None, o_id, "public".to_string(), "test".to_string(), ReplicaIdentity::Default, columns)
}
fn text_col(name: &str, typ: Option<Type>) -> Column {
Column::new(0, name.to_string(), typ, -1)
}
#[test]
fn test_row_to_json_text_columns() {
let mut converter = RelationConverter::new();
converter.add_relation(make_relation(1, vec![
text_col("id", Some(Type::INT4)),
text_col("name", Some(Type::TEXT)),
]));
let tuple = vec![
TupleData::Text(Bytes::from("42")),
TupleData::Text(Bytes::from("Alice")),
];
let result = converter.row_to_json((1, tuple)).unwrap();
assert_eq!(result["id"], json!(42));
assert_eq!(result["name"], json!("Alice"));
}
#[test]
fn test_row_to_json_with_null() {
let mut converter = RelationConverter::new();
converter.add_relation(make_relation(1, vec![
text_col("id", Some(Type::INT4)),
text_col("email", Some(Type::TEXT)),
]));
let tuple = vec![
TupleData::Text(Bytes::from("1")),
TupleData::Null,
];
let result = converter.row_to_json((1, tuple)).unwrap();
assert_eq!(result["id"], json!(1));
assert_eq!(result["email"], Value::Null);
}
#[test]
fn test_row_to_json_with_unchanged_toast() {
let mut converter = RelationConverter::new();
converter.add_relation(make_relation(1, vec![
text_col("data", Some(Type::TEXT)),
]));
let tuple = vec![TupleData::UnchangedToast];
let result = converter.row_to_json((1, tuple)).unwrap();
assert_eq!(result["data"], Value::Null);
}
#[test]
fn test_row_to_json_binary_rejected() {
let mut converter = RelationConverter::new();
converter.add_relation(make_relation(1, vec![
text_col("data", Some(Type::BYTEA)),
]));
let tuple = vec![TupleData::Binary(Bytes::from("data"))];
assert!(matches!(
converter.row_to_json((1, tuple)),
Err(RelationConversionError::BinaryFormatNotSupported)
));
}
#[test]
fn test_missing_relation() {
let converter = RelationConverter::new();
let tuple = vec![TupleData::Null];
assert!(matches!(
converter.row_to_json((999, tuple)),
Err(RelationConversionError::FailToFindMatchingTable)
));
}
#[test]
fn test_multiple_relations() {
let mut converter = RelationConverter::new();
converter.add_relation(make_relation(1, vec![text_col("a", Some(Type::TEXT))]));
converter.add_relation(make_relation(2, vec![text_col("b", Some(Type::INT4))]));
let result1 = converter.row_to_json((1, vec![TupleData::Text(Bytes::from("hello"))])).unwrap();
let result2 = converter.row_to_json((2, vec![TupleData::Text(Bytes::from("42"))])).unwrap();
assert_eq!(result1["a"], json!("hello"));
assert_eq!(result2["b"], json!(42));
}
#[test]
fn test_row_to_json_bool_column() {
let mut converter = RelationConverter::new();
converter.add_relation(make_relation(1, vec![
text_col("active", Some(Type::BOOL)),
]));
let result = converter.row_to_json((1, vec![TupleData::Text(Bytes::from("t"))])).unwrap();
assert_eq!(result["active"], json!(true));
}
}