forked from
lewis.moe/bspds-sandbox
I've been saying "PDSes seem easy enough, they're what, some CRUD to a db? I can do that in my sleep". well i'm sleeping rn so let's go
1mod common;
2use chrono::Utc;
3use common::*;
4use reqwest::StatusCode;
5use serde_json::{Value, json};
6
7#[tokio::test]
8async fn test_apply_writes_create() {
9 let client = client();
10 let (token, did) = create_account_and_login(&client).await;
11 let now = Utc::now().to_rfc3339();
12 let payload = json!({
13 "repo": did,
14 "writes": [
15 {
16 "$type": "com.atproto.repo.applyWrites#create",
17 "collection": "app.bsky.feed.post",
18 "value": {
19 "$type": "app.bsky.feed.post",
20 "text": "Batch created post 1",
21 "createdAt": now
22 }
23 },
24 {
25 "$type": "com.atproto.repo.applyWrites#create",
26 "collection": "app.bsky.feed.post",
27 "value": {
28 "$type": "app.bsky.feed.post",
29 "text": "Batch created post 2",
30 "createdAt": now
31 }
32 }
33 ]
34 });
35 let res = client
36 .post(format!(
37 "{}/xrpc/com.atproto.repo.applyWrites",
38 base_url().await
39 ))
40 .bearer_auth(&token)
41 .json(&payload)
42 .send()
43 .await
44 .expect("Failed to send request");
45 assert_eq!(res.status(), StatusCode::OK);
46 let body: Value = res.json().await.expect("Response was not valid JSON");
47 assert!(body["commit"]["cid"].is_string());
48 assert!(body["results"].is_array());
49 let results = body["results"].as_array().unwrap();
50 assert_eq!(results.len(), 2);
51 assert!(results[0]["uri"].is_string());
52 assert!(results[0]["cid"].is_string());
53}
54
55#[tokio::test]
56async fn test_apply_writes_update() {
57 let client = client();
58 let (token, did) = create_account_and_login(&client).await;
59 let now = Utc::now().to_rfc3339();
60 let rkey = format!("batch_update_{}", Utc::now().timestamp_millis());
61 let create_payload = json!({
62 "repo": did,
63 "collection": "app.bsky.feed.post",
64 "rkey": rkey,
65 "record": {
66 "$type": "app.bsky.feed.post",
67 "text": "Original post",
68 "createdAt": now
69 }
70 });
71 let res = client
72 .post(format!(
73 "{}/xrpc/com.atproto.repo.putRecord",
74 base_url().await
75 ))
76 .bearer_auth(&token)
77 .json(&create_payload)
78 .send()
79 .await
80 .expect("Failed to create");
81 assert_eq!(res.status(), StatusCode::OK);
82 let update_payload = json!({
83 "repo": did,
84 "writes": [
85 {
86 "$type": "com.atproto.repo.applyWrites#update",
87 "collection": "app.bsky.feed.post",
88 "rkey": rkey,
89 "value": {
90 "$type": "app.bsky.feed.post",
91 "text": "Updated post via applyWrites",
92 "createdAt": now
93 }
94 }
95 ]
96 });
97 let res = client
98 .post(format!(
99 "{}/xrpc/com.atproto.repo.applyWrites",
100 base_url().await
101 ))
102 .bearer_auth(&token)
103 .json(&update_payload)
104 .send()
105 .await
106 .expect("Failed to send request");
107 assert_eq!(res.status(), StatusCode::OK);
108 let body: Value = res.json().await.expect("Response was not valid JSON");
109 let results = body["results"].as_array().unwrap();
110 assert_eq!(results.len(), 1);
111 assert!(results[0]["uri"].is_string());
112}
113
114#[tokio::test]
115async fn test_apply_writes_delete() {
116 let client = client();
117 let (token, did) = create_account_and_login(&client).await;
118 let now = Utc::now().to_rfc3339();
119 let rkey = format!("batch_delete_{}", Utc::now().timestamp_millis());
120 let create_payload = json!({
121 "repo": did,
122 "collection": "app.bsky.feed.post",
123 "rkey": rkey,
124 "record": {
125 "$type": "app.bsky.feed.post",
126 "text": "Post to delete",
127 "createdAt": now
128 }
129 });
130 let res = client
131 .post(format!(
132 "{}/xrpc/com.atproto.repo.putRecord",
133 base_url().await
134 ))
135 .bearer_auth(&token)
136 .json(&create_payload)
137 .send()
138 .await
139 .expect("Failed to create");
140 assert_eq!(res.status(), StatusCode::OK);
141 let delete_payload = json!({
142 "repo": did,
143 "writes": [
144 {
145 "$type": "com.atproto.repo.applyWrites#delete",
146 "collection": "app.bsky.feed.post",
147 "rkey": rkey
148 }
149 ]
150 });
151 let res = client
152 .post(format!(
153 "{}/xrpc/com.atproto.repo.applyWrites",
154 base_url().await
155 ))
156 .bearer_auth(&token)
157 .json(&delete_payload)
158 .send()
159 .await
160 .expect("Failed to send request");
161 assert_eq!(res.status(), StatusCode::OK);
162 let get_res = client
163 .get(format!(
164 "{}/xrpc/com.atproto.repo.getRecord",
165 base_url().await
166 ))
167 .query(&[
168 ("repo", did.as_str()),
169 ("collection", "app.bsky.feed.post"),
170 ("rkey", rkey.as_str()),
171 ])
172 .send()
173 .await
174 .expect("Failed to verify");
175 assert_eq!(get_res.status(), StatusCode::NOT_FOUND);
176}
177
178#[tokio::test]
179async fn test_apply_writes_mixed_operations() {
180 let client = client();
181 let (token, did) = create_account_and_login(&client).await;
182 let now = Utc::now().to_rfc3339();
183 let rkey_to_delete = format!("mixed_del_{}", Utc::now().timestamp_millis());
184 let rkey_to_update = format!("mixed_upd_{}", Utc::now().timestamp_millis());
185 let setup_payload = json!({
186 "repo": did,
187 "writes": [
188 {
189 "$type": "com.atproto.repo.applyWrites#create",
190 "collection": "app.bsky.feed.post",
191 "rkey": rkey_to_delete,
192 "value": {
193 "$type": "app.bsky.feed.post",
194 "text": "To be deleted",
195 "createdAt": now
196 }
197 },
198 {
199 "$type": "com.atproto.repo.applyWrites#create",
200 "collection": "app.bsky.feed.post",
201 "rkey": rkey_to_update,
202 "value": {
203 "$type": "app.bsky.feed.post",
204 "text": "To be updated",
205 "createdAt": now
206 }
207 }
208 ]
209 });
210 let res = client
211 .post(format!(
212 "{}/xrpc/com.atproto.repo.applyWrites",
213 base_url().await
214 ))
215 .bearer_auth(&token)
216 .json(&setup_payload)
217 .send()
218 .await
219 .expect("Failed to setup");
220 assert_eq!(res.status(), StatusCode::OK);
221 let mixed_payload = json!({
222 "repo": did,
223 "writes": [
224 {
225 "$type": "com.atproto.repo.applyWrites#create",
226 "collection": "app.bsky.feed.post",
227 "value": {
228 "$type": "app.bsky.feed.post",
229 "text": "New post",
230 "createdAt": now
231 }
232 },
233 {
234 "$type": "com.atproto.repo.applyWrites#update",
235 "collection": "app.bsky.feed.post",
236 "rkey": rkey_to_update,
237 "value": {
238 "$type": "app.bsky.feed.post",
239 "text": "Updated text",
240 "createdAt": now
241 }
242 },
243 {
244 "$type": "com.atproto.repo.applyWrites#delete",
245 "collection": "app.bsky.feed.post",
246 "rkey": rkey_to_delete
247 }
248 ]
249 });
250 let res = client
251 .post(format!(
252 "{}/xrpc/com.atproto.repo.applyWrites",
253 base_url().await
254 ))
255 .bearer_auth(&token)
256 .json(&mixed_payload)
257 .send()
258 .await
259 .expect("Failed to send request");
260 assert_eq!(res.status(), StatusCode::OK);
261 let body: Value = res.json().await.expect("Response was not valid JSON");
262 let results = body["results"].as_array().unwrap();
263 assert_eq!(results.len(), 3);
264}
265
266#[tokio::test]
267async fn test_apply_writes_no_auth() {
268 let client = client();
269 let payload = json!({
270 "repo": "did:plc:test",
271 "writes": [
272 {
273 "$type": "com.atproto.repo.applyWrites#create",
274 "collection": "app.bsky.feed.post",
275 "value": {
276 "$type": "app.bsky.feed.post",
277 "text": "Test",
278 "createdAt": "2025-01-01T00:00:00Z"
279 }
280 }
281 ]
282 });
283 let res = client
284 .post(format!(
285 "{}/xrpc/com.atproto.repo.applyWrites",
286 base_url().await
287 ))
288 .json(&payload)
289 .send()
290 .await
291 .expect("Failed to send request");
292 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
293}
294
295#[tokio::test]
296async fn test_apply_writes_empty_writes() {
297 let client = client();
298 let (token, did) = create_account_and_login(&client).await;
299 let payload = json!({
300 "repo": did,
301 "writes": []
302 });
303 let res = client
304 .post(format!(
305 "{}/xrpc/com.atproto.repo.applyWrites",
306 base_url().await
307 ))
308 .bearer_auth(&token)
309 .json(&payload)
310 .send()
311 .await
312 .expect("Failed to send request");
313 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
314}