Skip to main content

max / synckit

6.4 KB · 203 lines History Blame Raw
1 //! OTA release publishing.
2
3 // ── The publisher's sequence ──
4 //
5 // Four authenticated calls in a fixed order (create, register, upload, confirm)
6 // plus one public check. Each writes to a URL the SDK builds itself out of the
7 // session's app id and the release id the server handed back, so the exact path
8 // is the contract: a release created at the wrong URL is a 404 the publisher
9 // only finds out about at release time.
10 use crate::common::*;
11 use uuid::Uuid;
12
13 const APP: &str = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
14 const RELEASE: &str = "11111111-2222-3333-4444-555555555555";
15
16 fn release_id() -> Uuid {
17 Uuid::parse_str(RELEASE).unwrap()
18 }
19
20 fn releases_path() -> String {
21 format!("/api/v1/sync/ota/apps/{APP}/releases")
22 }
23
24 fn artifacts_path() -> String {
25 format!("{}/{RELEASE}/artifacts", releases_path())
26 }
27
28 fn confirm_path() -> String {
29 format!("{}/confirm", artifacts_path())
30 }
31
32 #[tokio::test]
33 async fn create_release_posts_to_the_app_releases_url() {
34 let kit = MockKit::start().await;
35 let client = kit.authed();
36
37 kit.post(&releases_path())
38 .json(json!({
39 "id": RELEASE,
40 "version": "0.4.1",
41 "notes": "Bug fixes",
42 }))
43 .await;
44
45 let release = client
46 .ota_create_release("0.4.1", "Bug fixes")
47 .await
48 .unwrap();
49 assert_eq!(release.id, release_id());
50 assert_eq!(release.version, "0.4.1");
51 assert_eq!(release.notes, "Bug fixes");
52
53 // The app id comes from the session, not from the caller, so the path is
54 // the only place a wrong session would show.
55 assert_eq!(kit.hits(&releases_path()).await, 1);
56 let body = kit.body(&releases_path()).await;
57 assert_eq!(body["version"], "0.4.1");
58 assert_eq!(body["notes"], "Bug fixes");
59
60 let req = &kit.requests_to(&releases_path()).await[0];
61 assert!(
62 req.headers
63 .get("authorization")
64 .expect("the publisher call is authenticated")
65 .to_str()
66 .unwrap()
67 .starts_with("Bearer "),
68 );
69 }
70
71 #[tokio::test]
72 async fn register_artifact_posts_under_its_release_and_returns_the_upload_target() {
73 let kit = MockKit::start().await;
74 let client = kit.authed();
75
76 kit.post(&artifacts_path())
77 .json(json!({
78 "upload_url": "https://s3.example/put?sig=abc",
79 "s3_key": "ota/app/0.4.1/darwin/aarch64/artifact",
80 }))
81 .await;
82
83 let upload = client
84 .ota_register_artifact(release_id(), "darwin", "aarch64", 12_345, "RWS...==")
85 .await
86 .unwrap();
87 assert_eq!(upload.upload_url, "https://s3.example/put?sig=abc");
88 assert_eq!(upload.s3_key, "ota/app/0.4.1/darwin/aarch64/artifact");
89
90 assert_eq!(kit.hits(&artifacts_path()).await, 1);
91 let body = kit.body(&artifacts_path()).await;
92 assert_eq!(body["target"], "darwin");
93 assert_eq!(body["arch"], "aarch64");
94 assert_eq!(body["file_size"], 12_345);
95 assert_eq!(body["signature"], "RWS...==");
96 }
97
98 #[tokio::test]
99 async fn confirm_artifact_posts_to_the_confirm_url_under_its_release() {
100 let kit = MockKit::start().await;
101 let client = kit.authed();
102
103 // Both routes are mounted so a confirm that went to the register URL would
104 // still get a 200 and be caught by the hit counts rather than by an error.
105 kit.post(&artifacts_path()).json(json!({})).await;
106 kit.post(&confirm_path()).code(204).empty().await;
107
108 client
109 .ota_confirm_artifact(release_id(), "linux", "x86_64")
110 .await
111 .unwrap();
112
113 assert_eq!(kit.hits(&confirm_path()).await, 1);
114 assert_eq!(kit.hits(&artifacts_path()).await, 0);
115 let body = kit.body(&confirm_path()).await;
116 assert_eq!(body["target"], "linux");
117 assert_eq!(body["arch"], "x86_64");
118 }
119
120 #[tokio::test]
121 async fn upload_artifact_puts_the_bytes_unencrypted_to_the_presigned_url() {
122 let kit = MockKit::start().await;
123 let client = kit.authed();
124
125 const PRESIGNED: &str = "/s3/ota-artifact";
126 kit.put(PRESIGNED).code(200).empty().await;
127
128 let bytes = b"a tauri bundle, signed elsewhere".to_vec();
129 client
130 .ota_upload_artifact(&kit.url(PRESIGNED), bytes.clone())
131 .await
132 .unwrap();
133
134 let req = &kit.requests_to(PRESIGNED).await[0];
135 // OTA artifacts are public downloads, so the bytes go up as they are: an
136 // encrypted one would fail Tauri's signature check on every installed app.
137 assert_eq!(req.body, bytes);
138 assert_eq!(
139 req.headers.get("content-type").unwrap().to_str().unwrap(),
140 "application/octet-stream"
141 );
142 }
143
144 #[tokio::test]
145 async fn updater_check_reads_the_public_slug_url_and_maps_204_to_no_update() {
146 let kit = MockKit::start().await;
147 let client = kit.client();
148
149 let up_to_date = "/api/v1/sync/ota/goingson/darwin/aarch64/0.4.1";
150 let behind = "/api/v1/sync/ota/goingson/darwin/aarch64/0.4.0";
151
152 kit.get(up_to_date).code(204).empty().await;
153 kit.get(behind)
154 .json(json!({
155 "version": "0.4.1",
156 "url": "https://makenot.work/api/v1/sync/ota/download/abc",
157 "signature": "RWS=",
158 "notes": "Bug fixes",
159 "pub_date": "2026-06-07T00:00:00+00:00",
160 }))
161 .await;
162
163 // The updater endpoint is unauthenticated, so this runs on a client with no
164 // session at all: the version in the path is what selects the answer.
165 let manifest = client
166 .ota_updater_check("goingson", "darwin", "aarch64", "0.4.0")
167 .await
168 .unwrap()
169 .expect("a newer version is offered");
170 assert_eq!(manifest.version, "0.4.1");
171 assert_eq!(manifest.signature, "RWS=");
172 assert_eq!(manifest.notes, "Bug fixes");
173 assert_eq!(manifest.pub_date, "2026-06-07T00:00:00+00:00");
174
175 assert!(
176 client
177 .ota_updater_check("goingson", "darwin", "aarch64", "0.4.1")
178 .await
179 .unwrap()
180 .is_none(),
181 "204 means the caller is current"
182 );
183
184 assert_eq!(kit.hits(behind).await, 1);
185 assert_eq!(kit.hits(up_to_date).await, 1);
186 }
187
188 #[tokio::test]
189 async fn a_publisher_call_without_a_session_never_reaches_the_wire() {
190 let kit = MockKit::start().await;
191 let client = kit.client();
192
193 kit.post(&releases_path()).json(json!({})).await;
194
195 let err = client.ota_create_release("0.4.1", "").await.unwrap_err();
196 assert!(matches!(err, SyncKitError::NotAuthenticated), "got {err:?}");
197 assert_eq!(
198 kit.requests().await.len(),
199 0,
200 "the session check happens before the URL is built"
201 );
202 }
203