refactor(tests): reformat code and update dependency management

- Reorganized import statements in adapter tests for better readability
- Replaced or_insert_with(Vec::new) with or_default() in test closures
- Updated Cargo.lock with new dependency versions and checksums
- Added TLS features to tonic dependency configuration
- Included sqlx, chrono, and uuid dependencies with specific features
- Added jsonwebtoken and arc-swap as project dependencies
- Reformatted assertion statements to comply with line length limits
- Adjusted base64 import order in engine codec module
- Updated protobuf include statement formatting
This commit is contained in:
zhenyi
2026-06-11 12:11:05 +08:00
parent 06e8ee96a5
commit 821537186e
111 changed files with 10458 additions and 385 deletions
+170
View File
@@ -0,0 +1,170 @@
//! Poll on a message — new tables `message_poll`, `message_poll_option`,
//! `message_poll_vote`.
//!
//! Discord-style polls: attached to a message, with multiple options,
//! optional multi-vote, and an expiry time.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// The poll itself (one per message, optional).
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct MessagePoll {
pub id: Uuid,
pub message_id: Uuid,
/// The question displayed to voters.
pub question: String,
/// Whether users can select multiple options.
pub allow_multiselect: bool,
/// Maximum number of options a user can select (NULL = unlimited when multiselect).
pub max_selections: Option<i32>,
/// When voting closes (NULL = no expiry).
pub expires_at: Option<DateTime<Utc>>,
/// Total number of votes cast (denormalized for fast reads).
pub total_votes: i64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
/// A single selectable option within a poll.
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct MessagePollOption {
pub id: Uuid,
pub poll_id: Uuid,
/// Display text for this option.
pub text: String,
/// Optional emoji prefix (Discord-style).
pub emoji: Option<String>,
/// Number of votes this option received (denormalized).
pub vote_count: i64,
/// Display order.
pub position: i32,
}
/// A single vote cast by a user.
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct MessagePollVote {
pub id: Uuid,
pub poll_id: Uuid,
pub option_id: Uuid,
pub user_id: Uuid,
pub created_at: DateTime<Utc>,
}
/// Aggregated poll results for API responses.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PollResult {
pub poll: MessagePoll,
pub options: Vec<PollOptionResult>,
/// Which options the current user voted for (empty if not voted).
pub my_votes: Vec<Uuid>,
/// Whether the poll has expired.
pub is_expired: bool,
}
/// Option with its vote count and percentage.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PollOptionResult {
#[serde(flatten)]
pub option: MessagePollOption,
/// Percentage of total votes (0.0100.0), rounded to 1 decimal.
pub percentage: f64,
}
impl PollResult {
/// Compute percentages from total_votes.
pub fn from_poll(
poll: MessagePoll,
options: Vec<MessagePollOption>,
my_votes: Vec<Uuid>,
) -> Self {
let total = poll.total_votes.max(1) as f64;
let now = Utc::now();
let is_expired = poll.expires_at.is_some_and(|exp| now >= exp);
let options = options
.into_iter()
.map(|opt| {
let pct = if poll.total_votes > 0 {
(opt.vote_count as f64 / total * 100.0 * 10.0).round() / 10.0
} else {
0.0
};
PollOptionResult {
option: opt,
percentage: pct,
}
})
.collect();
Self {
poll,
options,
my_votes,
is_expired,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_poll_result_percentages() {
let poll = MessagePoll {
id: Uuid::now_v7(),
message_id: Uuid::now_v7(),
question: "Best language?".to_string(),
allow_multiselect: false,
max_selections: None,
expires_at: None,
total_votes: 10,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let options = vec![
MessagePollOption {
id: Uuid::now_v7(),
poll_id: poll.id,
text: "Rust".to_string(),
emoji: None,
vote_count: 7,
position: 0,
},
MessagePollOption {
id: Uuid::now_v7(),
poll_id: poll.id,
text: "Go".to_string(),
emoji: None,
vote_count: 3,
position: 1,
},
];
let result = PollResult::from_poll(poll, options, vec![]);
assert!(!result.is_expired);
assert_eq!(result.options[0].percentage, 70.0);
assert_eq!(result.options[1].percentage, 30.0);
}
#[test]
fn test_poll_result_zero_votes() {
let poll = MessagePoll {
id: Uuid::now_v7(),
message_id: Uuid::now_v7(),
question: "Empty poll".to_string(),
allow_multiselect: false,
max_selections: None,
expires_at: None,
total_votes: 0,
created_at: Utc::now(),
updated_at: Utc::now(),
};
let result = PollResult::from_poll(poll, vec![], vec![]);
assert!(result.options.is_empty());
}
}