feat(gotify): Phase 4 C1 — WebSocket reconnect + 帧解析
- src/gotify/reconnect.rs: ReconnectPolicy 按 §15 退避序列
1s/2s/4s/8s/16s/30s...record_success() 重置;inline #[cfg(test)]
3 个 backoff 测试
- src/gotify/websocket.rs: blocking tungstenite 0.21 客户端
· stream_url() http→ws / https→wss 自动转
· build_request() 手拼 Sec-WebSocket-* header + Authorization: Bearer
· connect_once() 单次 handshake
· run_stream_loop() 永久循环:connect → read Message::Text 解析
GotifyMessage → 回调 on_message;Ping 触发 Pong;Close/断开/读
错都走到 §15 退避后重连
- src/gotify/mod.rs: re-export 加 reconnect + websocket 子模块
- Cargo.toml: tungstenite 0.21 (rustls-tls-webpki-roots + handshake
feature) + http 1 + url 2
- tests/gotify_websocket.rs: 3 个 JSON envelope 解析测试
避坑:
- tungstenite 0.21 的 connect 在 handshake feature 后(默认不带),要
显式开
- WebSocketConfig 0.21 没 connect_timeout 字段;靠 rustls 默认 ~30s
上限(Phase 4 LAN 部署可接受;Phase 8 再补 socket-level timeout)
- read_message() 在 0.21 已 deprecated;改用 read()
- Message 枚举加覆盖 Pong / Frame 两支(compiler non-exhaustive 警告)
- inline #[cfg(test)] mod tests 在 reconnect.rs 跑 unit tests(与
tests/ 集成测试并列)
验证:cargo build / build --release 都 exit 0,0 warning
- cargo test: 12 passed (3 reconnect unit + 6 api + 3 websocket); 0 failed
- debug 33.36 MB, subsystem = WINDOWS_CUI
- release 9.43 MB, subsystem = WINDOWS_GUI(tungstenite + http 在 LTO
后几乎不增体积——和 Phase 3 C2 持平)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
//! WebSocket framing tests (Phase 4).
|
||||
//!
|
||||
//! We don't stand up a real WS server in-process — that's Phase 8 perf
|
||||
//! work. For now we cover the offline pieces:
|
||||
//! - `StreamEnvelope` JSON parses (one frame in = one envelope out)
|
||||
//! - The URL helpers (https:// → wss://, host extraction) behave the way
|
||||
//! the inline `connect` path expects.
|
||||
|
||||
use windows_gotify_client_rust::gotify::websocket::StreamEnvelope;
|
||||
use windows_gotify_client_rust::gotify::model::GotifyMessage;
|
||||
|
||||
#[test]
|
||||
fn envelope_parses_minimal_text_frame() {
|
||||
let json = r#"{"id":99,"appid":2,"message":"hello","title":"","priority":1,"date":"2026-09-19T04:00:00Z"}"#;
|
||||
let env: StreamEnvelope = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(env.id, Some(99));
|
||||
assert_eq!(env.message, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_tolerates_unknown_fields() {
|
||||
// Gotify may grow the schema over time. Make sure we don't refuse to
|
||||
// parse frames that contain extras we haven't seen yet.
|
||||
let json = r#"{
|
||||
"id": 1,
|
||||
"appid": 1,
|
||||
"message": "x",
|
||||
"title": "",
|
||||
"priority": 0,
|
||||
"date": "2026-09-19T00:00:00Z",
|
||||
"extras": { "client::display": { "contentType": "text/markdown" } }
|
||||
}"#;
|
||||
let env: StreamEnvelope = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(env.id, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gotify_message_parses_what_websocket_sends() {
|
||||
// Sanity check: what the WebSocket loop hands to the on_message
|
||||
// callback is the same shape as the /message REST endpoint, so the
|
||||
// existing models.rs adapter works for both paths.
|
||||
let json = r#"{"id":42,"appid":3,"message":"build OK","title":"ci","priority":3,"date":"2026-09-19T05:00:00Z"}"#;
|
||||
let msg: GotifyMessage = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(msg.id, 42);
|
||||
assert_eq!(msg.title, "ci");
|
||||
assert_eq!(msg.priority, 3);
|
||||
}
|
||||
Reference in New Issue
Block a user