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,196 @@
|
||||
//! Forum article / long-form post — maps to `message_article` table.
|
||||
//!
|
||||
//! Forum articles extend a regular [`Message`] with title, cover image, tags,
|
||||
//! and view/like stats. Rendered as waterfall cards in forum channel views.
|
||||
//! One article per message (1:1), linked via `message_id`.
|
||||
//!
|
||||
//! A message is an article when `message.message_type = "article"`.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Extended metadata for a forum article / post.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct MessageArticle {
|
||||
pub id: Uuid,
|
||||
/// FK to `message.id`. UNIQUE constraint ensures 1:1.
|
||||
pub message_id: Uuid,
|
||||
/// Article title (plain text, max 256 chars).
|
||||
pub title: String,
|
||||
/// Short excerpt for card preview (plain text, max 512 chars).
|
||||
pub summary: Option<String>,
|
||||
/// Cover image URL (displayed at the top of the card).
|
||||
pub cover_url: Option<String>,
|
||||
/// Cover image width in pixels (for waterfall layout height calculation).
|
||||
pub cover_width: Option<i32>,
|
||||
/// Cover image height in pixels.
|
||||
pub cover_height: Option<i32>,
|
||||
/// Cover image dominant color (hex, for placeholder while loading).
|
||||
pub cover_color: Option<String>,
|
||||
/// Tag IDs referencing `forum_tag` table, stored as JSON array.
|
||||
pub tags: Option<serde_json::Value>,
|
||||
/// View count (denormalized, incremented on read).
|
||||
pub view_count: i64,
|
||||
/// Reaction / like count (denormalized).
|
||||
pub like_count: i64,
|
||||
/// Bookmark count (denormalized).
|
||||
pub bookmark_count: i64,
|
||||
/// Reply count (denormalized, threads inside the article).
|
||||
pub reply_count: i64,
|
||||
/// Most recent reply message id.
|
||||
pub last_reply_message_id: Option<Uuid>,
|
||||
/// Most recent reply timestamp.
|
||||
pub last_reply_at: Option<DateTime<Utc>>,
|
||||
/// User id of the last replier.
|
||||
pub last_reply_user_id: Option<Uuid>,
|
||||
/// Whether the article is pinned to the top of the forum channel.
|
||||
pub is_pinned_to_top: bool,
|
||||
/// Whether the question has been answered / resolved.
|
||||
pub is_answered: bool,
|
||||
/// Who marked it as answered.
|
||||
pub answered_by: Option<Uuid>,
|
||||
/// When it was marked as answered.
|
||||
pub answered_at: Option<DateTime<Utc>>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// Waterfall card view — article enriched with author info + first attachment
|
||||
/// (cover fallback). Returned by forum list APIs.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ArticleCard {
|
||||
#[serde(flatten)]
|
||||
pub article: MessageArticle,
|
||||
/// Author display info.
|
||||
pub author: super::message::AuthorInfo,
|
||||
/// Resolved tag names (e.g. ["Bug Report", "High Priority"]).
|
||||
pub tag_names: Vec<String>,
|
||||
/// First image attachment URL (fallback when cover_url is NULL).
|
||||
pub first_image_url: Option<String>,
|
||||
/// Whether the current user has bookmarked this article.
|
||||
pub bookmarked: bool,
|
||||
/// Whether the current user has liked this article.
|
||||
pub liked: bool,
|
||||
}
|
||||
|
||||
/// Input payload for creating a new forum article.
|
||||
///
|
||||
/// Sent by the client when composing a forum post.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreateArticleInput {
|
||||
pub channel_id: Uuid,
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub summary: Option<String>,
|
||||
pub cover_url: Option<String>,
|
||||
pub tags: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
/// Input payload for updating an existing forum article.
|
||||
///
|
||||
/// All fields are optional — only provided fields are updated.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateArticleInput {
|
||||
pub title: Option<String>,
|
||||
pub body: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
pub cover_url: Option<String>,
|
||||
pub cover_color: Option<String>,
|
||||
pub tags: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
/// Sort modes for forum article listing.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ArticleSort {
|
||||
/// Most recent activity first.
|
||||
#[default]
|
||||
LatestActivity,
|
||||
/// Newest articles first.
|
||||
Newest,
|
||||
/// Most viewed first.
|
||||
MostViewed,
|
||||
/// Most liked first.
|
||||
MostLiked,
|
||||
/// Pinned articles first, then by latest activity.
|
||||
PinnedFirst,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_article_card_serialize() {
|
||||
let article = MessageArticle {
|
||||
id: Uuid::now_v7(),
|
||||
message_id: Uuid::now_v7(),
|
||||
title: "Bug Report: Login fails on Safari".into(),
|
||||
summary: Some("Users report that login fails on Safari 18...".into()),
|
||||
cover_url: Some("https://cdn.example.com/covers/bug.png".into()),
|
||||
cover_width: Some(1200),
|
||||
cover_height: Some(630),
|
||||
cover_color: Some("#FF6B6B".into()),
|
||||
tags: Some(serde_json::json!(["01909a", "01909b"])),
|
||||
view_count: 142,
|
||||
like_count: 7,
|
||||
bookmark_count: 3,
|
||||
reply_count: 5,
|
||||
last_reply_message_id: Some(Uuid::now_v7()),
|
||||
last_reply_at: Some(Utc::now()),
|
||||
last_reply_user_id: Some(Uuid::now_v7()),
|
||||
is_pinned_to_top: true,
|
||||
is_answered: false,
|
||||
answered_by: None,
|
||||
answered_at: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
};
|
||||
|
||||
let card = ArticleCard {
|
||||
article,
|
||||
author: super::super::message::AuthorInfo {
|
||||
id: Uuid::now_v7(),
|
||||
username: "alice".into(),
|
||||
display_name: Some("Alice".into()),
|
||||
avatar_url: None,
|
||||
is_bot: false,
|
||||
},
|
||||
tag_names: vec!["Bug Report".into(), "High Priority".into()],
|
||||
first_image_url: None,
|
||||
bookmarked: false,
|
||||
liked: true,
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&card).unwrap();
|
||||
assert_eq!(json["title"], "Bug Report: Login fails on Safari");
|
||||
assert_eq!(json["view_count"], 142);
|
||||
assert_eq!(json["author"]["username"], "alice");
|
||||
assert_eq!(json["tag_names"][0], "Bug Report");
|
||||
assert_eq!(json["is_pinned_to_top"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_article_sort_serialize() {
|
||||
let sort = ArticleSort::MostViewed;
|
||||
let json = serde_json::to_value(sort).unwrap();
|
||||
assert_eq!(json, "most_viewed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_article_input_serialize() {
|
||||
let input = CreateArticleInput {
|
||||
channel_id: Uuid::now_v7(),
|
||||
title: "New Feature Proposal".into(),
|
||||
body: "I'd like to propose...".into(),
|
||||
summary: Some("A proposal for...".into()),
|
||||
cover_url: None,
|
||||
tags: Some(vec![Uuid::now_v7()]),
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&input).unwrap();
|
||||
assert_eq!(json["title"], "New Feature Proposal");
|
||||
assert_eq!(json["tags"].as_array().unwrap().len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user