botrs 0.13.0

A Rust QQ Bot framework based on QQ Guild Bot API
Documentation
use super::*;

#[test]
fn test_rate_limit() {
    let rate_limit = RateLimit {
        bucket: Some("global".to_string()),
        limit: 100,
        remaining: 0,
        reset: chrono::Utc::now().timestamp() as u64 + 60,
        retry_after: Some(60),
    };

    assert_eq!(rate_limit.remaining, 0);
    assert_eq!(rate_limit.retry_after, Some(60));
    assert!(rate_limit.reset > chrono::Utc::now().timestamp() as u64);
}

#[test]
fn test_api_error() {
    let error: ApiError = serde_json::from_value(serde_json::json!({
        "code": 429,
        "message": "Rate limited"
    }))
    .unwrap();

    assert_eq!(error.err_code, None);
    assert_eq!(error.code, 429);
    assert_eq!(error.message, "Rate limited");

    let auth_error: ApiError = serde_json::from_value(serde_json::json!({
        "code": 401,
        "message": "Unauthorized"
    }))
    .unwrap();
    assert_eq!(auth_error.code, 401);
}

#[test]
fn api_error_accepts_current_err_code_field() {
    let error: ApiError = serde_json::from_value(serde_json::json!({
        "err_code": 11244,
        "message": "token expired",
        "trace_id": "trace-err-code"
    }))
    .unwrap();

    assert_eq!(error.code, 11244);
    assert_eq!(error.err_code, Some(11244));
    assert_eq!(error.message, "token expired");
    assert_eq!(error.trace_id.as_deref(), Some("trace-err-code"));
}

#[test]
fn websocket_ap_keeps_official_json_shape() {
    let ap: GatewayResponse = serde_json::from_value(serde_json::json!({
        "url": "wss://api.sgroup.qq.com/websocket",
        "shards": 2,
        "session_start_limit": {
            "total": 10,
            "remaining": 9,
            "reset_after": 1000,
            "max_concurrency": 1
        }
    }))
    .unwrap();

    assert_eq!(ap.url, "wss://api.sgroup.qq.com/websocket");
    assert_eq!(ap.shards, 2);
    assert_eq!(ap.session_start_limit.total, 10);
    assert_eq!(ap.session_start_limit.remaining, 9);
    assert_eq!(ap.session_start_limit.reset_after, 1000);
    assert_eq!(ap.session_start_limit.max_concurrency, 1);

    let value = serde_json::to_value(&ap).unwrap();
    assert_eq!(value["session_start_limit"]["reset_after"], 1000);
}

#[test]
fn bot_info_keeps_current_user_extra_fields() {
    let bot: BotInfo = serde_json::from_value(serde_json::json!({
        "id": "bot-1",
        "username": "bot",
        "avatar": "https://example.com/avatar.png",
        "union_openid": "UNION_OPENID_XXXXXX",
        "union_user_account": "UNION_ACCOUNT_XXXXXX",
        "share_url": "https://example.com/share",
        "welcome_msg": "hello"
    }))
    .unwrap();

    assert_eq!(bot.id, "bot-1");
    assert_eq!(bot.username, "bot");
    assert_eq!(bot.avatar, "https://example.com/avatar.png");
    assert!(!bot.bot);
    assert_eq!(bot.union_openid, "UNION_OPENID_XXXXXX");
    assert_eq!(bot.union_user_account, "UNION_ACCOUNT_XXXXXX");
    assert_eq!(bot.share_url, "https://example.com/share");
    assert_eq!(bot.welcome_msg, "hello");
}

#[test]
fn audio_action_uses_required_zero_value_fields() {
    let action: AudioAction = serde_json::from_value(serde_json::json!({})).unwrap();

    assert_eq!(action.guild_id, "");
    assert_eq!(action.channel_id, "");
    assert_eq!(action.audio_url, "");
    assert_eq!(action.text, "");
}

#[test]
fn audio_action_keeps_official_json_shape() {
    let action = AudioAction {
        guild_id: "guild-1".to_string(),
        channel_id: "channel-1".to_string(),
        audio_url: "https://example.com/audio.mp3".to_string(),
        text: "now playing".to_string(),
    };
    let value = serde_json::to_value(&action).unwrap();

    assert_eq!(value["guild_id"], "guild-1");
    assert_eq!(value["channel_id"], "channel-1");
    assert_eq!(value["audio_url"], "https://example.com/audio.mp3");
    assert_eq!(value["text"], "now playing");
}

#[test]
fn audio_action_from_value_tolerates_missing_fields() {
    let action = AudioAction::from_value(&serde_json::json!({
        "guild_id": "guild-1",
        "channel_id": 123,
    }));

    assert_eq!(action.guild_id, "guild-1");
    assert_eq!(action.channel_id, "");
    assert_eq!(action.audio_url, "");
    assert_eq!(action.text, "");
}

#[test]
fn pins_message_uses_required_zero_value_fields() {
    let pins: PinsMessage = serde_json::from_value(serde_json::json!({})).unwrap();

    assert!(pins.guild_id.is_empty());
    assert!(pins.channel_id.is_empty());
    assert!(pins.message_ids.is_empty());
}

#[test]
fn pins_message_keeps_official_json_shape() {
    let pins = PinsMessage {
        guild_id: "guild-1".to_string(),
        channel_id: "channel-1".to_string(),
        message_ids: vec!["message-1".to_string(), "message-2".to_string()],
    };
    let value = serde_json::to_value(&pins).unwrap();

    assert_eq!(value["guild_id"], "guild-1");
    assert_eq!(value["channel_id"], "channel-1");
    assert_eq!(value["message_ids"][0], "message-1");
}

#[test]
fn message_response_collects_extra_fields() {
    let response: MessageResponse = serde_json::from_value(serde_json::json!({
        "id": "message-1",
        "timestamp": "2026-01-01T00:00:00+08:00",
        "msg_seq": 1,
        "audit_id": "audit-1"
    }))
    .unwrap();

    assert_eq!(response.id.as_deref(), Some("message-1"));
    assert_eq!(
        response.timestamp.as_deref(),
        Some("2026-01-01T00:00:00+08:00")
    );
    assert_eq!(response.extra["msg_seq"], serde_json::json!(1));
    assert_eq!(response.extra["audit_id"], serde_json::json!("audit-1"));

    let value = serde_json::to_value(&response).unwrap();
    assert_eq!(value["id"], "message-1");
    assert_eq!(value["timestamp"], "2026-01-01T00:00:00+08:00");
    assert_eq!(value["msg_seq"], 1);
    assert_eq!(value["audit_id"], "audit-1");
}