- src/gotify/api.rs: blocking reqwest 封装(§14);health / applications / messages 三个方法;Bearer token 用 HeaderValue::set_sensitive 标记,日志绝不泄露(§20);10s per-request timeout - src/gotify/model.rs: GotifyHealth / GotifyApplication / GotifyMessage DTO 镜像官方 wire schema;defaultPriority 加 rename(camelCase) - src/gotify/mod.rs: re-export - src/error.rs: 加 AppError::Gotify(String) - Cargo.toml: reqwest 0.12 (blocking + json + rustls-tls) 从注释升级为真 依赖(AGENTS.md §44 模板里早就列了,Phase 1 当时推迟) - tests/gotify_api.rs: 6 个 JSON 解析测试 避坑: - reqwest 0.12 默认 log Bearer token;要 set_sensitive(true) 才行 - reqwest::Error 转 AppError 不能用返回 impl Fn 的 helper(path 是 &str 不是 'static)—— 改成每个调用点 inline closure - Gotify 的 defaultPriority 是 camelCase,其他字段都是单字; 用 #[serde(rename = "defaultPriority")] 单点 rename 而不是全局 rename_all = "camelCase"(因为 Rust 端字段名是 snake_case) 验证:cargo build / build --release 都 exit 0,0 warning - cargo test --test gotify_api: 6 passed; 0 failed - debug 27.88 MB, subsystem = WINDOWS_CUI - release 8.10 MB, subsystem = WINDOWS_GUI(reqwest 加进 release LTO 后体积几乎没变——rustls 是大头但被 strip + LTO 吃掉了)
87 lines
2.7 KiB
Rust
87 lines
2.7 KiB
Rust
//! Gotify wire-format tests.
|
|
//!
|
|
//! Phase 3 V1 only validates JSON parsing — the actual HTTP layer is
|
|
//! exercised manually against a real Gotify instance on the user's LAN.
|
|
//! Adding `wiremock` / `httpmock` for in-process server mocking is a
|
|
//! Phase 8 candidate once perf work makes a richer test suite useful.
|
|
|
|
use windows_gotify_client_rust::gotify::model::{
|
|
GotifyApplication, GotifyHealth, GotifyMessage, ServerConfig,
|
|
};
|
|
|
|
#[test]
|
|
fn server_config_trims_trailing_slash() {
|
|
let a = ServerConfig::new("https://gotify.example.com/", "tok");
|
|
assert_eq!(a.base_url, "https://gotify.example.com");
|
|
let b = ServerConfig::new("https://gotify.example.com", "tok");
|
|
assert_eq!(b.base_url, "https://gotify.example.com");
|
|
}
|
|
|
|
#[test]
|
|
fn health_parses_green() {
|
|
let json = r#"{"health":"green","database":"sqlite"}"#;
|
|
let h: GotifyHealth = serde_json::from_str(json).unwrap();
|
|
assert_eq!(h.health, "green");
|
|
assert_eq!(h.database, "sqlite");
|
|
}
|
|
|
|
#[test]
|
|
fn application_parses_minimal() {
|
|
let json = r#"{
|
|
"id": 1,
|
|
"name": "GitLab Runner",
|
|
"description": "CI build pings",
|
|
"defaultPriority": 1
|
|
}"#;
|
|
let app: GotifyApplication = serde_json::from_str(json).unwrap();
|
|
assert_eq!(app.id, 1);
|
|
assert_eq!(app.name, "GitLab Runner");
|
|
assert_eq!(app.default_priority, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn application_parses_with_optional_defaults() {
|
|
// Gotify may omit description / defaultPriority for clients created via
|
|
// a different code path. Make sure the `#[serde(default)]` works.
|
|
let json = r#"{ "id": 7, "name": "Airflow" }"#;
|
|
let app: GotifyApplication = serde_json::from_str(json).unwrap();
|
|
assert_eq!(app.id, 7);
|
|
assert_eq!(app.name, "Airflow");
|
|
assert_eq!(app.description, "");
|
|
assert_eq!(app.default_priority, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn message_parses_full_record() {
|
|
let json = r#"{
|
|
"id": 1024,
|
|
"appid": 1,
|
|
"message": "Deploy v1.4.2 to production completed in 3m 12s.",
|
|
"title": "Build succeeded",
|
|
"priority": 1,
|
|
"date": "2026-09-19T03:42:18.000Z",
|
|
"extras": {
|
|
"client::display": { "contentType": "text/markdown" }
|
|
}
|
|
}"#;
|
|
let msg: GotifyMessage = serde_json::from_str(json).unwrap();
|
|
assert_eq!(msg.id, 1024);
|
|
assert_eq!(msg.appid, 1);
|
|
assert_eq!(msg.priority, 1);
|
|
assert!(msg.extras.contains_key("client::display"));
|
|
}
|
|
|
|
#[test]
|
|
fn message_parses_with_no_extras() {
|
|
let json = r#"{
|
|
"id": 99,
|
|
"appid": 2,
|
|
"message": "low battery",
|
|
"title": "",
|
|
"priority": 0,
|
|
"date": "2026-09-19T04:00:00Z"
|
|
}"#;
|
|
let msg: GotifyMessage = serde_json::from_str(json).unwrap();
|
|
assert_eq!(msg.title, "");
|
|
assert!(msg.extras.is_empty());
|
|
} |