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:
@@ -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.0–100.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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user