1use crate::{CardBlock, MessageCard, MessageEnvelope, OutKind, OutMessage};
2use anyhow::{Result, bail};
3use time::OffsetDateTime;
4
5pub fn validate_envelope(env: &MessageEnvelope) -> Result<()> {
26 if env.tenant.trim().is_empty() {
27 bail!("tenant empty");
28 }
29 if env.chat_id.trim().is_empty() {
30 bail!("chat_id empty");
31 }
32 if env.user_id.trim().is_empty() {
33 bail!("user_id empty");
34 }
35 if env.msg_id.trim().is_empty() {
36 bail!("msg_id empty");
37 }
38 OffsetDateTime::parse(
40 &env.timestamp,
41 &time::format_description::well_known::Rfc3339,
42 )
43 .map_err(|e| anyhow::anyhow!("invalid timestamp: {e}"))?;
44 Ok(())
45}
46
47pub fn validate_out(out: &OutMessage) -> Result<()> {
69 if out.tenant.trim().is_empty() {
70 bail!("tenant empty");
71 }
72 if out.chat_id.trim().is_empty() {
73 bail!("chat_id empty");
74 }
75 match out.kind {
76 OutKind::Text => {
77 if out.text.as_deref().unwrap_or("").trim().is_empty() {
78 bail!("text empty");
79 }
80 }
81 OutKind::Card => {
82 let card = out
83 .message_card
84 .as_ref()
85 .ok_or_else(|| anyhow::anyhow!("card missing"))?;
86 validate_card(card)?;
87 }
88 }
89 Ok(())
90}
91
92pub fn validate_card(card: &MessageCard) -> Result<()> {
117 if card.body.is_empty() && card.title.as_deref().unwrap_or("").is_empty() {
118 bail!("card must have title or body");
119 }
120 for block in &card.body {
121 match block {
122 CardBlock::Text { text, .. } if text.trim().is_empty() => bail!("empty text block"),
123 CardBlock::Fact { label, value }
124 if label.trim().is_empty() || value.trim().is_empty() =>
125 {
126 bail!("empty fact")
127 }
128 CardBlock::Image { url } if url.trim().is_empty() => bail!("empty image url"),
129 _ => {}
130 }
131 }
132 Ok(())
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::{CardAction, Platform, make_tenant_ctx};
139 use serde_json::json;
140 use std::collections::BTreeMap;
141
142 fn sample_envelope() -> MessageEnvelope {
143 MessageEnvelope {
144 tenant: "acme".into(),
145 platform: Platform::Teams,
146 chat_id: "chat-1".into(),
147 user_id: "user-7".into(),
148 thread_id: None,
149 msg_id: "msg-1".into(),
150 text: Some("Hello".into()),
151 timestamp: "2024-01-01T00:00:00Z".into(),
152 context: BTreeMap::new(),
153 }
154 }
155
156 fn sample_out(kind: OutKind) -> OutMessage {
157 OutMessage {
158 ctx: make_tenant_ctx("acme".into(), None, None),
159 tenant: "acme".into(),
160 platform: Platform::Telegram,
161 chat_id: "chat-1".into(),
162 thread_id: None,
163 kind,
164 text: Some("Hello".into()),
165 message_card: None,
166 #[cfg(feature = "adaptive-cards")]
167 adaptive_card: None,
168 meta: Default::default(),
169 }
170 }
171
172 #[test]
173 fn envelope_rejects_empty_tenant() {
174 let mut env = sample_envelope();
175 env.tenant = " ".into();
176 assert!(validate_envelope(&env).is_err());
177 }
178
179 #[test]
180 fn envelope_rejects_bad_timestamp() {
181 let mut env = sample_envelope();
182 env.timestamp = "not-a-date".into();
183 assert!(validate_envelope(&env).is_err());
184 }
185
186 #[test]
187 fn out_text_requires_content() {
188 let mut out = sample_out(OutKind::Text);
189 out.text = Some(" ".into());
190 assert!(validate_out(&out).is_err());
191 }
192
193 #[test]
194 fn out_card_requires_message_card() {
195 let out = sample_out(OutKind::Card);
196 assert!(validate_out(&out).is_err());
197 }
198
199 #[test]
200 fn card_requires_body_or_title() {
201 let card = MessageCard {
202 title: None,
203 body: vec![],
204 actions: vec![],
205 };
206 assert!(validate_card(&card).is_err());
207 }
208
209 #[test]
210 fn card_rejects_empty_fact_fields() {
211 let card = MessageCard {
212 title: Some("Facts".into()),
213 body: vec![CardBlock::Fact {
214 label: " ".into(),
215 value: "".into(),
216 }],
217 actions: vec![],
218 };
219 assert!(validate_card(&card).is_err());
220 }
221
222 #[test]
223 fn card_accepts_valid_structure() {
224 let card = MessageCard {
225 title: Some("Weather".into()),
226 body: vec![
227 CardBlock::Text {
228 text: "Sunny".into(),
229 markdown: false,
230 },
231 CardBlock::Fact {
232 label: "High".into(),
233 value: "22C".into(),
234 },
235 ],
236 actions: vec![CardAction::Postback {
237 title: "Ack".into(),
238 data: json!({"ok": true}),
239 }],
240 };
241 assert!(validate_card(&card).is_ok());
242 }
243}