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 | | } |