generalbots/bottest/src/mocks/teams.rs
Rodrigo Rodriguez (Pragmatismo) 037db5c381 feat: Major workspace reorganization and documentation update
- Add comprehensive documentation in botbook/ with 12 chapters
- Add botapp/ Tauri desktop application
- Add botdevice/ IoT device support
- Add botlib/ shared library crate
- Add botmodels/ Python ML models service
- Add botplugin/ browser extension
- Add botserver/ reorganized server code
- Add bottemplates/ bot templates
- Add bottest/ integration tests
- Add botui/ web UI server
- Add CI/CD workflows in .forgejo/workflows/
- Add AGENTS.md and PROD.md documentation
- Add dependency management scripts (DEPENDENCIES.sh/ps1)
- Remove legacy src/ structure and migrations
- Clean up temporary and backup files
2026-04-19 08:14:25 -03:00

940 lines
30 KiB
Rust

use super::{new_expectation_store, ExpectationStore};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;
use wiremock::matchers::{method, path, path_regex};
use wiremock::{Mock, MockServer, ResponseTemplate};
pub struct MockTeams {
server: MockServer,
port: u16,
expectations: ExpectationStore,
sent_activities: Arc<Mutex<Vec<Activity>>>,
conversations: Arc<Mutex<HashMap<String, ConversationInfo>>>,
bot_id: String,
bot_name: String,
tenant_id: String,
service_url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Activity {
#[serde(rename = "type")]
pub kind: String,
pub id: String,
pub timestamp: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub local_timestamp: Option<String>,
pub service_url: String,
pub channel_id: String,
pub from: ChannelAccount,
pub conversation: ConversationAccount,
pub recipient: ChannelAccount,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<Attachment>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entities: Option<Vec<Entity>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_data: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reply_to_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
impl Default for Activity {
fn default() -> Self {
Self {
kind: "message".to_string(),
id: Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
local_timestamp: None,
service_url: String::new(),
channel_id: "msteams".to_string(),
from: ChannelAccount::default(),
conversation: ConversationAccount::default(),
recipient: ChannelAccount::default(),
text: None,
text_format: Some("plain".to_string()),
locale: Some("en-US".to_string()),
attachments: None,
entities: None,
channel_data: None,
action: None,
reply_to_id: None,
value: None,
name: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ChannelAccount {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aad_object_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ConversationAccount {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conversation_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_group: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Attachment {
pub content_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Entity {
#[serde(rename = "type")]
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mentioned: Option<ChannelAccount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(flatten)]
pub additional: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct ConversationInfo {
pub id: String,
pub tenant_id: String,
pub service_url: String,
pub members: Vec<ChannelAccount>,
pub is_group: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResourceResponse {
pub id: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConversationsResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub continuation_token: Option<String>,
pub conversations: Vec<ConversationMembers>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConversationMembers {
pub id: String,
pub members: Vec<ChannelAccount>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamsChannelAccount {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aad_object_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_principal_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tenant_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub given_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub surname: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamsMeetingInfo {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub join_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdaptiveCardInvokeResponse {
pub status_code: u16,
#[serde(rename = "type")]
pub response_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error: ErrorBody,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorBody {
pub code: String,
pub message: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InvokeResponse {
pub status: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
}
impl MockTeams {
pub const DEFAULT_BOT_ID: &'static str = "28:test-bot-id";
pub const DEFAULT_BOT_NAME: &'static str = "TestBot";
pub const DEFAULT_TENANT_ID: &'static str = "test-tenant-id";
pub async fn start(port: u16) -> Result<Self> {
let listener = std::net::TcpListener::bind(format!("127.0.0.1:{port}"))
.context("Failed to bind MockTeams port")?;
let server = MockServer::builder().listener(listener).start().await;
let service_url = format!("http://127.0.0.1:{port}");
let mock = Self {
server,
port,
expectations: new_expectation_store(),
sent_activities: Arc::new(Mutex::new(Vec::new())),
conversations: Arc::new(Mutex::new(HashMap::new())),
bot_id: Self::DEFAULT_BOT_ID.to_string(),
bot_name: Self::DEFAULT_BOT_NAME.to_string(),
tenant_id: Self::DEFAULT_TENANT_ID.to_string(),
service_url,
};
mock.setup_default_routes().await;
Ok(mock)
}
pub async fn start_with_config(
port: u16,
bot_id: &str,
bot_name: &str,
tenant_id: &str,
) -> Result<Self> {
let listener = std::net::TcpListener::bind(format!("127.0.0.1:{port}"))
.context("Failed to bind MockTeams port")?;
let server = MockServer::builder().listener(listener).start().await;
let service_url = format!("http://127.0.0.1:{port}");
let mock = Self {
server,
port,
expectations: new_expectation_store(),
sent_activities: Arc::new(Mutex::new(Vec::new())),
conversations: Arc::new(Mutex::new(HashMap::new())),
bot_id: bot_id.to_string(),
bot_name: bot_name.to_string(),
tenant_id: tenant_id.to_string(),
service_url,
};
mock.setup_default_routes().await;
Ok(mock)
}
async fn setup_default_routes(&self) {
let sent_activities = self.sent_activities.clone();
Mock::given(method("POST"))
.and(path_regex(r"/v3/conversations/.+/activities"))
.respond_with(move |req: &wiremock::Request| {
let body: serde_json::Value = req.body_json().unwrap_or_default();
let activity = Activity {
kind: body
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("message")
.to_string(),
id: Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
local_timestamp: None,
service_url: String::new(),
channel_id: "msteams".to_string(),
from: ChannelAccount::default(),
conversation: ConversationAccount::default(),
recipient: ChannelAccount::default(),
text: body.get("text").and_then(|v| v.as_str()).map(String::from),
text_format: None,
locale: None,
attachments: None,
entities: None,
channel_data: None,
action: None,
reply_to_id: None,
value: None,
name: None,
};
sent_activities.lock().unwrap().push(activity.clone());
let response = ResourceResponse { id: activity.id };
ResponseTemplate::new(200).set_body_json(&response)
})
.mount(&self.server)
.await;
Mock::given(method("POST"))
.and(path_regex(r"/v3/conversations/.+/activities/.+"))
.respond_with(|_req: &wiremock::Request| {
let response = ResourceResponse {
id: Uuid::new_v4().to_string(),
};
ResponseTemplate::new(200).set_body_json(&response)
})
.mount(&self.server)
.await;
Mock::given(method("PUT"))
.and(path_regex(r"/v3/conversations/.+/activities/.+"))
.respond_with(|_req: &wiremock::Request| {
let response = ResourceResponse {
id: Uuid::new_v4().to_string(),
};
ResponseTemplate::new(200).set_body_json(&response)
})
.mount(&self.server)
.await;
Mock::given(method("DELETE"))
.and(path_regex(r"/v3/conversations/.+/activities/.+"))
.respond_with(ResponseTemplate::new(200))
.mount(&self.server)
.await;
Mock::given(method("GET"))
.and(path_regex(r"/v3/conversations/.+/members"))
.respond_with(|_req: &wiremock::Request| {
let members = vec![TeamsChannelAccount {
id: "user-1".to_string(),
name: Some("Test User".to_string()),
aad_object_id: Some(Uuid::new_v4().to_string()),
email: Some("testuser@example.com".to_string()),
user_principal_name: Some("testuser@example.com".to_string()),
tenant_id: Some("test-tenant".to_string()),
given_name: Some("Test".to_string()),
surname: Some("User".to_string()),
}];
ResponseTemplate::new(200).set_body_json(&members)
})
.mount(&self.server)
.await;
Mock::given(method("GET"))
.and(path_regex(r"/v3/conversations/.+/members/.+"))
.respond_with(|_req: &wiremock::Request| {
let member = TeamsChannelAccount {
id: "user-1".to_string(),
name: Some("Test User".to_string()),
aad_object_id: Some(Uuid::new_v4().to_string()),
email: Some("testuser@example.com".to_string()),
user_principal_name: Some("testuser@example.com".to_string()),
tenant_id: Some("test-tenant".to_string()),
given_name: Some("Test".to_string()),
surname: Some("User".to_string()),
};
ResponseTemplate::new(200).set_body_json(&member)
})
.mount(&self.server)
.await;
Mock::given(method("POST"))
.and(path("/v3/conversations"))
.respond_with(|_req: &wiremock::Request| {
let conversation = ConversationAccount {
id: format!("conv-{}", Uuid::new_v4()),
name: None,
conversation_type: Some("personal".to_string()),
is_group: Some(false),
tenant_id: Some("test-tenant".to_string()),
};
ResponseTemplate::new(200).set_body_json(&conversation)
})
.mount(&self.server)
.await;
Mock::given(method("GET"))
.and(path("/v3/conversations"))
.respond_with(|_req: &wiremock::Request| {
let result = ConversationsResult {
continuation_token: None,
conversations: vec![],
};
ResponseTemplate::new(200).set_body_json(&result)
})
.mount(&self.server)
.await;
Mock::given(method("POST"))
.and(path("/botframework.com/oauth2/v2.0/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"token_type": "Bearer",
"expires_in": 3600,
"access_token": format!("test_token_{}", Uuid::new_v4())
})))
.mount(&self.server)
.await;
}
#[must_use]
pub fn simulate_message(&self, from_id: &str, from_name: &str, text: &str) -> Activity {
let conversation_id = format!("conv-{}", Uuid::new_v4());
Activity {
kind: "message".to_string(),
id: Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
local_timestamp: Some(chrono::Utc::now().to_rfc3339()),
service_url: self.service_url.clone(),
channel_id: "msteams".to_string(),
from: ChannelAccount {
id: from_id.to_string(),
name: Some(from_name.to_string()),
aad_object_id: Some(Uuid::new_v4().to_string()),
role: Some("user".to_string()),
},
conversation: ConversationAccount {
id: conversation_id,
name: None,
conversation_type: Some("personal".to_string()),
is_group: Some(false),
tenant_id: Some(self.tenant_id.clone()),
},
recipient: ChannelAccount {
id: self.bot_id.clone(),
name: Some(self.bot_name.clone()),
aad_object_id: None,
role: Some("bot".to_string()),
},
text: Some(text.to_string()),
text_format: Some("plain".to_string()),
locale: Some("en-US".to_string()),
attachments: None,
entities: None,
channel_data: Some(serde_json::json!({
"tenant": {
"id": self.tenant_id
}
})),
action: None,
reply_to_id: None,
value: None,
name: None,
}
}
#[must_use]
pub fn simulate_mention(&self, from_id: &str, from_name: &str, text: &str) -> Activity {
let mut activity = self.simulate_message(from_id, from_name, text);
let mention_text = format!("<at>{}</at>", self.bot_name);
activity.text = Some(format!("{mention_text} {text}"));
activity.entities = Some(vec![Entity {
kind: "mention".to_string(),
mentioned: Some(ChannelAccount {
id: self.bot_id.clone(),
name: Some(self.bot_name.clone()),
aad_object_id: None,
role: None,
}),
text: Some(mention_text),
additional: HashMap::new(),
}]);
activity
}
#[must_use]
pub fn simulate_member_added(&self, member_id: &str, member_name: &str) -> Activity {
let conversation_id = format!("conv-{}", Uuid::new_v4());
Activity {
kind: "conversationUpdate".to_string(),
id: Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
local_timestamp: None,
service_url: self.service_url.clone(),
channel_id: "msteams".to_string(),
from: ChannelAccount {
id: member_id.to_string(),
name: Some(member_name.to_string()),
aad_object_id: None,
role: None,
},
conversation: ConversationAccount {
id: conversation_id,
name: None,
conversation_type: Some("personal".to_string()),
is_group: Some(false),
tenant_id: Some(self.tenant_id.clone()),
},
recipient: ChannelAccount {
id: self.bot_id.clone(),
name: Some(self.bot_name.clone()),
aad_object_id: None,
role: Some("bot".to_string()),
},
text: None,
text_format: None,
locale: None,
attachments: None,
entities: None,
channel_data: Some(serde_json::json!({
"tenant": {
"id": self.tenant_id
},
"eventType": "teamMemberAdded"
})),
action: Some("add".to_string()),
reply_to_id: None,
value: None,
name: None,
}
}
#[must_use]
pub fn simulate_invoke(
&self,
from_id: &str,
from_name: &str,
name: &str,
value: serde_json::Value,
) -> Activity {
let conversation_id = format!("conv-{}", Uuid::new_v4());
Activity {
kind: "invoke".to_string(),
id: Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
local_timestamp: None,
service_url: self.service_url.clone(),
channel_id: "msteams".to_string(),
from: ChannelAccount {
id: from_id.to_string(),
name: Some(from_name.to_string()),
aad_object_id: Some(Uuid::new_v4().to_string()),
role: Some("user".to_string()),
},
conversation: ConversationAccount {
id: conversation_id,
name: None,
conversation_type: Some("personal".to_string()),
is_group: Some(false),
tenant_id: Some(self.tenant_id.clone()),
},
recipient: ChannelAccount {
id: self.bot_id.clone(),
name: Some(self.bot_name.clone()),
aad_object_id: None,
role: Some("bot".to_string()),
},
text: None,
text_format: None,
locale: Some("en-US".to_string()),
attachments: None,
entities: None,
channel_data: Some(serde_json::json!({
"tenant": {
"id": self.tenant_id
}
})),
action: None,
reply_to_id: None,
value: Some(value),
name: Some(name.to_string()),
}
}
#[must_use]
pub fn simulate_adaptive_card_action(
&self,
from_id: &str,
from_name: &str,
action_data: serde_json::Value,
) -> Activity {
self.simulate_invoke(
from_id,
from_name,
"adaptiveCard/action",
serde_json::json!({
"action": {
"type": "Action.Execute",
"verb": "submitAction",
"data": action_data
}
}),
)
}
#[must_use]
pub fn simulate_reaction(
&self,
from_id: &str,
from_name: &str,
message_id: &str,
reaction: &str,
) -> Activity {
let conversation_id = format!("conv-{}", Uuid::new_v4());
Activity {
kind: "messageReaction".to_string(),
id: Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
local_timestamp: None,
service_url: self.service_url.clone(),
channel_id: "msteams".to_string(),
from: ChannelAccount {
id: from_id.to_string(),
name: Some(from_name.to_string()),
aad_object_id: None,
role: None,
},
conversation: ConversationAccount {
id: conversation_id,
name: None,
conversation_type: Some("personal".to_string()),
is_group: Some(false),
tenant_id: Some(self.tenant_id.clone()),
},
recipient: ChannelAccount {
id: self.bot_id.clone(),
name: Some(self.bot_name.clone()),
aad_object_id: None,
role: Some("bot".to_string()),
},
text: None,
text_format: None,
locale: None,
attachments: None,
entities: None,
channel_data: None,
action: None,
reply_to_id: Some(message_id.to_string()),
value: Some(serde_json::json!({
"reactionsAdded": [{
"type": reaction
}]
})),
name: None,
}
}
pub async fn expect_error(&self, code: &str, message: &str) {
let error_response = ErrorResponse {
error: ErrorBody {
code: code.to_string(),
message: message.to_string(),
},
};
Mock::given(method("POST"))
.and(path_regex(r"/v3/conversations/.+/activities"))
.respond_with(ResponseTemplate::new(400).set_body_json(&error_response))
.mount(&self.server)
.await;
}
pub async fn expect_unauthorized(&self) {
self.expect_error("Unauthorized", "Token validation failed")
.await;
}
pub async fn expect_not_found(&self) {
self.expect_error("NotFound", "Conversation not found")
.await;
}
#[must_use]
pub fn sent_activities(&self) -> Vec<Activity> {
self.sent_activities.lock().unwrap().clone()
}
#[must_use]
pub fn sent_activities_containing(&self, text: &str) -> Vec<Activity> {
self.sent_activities
.lock()
.unwrap()
.iter()
.filter(|a| a.text.as_ref().is_some_and(|t| t.contains(text)))
.cloned()
.collect()
}
#[must_use]
pub fn last_sent_activity(&self) -> Option<Activity> {
self.sent_activities.lock().unwrap().last().cloned()
}
pub fn clear_sent_activities(&self) {
self.sent_activities.lock().unwrap().clear();
}
pub fn register_conversation(&self, info: ConversationInfo) {
self.conversations
.lock()
.unwrap()
.insert(info.id.clone(), info);
}
#[must_use]
pub fn url(&self) -> String {
format!("http://127.0.0.1:{}", self.port)
}
#[must_use]
pub fn service_url(&self) -> String {
self.service_url.clone()
}
#[must_use]
pub const fn port(&self) -> u16 {
self.port
}
#[must_use]
pub fn bot_id(&self) -> &str {
&self.bot_id
}
#[must_use]
pub fn bot_name(&self) -> &str {
&self.bot_name
}
#[must_use]
pub fn tenant_id(&self) -> &str {
&self.tenant_id
}
pub fn verify(&self) -> Result<()> {
let store = self.expectations.lock().unwrap();
for (_, exp) in store.iter() {
exp.verify()?;
}
Ok(())
}
pub async fn reset(&self) {
self.server.reset().await;
self.sent_activities.lock().unwrap().clear();
self.conversations.lock().unwrap().clear();
self.expectations.lock().unwrap().clear();
self.setup_default_routes().await;
}
pub async fn received_requests(&self) -> Vec<wiremock::Request> {
self.server.received_requests().await.unwrap_or_default()
}
}
pub fn adaptive_card(content: serde_json::Value) -> Attachment {
Attachment {
content_type: "application/vnd.microsoft.card.adaptive".to_string(),
content_url: None,
content: Some(content),
name: None,
thumbnail_url: None,
}
}
pub fn hero_card(title: &str, subtitle: Option<&str>, text: Option<&str>) -> Attachment {
Attachment {
content_type: "application/vnd.microsoft.card.hero".to_string(),
content_url: None,
content: Some(serde_json::json!({
"title": title,
"subtitle": subtitle,
"text": text
})),
name: None,
thumbnail_url: None,
}
}
pub fn thumbnail_card(
title: &str,
subtitle: Option<&str>,
text: Option<&str>,
image_url: Option<&str>,
) -> Attachment {
Attachment {
content_type: "application/vnd.microsoft.card.thumbnail".to_string(),
content_url: None,
content: Some(serde_json::json!({
"title": title,
"subtitle": subtitle,
"text": text,
"images": image_url.map(|url| vec![serde_json::json!({"url": url})])
})),
name: None,
thumbnail_url: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_activity_default() {
let activity = Activity::default();
assert_eq!(activity.kind, "message");
assert_eq!(activity.channel_id, "msteams");
assert!(!activity.id.is_empty());
}
#[test]
fn test_activity_serialization() {
let activity = Activity {
kind: "message".to_string(),
id: "test-id".to_string(),
timestamp: "2024-01-01T00:00:00Z".to_string(),
local_timestamp: None,
service_url: "http://localhost".to_string(),
channel_id: "msteams".to_string(),
from: ChannelAccount {
id: "user-1".to_string(),
name: Some("Test User".to_string()),
aad_object_id: None,
role: None,
},
conversation: ConversationAccount {
id: "conv-1".to_string(),
name: None,
conversation_type: Some("personal".to_string()),
is_group: Some(false),
tenant_id: Some("tenant-1".to_string()),
},
recipient: ChannelAccount::default(),
text: Some("Hello!".to_string()),
text_format: None,
locale: None,
attachments: None,
entities: None,
channel_data: None,
action: None,
reply_to_id: None,
value: None,
name: None,
};
let json = serde_json::to_string(&activity).unwrap();
assert!(json.contains("Hello!"));
assert!(json.contains("msteams"));
assert!(json.contains("Test User"));
}
#[test]
fn test_resource_response() {
let response = ResourceResponse {
id: "msg-123".to_string(),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("msg-123"));
}
#[test]
fn test_adaptive_card_helper() {
let card = adaptive_card(serde_json::json!({
"type": "AdaptiveCard",
"body": [{"type": "TextBlock", "text": "Hello"}]
}));
assert_eq!(card.content_type, "application/vnd.microsoft.card.adaptive");
assert!(card.content.is_some());
}
#[test]
fn test_hero_card_helper() {
let card = hero_card("Title", Some("Subtitle"), Some("Text"));
assert_eq!(card.content_type, "application/vnd.microsoft.card.hero");
let content = card.content.unwrap();
assert_eq!(content["title"], "Title");
}
#[test]
fn test_entity_mention() {
let entity = Entity {
kind: "mention".to_string(),
mentioned: Some(ChannelAccount {
id: "bot-id".to_string(),
name: Some("Bot".to_string()),
aad_object_id: None,
role: None,
}),
text: Some("<at>Bot</at>".to_string()),
additional: HashMap::new(),
};
let json = serde_json::to_string(&entity).unwrap();
assert!(json.contains("mention"));
assert!(json.contains("<at>Bot</at>"));
}
#[test]
fn test_error_response() {
let error = ErrorResponse {
error: ErrorBody {
code: "BadRequest".to_string(),
message: "Invalid activity".to_string(),
},
};
let json = serde_json::to_string(&error).unwrap();
assert!(json.contains("BadRequest"));
assert!(json.contains("Invalid activity"));
}
}