feat(test): Week 3 质量保障体系 — 55 新增测试 + CI/CD 流水线
前端第二批测试 (42 用例): - AuthBloc: 16 用例 (启动恢复/登录/注册/角色选择/班级码/登出) - HomeBloc: 8 用例 (数据加载/今日检测/心情统计/连续天数/离线容错) - CalendarBloc: 10 用例 (月份切换/日期选择/视图模式/状态保持) - MoodBloc: 8 用例 (统计加载/周期切换/API解析/错误处理) 后端 P0 单元测试 (13 用例): - journal_service: 5 用例 (model_to_resp 转换/mood回退/weather回退/tags解析) - sync_service: 8 用例 (冲突收集/DTO构造/序列化roundtrip/非冲突排除) CI/CD: - pr-check.yml: PR 触发 cargo fmt+check+clippy+test + flutter analyze+test - main-merge.yml: main push 触发完整检查 + cargo audit 安全审计 测试统计: 前端 84 通过, 后端 73 通过 (全部通过)
This commit is contained in:
@@ -304,3 +304,89 @@ fn model_to_resp(model: journal_entry::Model) -> JournalResp {
|
||||
updated_at: model.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dto::{Mood, Weather};
|
||||
use chrono::{NaiveDate, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 构造一个带默认值的测试用 journal_entry::Model
|
||||
fn make_test_model() -> journal_entry::Model {
|
||||
journal_entry::Model {
|
||||
id: Uuid::now_v7(),
|
||||
tenant_id: Uuid::now_v7(),
|
||||
author_id: Uuid::now_v7(),
|
||||
class_id: None,
|
||||
title: "测试日记".to_string(),
|
||||
date: NaiveDate::from_ymd_opt(2026, 6, 1).unwrap(),
|
||||
mood: "\"happy\"".to_string(),
|
||||
weather: "\"sunny\"".to_string(),
|
||||
tags: Some(serde_json::json!(["tag1", "tag2"])),
|
||||
is_private: true,
|
||||
shared_to_class: false,
|
||||
assigned_topic_id: None,
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
created_by: Uuid::now_v7(),
|
||||
updated_by: Uuid::now_v7(),
|
||||
deleted_at: None,
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_to_resp_normal_conversion() {
|
||||
let model = make_test_model();
|
||||
let resp = model_to_resp(model);
|
||||
|
||||
assert_eq!(resp.title, "测试日记");
|
||||
assert!(matches!(resp.mood, Mood::Happy));
|
||||
assert!(matches!(resp.weather, Weather::Sunny));
|
||||
assert_eq!(resp.tags, vec!["tag1", "tag2"]);
|
||||
assert!(resp.is_private);
|
||||
assert!(!resp.shared_to_class);
|
||||
assert_eq!(resp.version, 1);
|
||||
assert!(resp.class_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_to_resp_invalid_mood_falls_back_to_happy() {
|
||||
let mut model = make_test_model();
|
||||
// 不是合法 JSON 字符串,serde_json::from_str 会失败
|
||||
model.mood = "invalid_json".to_string();
|
||||
let resp = model_to_resp(model);
|
||||
|
||||
assert!(matches!(resp.mood, Mood::Happy));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_to_resp_invalid_weather_falls_back_to_sunny() {
|
||||
let mut model = make_test_model();
|
||||
// 不是合法 JSON 字符串
|
||||
model.weather = "xxx".to_string();
|
||||
let resp = model_to_resp(model);
|
||||
|
||||
assert!(matches!(resp.weather, Weather::Sunny));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_to_resp_tags_none_yields_empty_list() {
|
||||
let mut model = make_test_model();
|
||||
model.tags = None;
|
||||
let resp = model_to_resp(model);
|
||||
|
||||
assert!(resp.tags.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_to_resp_tags_not_array_yields_empty_list() {
|
||||
let mut model = make_test_model();
|
||||
// 有效 JSON 但不是数组 → serde_json::from_value::<Vec<String>> 失败
|
||||
model.tags = Some(serde_json::json!("not_an_array"));
|
||||
let resp = model_to_resp(model);
|
||||
|
||||
assert!(resp.tags.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,3 +234,169 @@ impl SyncService {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dto::SyncChange;
|
||||
|
||||
#[test]
|
||||
fn conflict_info_construction() {
|
||||
let journal_id = uuid::Uuid::now_v7();
|
||||
let info = ConflictInfo {
|
||||
journal_id,
|
||||
local_version: 2,
|
||||
server_version: 5,
|
||||
};
|
||||
|
||||
assert_eq!(info.journal_id, journal_id);
|
||||
assert_eq!(info.local_version, 2);
|
||||
assert_eq!(info.server_version, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflict_info_serializes_with_correct_fields() {
|
||||
let info = ConflictInfo {
|
||||
journal_id: uuid::Uuid::nil(),
|
||||
local_version: 1,
|
||||
server_version: 3,
|
||||
};
|
||||
let json = serde_json::to_string(&info).unwrap();
|
||||
|
||||
assert!(json.contains("\"local_version\":1"));
|
||||
assert!(json.contains("\"server_version\":3"));
|
||||
assert!(json.contains("\"journal_id\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_change_create_journal_carries_data() {
|
||||
let data = serde_json::json!({
|
||||
"title": "我的日记",
|
||||
"mood": "happy"
|
||||
});
|
||||
let change = SyncChange::CreateJournal { data: data.clone() };
|
||||
|
||||
// 验证 match 可以正确提取 data
|
||||
match &change {
|
||||
SyncChange::CreateJournal { data } => {
|
||||
assert_eq!(data.get("title").unwrap().as_str().unwrap(), "我的日记");
|
||||
assert_eq!(data.get("mood").unwrap().as_str().unwrap(), "happy");
|
||||
}
|
||||
_ => panic!("Expected CreateJournal variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_change_update_version_extraction() {
|
||||
let id = uuid::Uuid::now_v7();
|
||||
let change = SyncChange::UpdateJournal {
|
||||
id,
|
||||
version: 7,
|
||||
data: serde_json::json!({"title": "更新标题"}),
|
||||
};
|
||||
|
||||
match &change {
|
||||
SyncChange::UpdateJournal {
|
||||
id: cid,
|
||||
version,
|
||||
data,
|
||||
} => {
|
||||
assert_eq!(*cid, id);
|
||||
assert_eq!(*version, 7);
|
||||
assert_eq!(data.get("title").unwrap().as_str().unwrap(), "更新标题");
|
||||
}
|
||||
_ => panic!("Expected UpdateJournal variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_change_delete_version_extraction() {
|
||||
let id = uuid::Uuid::now_v7();
|
||||
let change = SyncChange::DeleteJournal { id, version: 3 };
|
||||
|
||||
match &change {
|
||||
SyncChange::DeleteJournal {
|
||||
id: cid,
|
||||
version,
|
||||
} => {
|
||||
assert_eq!(*cid, id);
|
||||
assert_eq!(*version, 3);
|
||||
}
|
||||
_ => panic!("Expected DeleteJournal variant"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_change_roundtrip_serialization() {
|
||||
let change = SyncChange::UpdateJournal {
|
||||
id: uuid::Uuid::nil(),
|
||||
version: 2,
|
||||
data: serde_json::json!({"title": "test"}),
|
||||
};
|
||||
let json = serde_json::to_string(&change).unwrap();
|
||||
let back: SyncChange = serde_json::from_str(&json).unwrap();
|
||||
|
||||
match back {
|
||||
SyncChange::UpdateJournal { version, .. } => assert_eq!(version, 2),
|
||||
_ => panic!("Expected UpdateJournal after roundtrip"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflict_collection_pattern_mimics_sync_logic() {
|
||||
// 模拟 sync 方法中 VersionConflict 收集到 conflicts 列表的行为
|
||||
let mut conflicts: Vec<ConflictInfo> = Vec::new();
|
||||
|
||||
let errors: Vec<DiaryError> = vec![
|
||||
DiaryError::VersionConflict {
|
||||
local: 1,
|
||||
server: 3,
|
||||
},
|
||||
DiaryError::VersionConflict {
|
||||
local: 2,
|
||||
server: 5,
|
||||
},
|
||||
];
|
||||
|
||||
for e in errors {
|
||||
match e {
|
||||
DiaryError::VersionConflict { local, server } => {
|
||||
conflicts.push(ConflictInfo {
|
||||
journal_id: uuid::Uuid::nil(),
|
||||
local_version: local,
|
||||
server_version: server,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(conflicts.len(), 2);
|
||||
assert_eq!(conflicts[0].local_version, 1);
|
||||
assert_eq!(conflicts[0].server_version, 3);
|
||||
assert_eq!(conflicts[1].local_version, 2);
|
||||
assert_eq!(conflicts[1].server_version, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_conflict_error_does_not_collect() {
|
||||
// 验证非 VersionConflict 错误不会被收集到 conflicts 列表
|
||||
let mut conflicts: Vec<ConflictInfo> = Vec::new();
|
||||
let error = DiaryError::NotFound("日记不存在".to_string());
|
||||
|
||||
match error {
|
||||
DiaryError::VersionConflict { local, server } => {
|
||||
conflicts.push(ConflictInfo {
|
||||
journal_id: uuid::Uuid::nil(),
|
||||
local_version: local,
|
||||
server_version: server,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
// 其他错误不应收集
|
||||
}
|
||||
}
|
||||
|
||||
assert!(conflicts.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user