use serde_json::Value;
pub(crate) fn props_with_event_seq(post: &Value, root: &Value, event_seq: Option<u64>) -> String {
let base = post
.get("props")
.or_else(|| root.get("props"))
.cloned()
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
match base {
Value::Object(mut map) => {
map.remove("quickReply");
map.remove("quick_reply");
if let Some(seq) = event_seq {
map.insert("channel_event_seq".to_string(), Value::from(seq));
}
Value::Object(map).to_string()
}
base => {
if base.is_null() {
String::new()
} else {
base.to_string()
}
}
}
}
#[cfg(test)]
mod tests {
use super::props_with_event_seq;
use serde_json::json;
#[test]
fn top_level_quickreply_stays_outside_props() {
let post = json!({
"props": { "channel_event_seq": 50 },
"quickReply": [{ "emoji": "👍", "userIds": ["u1", "u2"] }],
});
let out = props_with_event_seq(&post, &json!({}), None);
let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&out) else {
panic!("props 应为合法 JSON");
};
assert_eq!(parsed["channel_event_seq"], 50, "原 props 业务键保留");
assert!(parsed.get("quickReply").is_none());
}
#[test]
fn root_level_quickreply_stays_outside_props() {
let post = json!({ "props": {} });
let root = json!({ "quickReply": [{ "emoji": "🎉", "userIds": ["u9"] }] });
let out = props_with_event_seq(&post, &root, None);
let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&out) else {
panic!("props 应为合法 JSON");
};
assert!(parsed.get("quickReply").is_none());
}
#[test]
fn nested_quickreply_is_removed_from_props() {
let post = json!({
"props": {
"quickReply": [{ "emoji": "thumb", "userIds": ["member-1"] }],
"businessKey": "kept"
}
});
let parsed: serde_json::Value =
serde_json::from_str(&props_with_event_seq(&post, &json!({}), None)).unwrap();
assert!(parsed.get("quickReply").is_none());
assert_eq!(parsed["businessKey"], "kept");
}
#[test]
fn plain_post_without_quickreply_keeps_props_untouched() {
let post = json!({ "props": { "channel_event_seq": 7 } });
let out = props_with_event_seq(&post, &json!({}), None);
let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&out) else {
panic!("props 应为合法 JSON");
};
assert_eq!(parsed["channel_event_seq"], 7);
assert!(
parsed.get("quickReply").is_none(),
"无反应数据时不应注入 quickReply 键"
);
}
#[test]
fn missing_props_yields_empty_object_explicit_null_yields_empty_string() {
let missing = props_with_event_seq(&json!({}), &json!({}), None);
assert_eq!(missing, "{}", "props 缺省 + 无 quickReply → 空对象串");
let explicit_null = props_with_event_seq(&json!({ "props": null }), &json!({}), None);
assert_eq!(explicit_null, "", "显式 null props → 空串");
}
#[test]
fn sequenced_live_post_persists_event_seq_inside_props() {
let post = json!({"props": {}, "message": "live"});
let out = props_with_event_seq(&post, &json!({}), Some(27));
let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&out) else {
panic!("props 应为合法 JSON");
};
assert_eq!(parsed["channel_event_seq"], 27);
}
}