Coverage Report

Created: 2026-11-04 02:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
super-tts-app/src/daemon/client/v1/update.rs
Line
Count
Source
1
// SPDX-License-Identifier: GPL-3.0-only
2
//! GET /v1/update and POST /v1/update/check.
3
//! Contract: docs/protocol/endpoints/v1/update.md
4
5
use super_tts_shared::daemon::http_client::transport;
6
use super_tts_shared::daemon::http_client::{HttpError, HttpResult};
7
use super_tts_shared::models::self_update::SelfUpdateStatus;
8
9
use crate::daemon::client::internal::session::with_settings_token;
10
11
/// `Ok(None)` when the daemon predates `/v1/update` (404).
12
0
pub async fn get_update_status() -> HttpResult<Option<SelfUpdateStatus>> {
13
0
    with_settings_token(|socket, token| async move {
14
0
        match transport::get_json::<SelfUpdateStatus>(socket, &token, "/update").await {
15
0
            Ok(s) => Ok(Some(s)),
16
0
            Err(e) if is_not_found(&e) => Ok(None),
17
0
            Err(e) => Err(e),
18
        }
19
0
    })
20
0
    .await
21
0
}
22
23
/// `Ok(None)` when the daemon predates `/v1/update` (404) — same fallback
24
/// as `get_update_status`, since a daemon old enough to lack the `GET` route
25
/// lacks this `POST` one too.
26
0
pub async fn check_update_now() -> HttpResult<Option<SelfUpdateStatus>> {
27
0
    with_settings_token(|socket, token| async move {
28
0
        match transport::post_json::<SelfUpdateStatus>(
29
0
            socket,
30
0
            &token,
31
0
            "/update/check",
32
0
            &serde_json::json!({}),
33
        )
34
0
        .await
35
        {
36
0
            Ok(s) => Ok(Some(s)),
37
0
            Err(e) if is_not_found(&e) => Ok(None),
38
0
            Err(e) => Err(e),
39
        }
40
0
    })
41
0
    .await
42
0
}
43
44
/// True for a "route not found" 404 — the signal that the connected daemon
45
/// predates `/v1/update` entirely. `HttpError` has no typed status (only
46
/// `InvalidSession`/`AuthDenied` are distinguished at the transport layer;
47
/// see `transport.rs:128-151`, `daemon_error`), so this falls back to a
48
/// string match on the status `daemon_error` folds into the message.
49
///
50
/// An old daemon has no `/update` route at all, so axum's default fallback
51
/// (no JSON body) answers — `daemon_error` then has no `error_code`/`message`
52
/// to extract and produces the undetailed form, `"daemon returned HTTP
53
/// 404"` (no parentheses). A *known* route rejecting the request with a
54
/// classified 404 (unlikely for this endpoint, but `daemon_error` is shared
55
/// machinery) would instead produce `"{detail} (HTTP 404)"`. Matching on
56
/// `"HTTP 404"` without the parentheses covers both shapes.
57
4
fn is_not_found(e: &HttpError) -> bool {
58
3
    
matches!2
(e, HttpError::Other(msg) if msg.contains("HTTP 404"
)2
)
59
4
}
60
61
#[cfg(test)]
62
mod tests {
63
    use super::is_not_found;
64
    use super_tts_shared::daemon::http_client::HttpError;
65
66
    /// The shape an *old* daemon (no `/update` route registered) actually
67
    /// produces: axum's default fallback has no body, so `daemon_error` has
68
    /// no `error_code`/`message` to report and falls back to the undetailed
69
    /// form.
70
    #[test]
71
1
    fn detects_undetailed_404() {
72
1
        assert!(is_not_found(&HttpError::Other(
73
1
            "daemon returned HTTP 404".to_string()
74
1
        )));
75
1
    }
76
77
    /// The shape a *known* route's classified 404 would produce, if this
78
    /// endpoint ever grew one.
79
    #[test]
80
1
    fn detects_detailed_404() {
81
1
        assert!(is_not_found(&HttpError::Other(
82
1
            "not_found (HTTP 404)".to_string()
83
1
        )));
84
1
    }
85
86
    #[test]
87
1
    fn does_not_match_other_statuses_or_variants() {
88
1
        assert!(!is_not_found(&HttpError::Other(
89
1
            "daemon returned HTTP 500".to_string()
90
1
        )));
91
1
        assert!(!is_not_found(&HttpError::InvalidSession {
92
1
            reason: "expired".to_string()
93
1
        }));
94
1
    }
95
}