+40
.sqlx/query-2ff22a8c39914689d6cf215ba201fa4ced50b7a003ce01bf7603a7f125113447.json
+40
.sqlx/query-2ff22a8c39914689d6cf215ba201fa4ced50b7a003ce01bf7603a7f125113447.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT code, available_uses, created_at, disabled\n FROM invite_codes\n WHERE created_by_user = $1\n ORDER BY created_at DESC\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "code",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "available_uses",
14
+
"type_info": "Int4"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "created_at",
19
+
"type_info": "Timestamptz"
20
+
},
21
+
{
22
+
"ordinal": 3,
23
+
"name": "disabled",
24
+
"type_info": "Bool"
25
+
}
26
+
],
27
+
"parameters": {
28
+
"Left": [
29
+
"Uuid"
30
+
]
31
+
},
32
+
"nullable": [
33
+
false,
34
+
false,
35
+
false,
36
+
true
37
+
]
38
+
},
39
+
"hash": "2ff22a8c39914689d6cf215ba201fa4ced50b7a003ce01bf7603a7f125113447"
40
+
}
+14
.sqlx/query-3609b5817e4564b824b0c0f4fe32488ee7caed02cee08fb163e4914c5349eb11.json
+14
.sqlx/query-3609b5817e4564b824b0c0f4fe32488ee7caed02cee08fb163e4914c5349eb11.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE users SET invites_disabled = TRUE WHERE did = $1",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text"
9
+
]
10
+
},
11
+
"nullable": []
12
+
},
13
+
"hash": "3609b5817e4564b824b0c0f4fe32488ee7caed02cee08fb163e4914c5349eb11"
14
+
}
+15
.sqlx/query-411a7cff2d43612379903d6343da0761ae5b8b30a2fa1c89afb85047d4fbe3eb.json
+15
.sqlx/query-411a7cff2d43612379903d6343da0761ae5b8b30a2fa1c89afb85047d4fbe3eb.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE records SET takedown_ref = $1 WHERE record_cid = $2",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text",
9
+
"Text"
10
+
]
11
+
},
12
+
"nullable": []
13
+
},
14
+
"hash": "411a7cff2d43612379903d6343da0761ae5b8b30a2fa1c89afb85047d4fbe3eb"
15
+
}
+14
.sqlx/query-413c5b03501a399dca13f345fcae05770517091d73db93966853e944c68ee237.json
+14
.sqlx/query-413c5b03501a399dca13f345fcae05770517091d73db93966853e944c68ee237.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Uuid"
9
+
]
10
+
},
11
+
"nullable": []
12
+
},
13
+
"hash": "413c5b03501a399dca13f345fcae05770517091d73db93966853e944c68ee237"
14
+
}
+15
.sqlx/query-41d35cebdf29be500e30ef636ad96450620f71087c174e5a74446fcdb29a2ba8.json
+15
.sqlx/query-41d35cebdf29be500e30ef636ad96450620f71087c174e5a74446fcdb29a2ba8.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE blobs SET takedown_ref = $1 WHERE cid = $2",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text",
9
+
"Text"
10
+
]
11
+
},
12
+
"nullable": []
13
+
},
14
+
"hash": "41d35cebdf29be500e30ef636ad96450620f71087c174e5a74446fcdb29a2ba8"
15
+
}
+28
.sqlx/query-5d5442136932d4088873a935c41cb3a683c4771e4fb8c151b3fd5119fb6c1068.json
+28
.sqlx/query-5d5442136932d4088873a935c41cb3a683c4771e4fb8c151b3fd5119fb6c1068.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT u.did, icu.used_at\n FROM invite_code_uses icu\n JOIN users u ON icu.used_by_user = u.id\n WHERE icu.code = $1\n ORDER BY icu.used_at DESC\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "did",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "used_at",
14
+
"type_info": "Timestamptz"
15
+
}
16
+
],
17
+
"parameters": {
18
+
"Left": [
19
+
"Text"
20
+
]
21
+
},
22
+
"nullable": [
23
+
false,
24
+
false
25
+
]
26
+
},
27
+
"hash": "5d5442136932d4088873a935c41cb3a683c4771e4fb8c151b3fd5119fb6c1068"
28
+
}
+28
.sqlx/query-62942bd21d545eb15bfea4f46378b6c2ebfe12b8bc9e27c63a6c0f77a9105303.json
+28
.sqlx/query-62942bd21d545eb15bfea4f46378b6c2ebfe12b8bc9e27c63a6c0f77a9105303.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT cid, takedown_ref FROM blobs WHERE cid = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "cid",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "takedown_ref",
14
+
"type_info": "Text"
15
+
}
16
+
],
17
+
"parameters": {
18
+
"Left": [
19
+
"Text"
20
+
]
21
+
},
22
+
"nullable": [
23
+
false,
24
+
true
25
+
]
26
+
},
27
+
"hash": "62942bd21d545eb15bfea4f46378b6c2ebfe12b8bc9e27c63a6c0f77a9105303"
28
+
}
+22
.sqlx/query-6819c68a3c06083a826eb94271cc8ff0d4c2bbd33b9051f50a1a46ecc8d3e85b.json
+22
.sqlx/query-6819c68a3c06083a826eb94271cc8ff0d4c2bbd33b9051f50a1a46ecc8d3e85b.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT did FROM users WHERE id = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "did",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Uuid"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "6819c68a3c06083a826eb94271cc8ff0d4c2bbd33b9051f50a1a46ecc8d3e85b"
22
+
}
+14
.sqlx/query-78ed180c33b8f1f7a3adcd3dd0e7e5988ae1dbc2e10009df9fe44fb0fbbe95b3.json
+14
.sqlx/query-78ed180c33b8f1f7a3adcd3dd0e7e5988ae1dbc2e10009df9fe44fb0fbbe95b3.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE users SET invites_disabled = FALSE WHERE did = $1",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text"
9
+
]
10
+
},
11
+
"nullable": []
12
+
},
13
+
"hash": "78ed180c33b8f1f7a3adcd3dd0e7e5988ae1dbc2e10009df9fe44fb0fbbe95b3"
14
+
}
+14
.sqlx/query-7b2d1d4ac06063e07a7c7a7d0fb434db08ce312eb2864405d7f96f4e985ed036.json
+14
.sqlx/query-7b2d1d4ac06063e07a7c7a7d0fb434db08ce312eb2864405d7f96f4e985ed036.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE invite_codes SET disabled = TRUE WHERE code = $1",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text"
9
+
]
10
+
},
11
+
"nullable": []
12
+
},
13
+
"hash": "7b2d1d4ac06063e07a7c7a7d0fb434db08ce312eb2864405d7f96f4e985ed036"
14
+
}
+34
.sqlx/query-7d1617283733986244b8129cdd14ec1d04510aa73e4ae350a54f57629b9eaff9.json
+34
.sqlx/query-7d1617283733986244b8129cdd14ec1d04510aa73e4ae350a54f57629b9eaff9.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT did, deactivated_at, takedown_ref FROM users WHERE did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "did",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "deactivated_at",
14
+
"type_info": "Timestamptz"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "takedown_ref",
19
+
"type_info": "Text"
20
+
}
21
+
],
22
+
"parameters": {
23
+
"Left": [
24
+
"Text"
25
+
]
26
+
},
27
+
"nullable": [
28
+
false,
29
+
true,
30
+
true
31
+
]
32
+
},
33
+
"hash": "7d1617283733986244b8129cdd14ec1d04510aa73e4ae350a54f57629b9eaff9"
34
+
}
+16
.sqlx/query-bbe639bb24cc1bb3cc144baae263e7e3411e185bf7c91751ee1046c64a81df52.json
+16
.sqlx/query-bbe639bb24cc1bb3cc144baae263e7e3411e185bf7c91751ee1046c64a81df52.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "INSERT INTO invite_codes (code, available_uses, created_by_user) VALUES ($1, $2, $3)",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text",
9
+
"Int4",
10
+
"Uuid"
11
+
]
12
+
},
13
+
"nullable": []
14
+
},
15
+
"hash": "bbe639bb24cc1bb3cc144baae263e7e3411e185bf7c91751ee1046c64a81df52"
16
+
}
+15
.sqlx/query-cd25ddc034a51748f699e2fcd1312691123aee9904eb2ee4073ed0f2c8c49bf9.json
+15
.sqlx/query-cd25ddc034a51748f699e2fcd1312691123aee9904eb2ee4073ed0f2c8c49bf9.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "UPDATE users SET takedown_ref = $1 WHERE did = $2",
4
+
"describe": {
5
+
"columns": [],
6
+
"parameters": {
7
+
"Left": [
8
+
"Text",
9
+
"Text"
10
+
]
11
+
},
12
+
"nullable": []
13
+
},
14
+
"hash": "cd25ddc034a51748f699e2fcd1312691123aee9904eb2ee4073ed0f2c8c49bf9"
15
+
}
+22
.sqlx/query-da0e9a9edad3895ed5015b52335f5a0256e7bdc6c79e6faa927414d68800404c.json
+22
.sqlx/query-da0e9a9edad3895ed5015b52335f5a0256e7bdc6c79e6faa927414d68800404c.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT invites_disabled FROM users WHERE did = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "invites_disabled",
9
+
"type_info": "Bool"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Text"
15
+
]
16
+
},
17
+
"nullable": [
18
+
true
19
+
]
20
+
},
21
+
"hash": "da0e9a9edad3895ed5015b52335f5a0256e7bdc6c79e6faa927414d68800404c"
22
+
}
+28
.sqlx/query-fbc8ab04fe5e06d6e6de9a4eeaabee8af9ee887812bcfe5893df1c7e682747c1.json
+28
.sqlx/query-fbc8ab04fe5e06d6e6de9a4eeaabee8af9ee887812bcfe5893df1c7e682747c1.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "SELECT r.id, r.takedown_ref FROM records r WHERE r.record_cid = $1",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "id",
9
+
"type_info": "Uuid"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "takedown_ref",
14
+
"type_info": "Text"
15
+
}
16
+
],
17
+
"parameters": {
18
+
"Left": [
19
+
"Text"
20
+
]
21
+
},
22
+
"nullable": [
23
+
false,
24
+
true
25
+
]
26
+
},
27
+
"hash": "fbc8ab04fe5e06d6e6de9a4eeaabee8af9ee887812bcfe5893df1c7e682747c1"
28
+
}
-2414
tests/lifecycle.rs
-2414
tests/lifecycle.rs
···
1
-
mod common;
2
-
use common::*;
3
-
4
-
use base64::Engine;
5
-
use chrono::Utc;
6
-
use reqwest::{self, StatusCode, header};
7
-
use serde_json::{Value, json};
8
-
use std::time::Duration;
9
-
10
-
async fn setup_new_user(handle_prefix: &str) -> (String, String) {
11
-
let client = client();
12
-
let ts = Utc::now().timestamp_millis();
13
-
let handle = format!("{}-{}.test", handle_prefix, ts);
14
-
let email = format!("{}-{}@test.com", handle_prefix, ts);
15
-
let password = "e2e-password-123";
16
-
17
-
let create_account_payload = json!({
18
-
"handle": handle,
19
-
"email": email,
20
-
"password": password
21
-
});
22
-
let create_res = client
23
-
.post(format!(
24
-
"{}/xrpc/com.atproto.server.createAccount",
25
-
base_url().await
26
-
))
27
-
.json(&create_account_payload)
28
-
.send()
29
-
.await
30
-
.expect("setup_new_user: Failed to send createAccount");
31
-
32
-
if create_res.status() != reqwest::StatusCode::OK {
33
-
panic!(
34
-
"setup_new_user: Failed to create account: {:?}",
35
-
create_res.text().await
36
-
);
37
-
}
38
-
39
-
let create_body: Value = create_res
40
-
.json()
41
-
.await
42
-
.expect("setup_new_user: createAccount response was not JSON");
43
-
44
-
let new_did = create_body["did"]
45
-
.as_str()
46
-
.expect("setup_new_user: Response had no DID")
47
-
.to_string();
48
-
let new_jwt = create_body["accessJwt"]
49
-
.as_str()
50
-
.expect("setup_new_user: Response had no accessJwt")
51
-
.to_string();
52
-
53
-
(new_did, new_jwt)
54
-
}
55
-
56
-
#[tokio::test]
57
-
async fn test_post_crud_lifecycle() {
58
-
let client = client();
59
-
let (did, jwt) = setup_new_user("lifecycle-crud").await;
60
-
let collection = "app.bsky.feed.post";
61
-
62
-
let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis());
63
-
let now = Utc::now().to_rfc3339();
64
-
65
-
let original_text = "Hello from the lifecycle test!";
66
-
let create_payload = json!({
67
-
"repo": did,
68
-
"collection": collection,
69
-
"rkey": rkey,
70
-
"record": {
71
-
"$type": collection,
72
-
"text": original_text,
73
-
"createdAt": now
74
-
}
75
-
});
76
-
77
-
let create_res = client
78
-
.post(format!(
79
-
"{}/xrpc/com.atproto.repo.putRecord",
80
-
base_url().await
81
-
))
82
-
.bearer_auth(&jwt)
83
-
.json(&create_payload)
84
-
.send()
85
-
.await
86
-
.expect("Failed to send create request");
87
-
88
-
if create_res.status() != reqwest::StatusCode::OK {
89
-
let status = create_res.status();
90
-
let body = create_res
91
-
.text()
92
-
.await
93
-
.unwrap_or_else(|_| "Could not get body".to_string());
94
-
panic!(
95
-
"Failed to create record. Status: {}, Body: {}",
96
-
status, body
97
-
);
98
-
}
99
-
100
-
let create_body: Value = create_res
101
-
.json()
102
-
.await
103
-
.expect("create response was not JSON");
104
-
let uri = create_body["uri"].as_str().unwrap();
105
-
106
-
let params = [
107
-
("repo", did.as_str()),
108
-
("collection", collection),
109
-
("rkey", &rkey),
110
-
];
111
-
let get_res = client
112
-
.get(format!(
113
-
"{}/xrpc/com.atproto.repo.getRecord",
114
-
base_url().await
115
-
))
116
-
.query(¶ms)
117
-
.send()
118
-
.await
119
-
.expect("Failed to send get request");
120
-
121
-
assert_eq!(
122
-
get_res.status(),
123
-
reqwest::StatusCode::OK,
124
-
"Failed to get record after create"
125
-
);
126
-
let get_body: Value = get_res.json().await.expect("get response was not JSON");
127
-
assert_eq!(get_body["uri"], uri);
128
-
assert_eq!(get_body["value"]["text"], original_text);
129
-
130
-
let updated_text = "This post has been updated.";
131
-
let update_payload = json!({
132
-
"repo": did,
133
-
"collection": collection,
134
-
"rkey": rkey,
135
-
"record": {
136
-
"$type": collection,
137
-
"text": updated_text,
138
-
"createdAt": now
139
-
}
140
-
});
141
-
142
-
let update_res = client
143
-
.post(format!(
144
-
"{}/xrpc/com.atproto.repo.putRecord",
145
-
base_url().await
146
-
))
147
-
.bearer_auth(&jwt)
148
-
.json(&update_payload)
149
-
.send()
150
-
.await
151
-
.expect("Failed to send update request");
152
-
153
-
assert_eq!(
154
-
update_res.status(),
155
-
reqwest::StatusCode::OK,
156
-
"Failed to update record"
157
-
);
158
-
159
-
let get_updated_res = client
160
-
.get(format!(
161
-
"{}/xrpc/com.atproto.repo.getRecord",
162
-
base_url().await
163
-
))
164
-
.query(¶ms)
165
-
.send()
166
-
.await
167
-
.expect("Failed to send get-after-update request");
168
-
169
-
assert_eq!(
170
-
get_updated_res.status(),
171
-
reqwest::StatusCode::OK,
172
-
"Failed to get record after update"
173
-
);
174
-
let get_updated_body: Value = get_updated_res
175
-
.json()
176
-
.await
177
-
.expect("get-updated response was not JSON");
178
-
assert_eq!(
179
-
get_updated_body["value"]["text"], updated_text,
180
-
"Text was not updated"
181
-
);
182
-
183
-
let delete_payload = json!({
184
-
"repo": did,
185
-
"collection": collection,
186
-
"rkey": rkey
187
-
});
188
-
189
-
let delete_res = client
190
-
.post(format!(
191
-
"{}/xrpc/com.atproto.repo.deleteRecord",
192
-
base_url().await
193
-
))
194
-
.bearer_auth(&jwt)
195
-
.json(&delete_payload)
196
-
.send()
197
-
.await
198
-
.expect("Failed to send delete request");
199
-
200
-
assert_eq!(
201
-
delete_res.status(),
202
-
reqwest::StatusCode::OK,
203
-
"Failed to delete record"
204
-
);
205
-
206
-
let get_deleted_res = client
207
-
.get(format!(
208
-
"{}/xrpc/com.atproto.repo.getRecord",
209
-
base_url().await
210
-
))
211
-
.query(¶ms)
212
-
.send()
213
-
.await
214
-
.expect("Failed to send get-after-delete request");
215
-
216
-
assert_eq!(
217
-
get_deleted_res.status(),
218
-
reqwest::StatusCode::NOT_FOUND,
219
-
"Record was found, but it should be deleted"
220
-
);
221
-
}
222
-
223
-
#[tokio::test]
224
-
async fn test_record_update_conflict_lifecycle() {
225
-
let client = client();
226
-
let (user_did, user_jwt) = setup_new_user("user-conflict").await;
227
-
228
-
let profile_payload = json!({
229
-
"repo": user_did,
230
-
"collection": "app.bsky.actor.profile",
231
-
"rkey": "self",
232
-
"record": {
233
-
"$type": "app.bsky.actor.profile",
234
-
"displayName": "Original Name"
235
-
}
236
-
});
237
-
let create_res = client
238
-
.post(format!(
239
-
"{}/xrpc/com.atproto.repo.putRecord",
240
-
base_url().await
241
-
))
242
-
.bearer_auth(&user_jwt)
243
-
.json(&profile_payload)
244
-
.send()
245
-
.await
246
-
.expect("create profile failed");
247
-
248
-
if create_res.status() != reqwest::StatusCode::OK {
249
-
return;
250
-
}
251
-
252
-
let get_res = client
253
-
.get(format!(
254
-
"{}/xrpc/com.atproto.repo.getRecord",
255
-
base_url().await
256
-
))
257
-
.query(&[
258
-
("repo", &user_did),
259
-
("collection", &"app.bsky.actor.profile".to_string()),
260
-
("rkey", &"self".to_string()),
261
-
])
262
-
.send()
263
-
.await
264
-
.expect("getRecord failed");
265
-
let get_body: Value = get_res.json().await.expect("getRecord not json");
266
-
let cid_v1 = get_body["cid"]
267
-
.as_str()
268
-
.expect("Profile v1 had no CID")
269
-
.to_string();
270
-
271
-
let update_payload_v2 = json!({
272
-
"repo": user_did,
273
-
"collection": "app.bsky.actor.profile",
274
-
"rkey": "self",
275
-
"record": {
276
-
"$type": "app.bsky.actor.profile",
277
-
"displayName": "Updated Name (v2)"
278
-
},
279
-
"swapRecord": cid_v1
280
-
});
281
-
let update_res_v2 = client
282
-
.post(format!(
283
-
"{}/xrpc/com.atproto.repo.putRecord",
284
-
base_url().await
285
-
))
286
-
.bearer_auth(&user_jwt)
287
-
.json(&update_payload_v2)
288
-
.send()
289
-
.await
290
-
.expect("putRecord v2 failed");
291
-
assert_eq!(
292
-
update_res_v2.status(),
293
-
reqwest::StatusCode::OK,
294
-
"v2 update failed"
295
-
);
296
-
let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json");
297
-
let cid_v2 = update_body_v2["cid"]
298
-
.as_str()
299
-
.expect("v2 response had no CID")
300
-
.to_string();
301
-
302
-
let update_payload_v3_stale = json!({
303
-
"repo": user_did,
304
-
"collection": "app.bsky.actor.profile",
305
-
"rkey": "self",
306
-
"record": {
307
-
"$type": "app.bsky.actor.profile",
308
-
"displayName": "Stale Update (v3)"
309
-
},
310
-
"swapRecord": cid_v1
311
-
});
312
-
let update_res_v3_stale = client
313
-
.post(format!(
314
-
"{}/xrpc/com.atproto.repo.putRecord",
315
-
base_url().await
316
-
))
317
-
.bearer_auth(&user_jwt)
318
-
.json(&update_payload_v3_stale)
319
-
.send()
320
-
.await
321
-
.expect("putRecord v3 (stale) failed");
322
-
323
-
assert_eq!(
324
-
update_res_v3_stale.status(),
325
-
reqwest::StatusCode::CONFLICT,
326
-
"Stale update did not cause a 409 Conflict"
327
-
);
328
-
329
-
let update_payload_v3_good = json!({
330
-
"repo": user_did,
331
-
"collection": "app.bsky.actor.profile",
332
-
"rkey": "self",
333
-
"record": {
334
-
"$type": "app.bsky.actor.profile",
335
-
"displayName": "Good Update (v3)"
336
-
},
337
-
"swapRecord": cid_v2
338
-
});
339
-
let update_res_v3_good = client
340
-
.post(format!(
341
-
"{}/xrpc/com.atproto.repo.putRecord",
342
-
base_url().await
343
-
))
344
-
.bearer_auth(&user_jwt)
345
-
.json(&update_payload_v3_good)
346
-
.send()
347
-
.await
348
-
.expect("putRecord v3 (good) failed");
349
-
350
-
assert_eq!(
351
-
update_res_v3_good.status(),
352
-
reqwest::StatusCode::OK,
353
-
"v3 (good) update failed"
354
-
);
355
-
}
356
-
357
-
async fn create_post(
358
-
client: &reqwest::Client,
359
-
did: &str,
360
-
jwt: &str,
361
-
text: &str,
362
-
) -> (String, String) {
363
-
let collection = "app.bsky.feed.post";
364
-
let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis());
365
-
let now = Utc::now().to_rfc3339();
366
-
367
-
let create_payload = json!({
368
-
"repo": did,
369
-
"collection": collection,
370
-
"rkey": rkey,
371
-
"record": {
372
-
"$type": collection,
373
-
"text": text,
374
-
"createdAt": now
375
-
}
376
-
});
377
-
378
-
let create_res = client
379
-
.post(format!(
380
-
"{}/xrpc/com.atproto.repo.putRecord",
381
-
base_url().await
382
-
))
383
-
.bearer_auth(jwt)
384
-
.json(&create_payload)
385
-
.send()
386
-
.await
387
-
.expect("Failed to send create post request");
388
-
389
-
assert_eq!(
390
-
create_res.status(),
391
-
reqwest::StatusCode::OK,
392
-
"Failed to create post record"
393
-
);
394
-
let create_body: Value = create_res
395
-
.json()
396
-
.await
397
-
.expect("create post response was not JSON");
398
-
let uri = create_body["uri"].as_str().unwrap().to_string();
399
-
let cid = create_body["cid"].as_str().unwrap().to_string();
400
-
(uri, cid)
401
-
}
402
-
403
-
async fn create_follow(
404
-
client: &reqwest::Client,
405
-
follower_did: &str,
406
-
follower_jwt: &str,
407
-
followee_did: &str,
408
-
) -> (String, String) {
409
-
let collection = "app.bsky.graph.follow";
410
-
let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis());
411
-
let now = Utc::now().to_rfc3339();
412
-
413
-
let create_payload = json!({
414
-
"repo": follower_did,
415
-
"collection": collection,
416
-
"rkey": rkey,
417
-
"record": {
418
-
"$type": collection,
419
-
"subject": followee_did,
420
-
"createdAt": now
421
-
}
422
-
});
423
-
424
-
let create_res = client
425
-
.post(format!(
426
-
"{}/xrpc/com.atproto.repo.putRecord",
427
-
base_url().await
428
-
))
429
-
.bearer_auth(follower_jwt)
430
-
.json(&create_payload)
431
-
.send()
432
-
.await
433
-
.expect("Failed to send create follow request");
434
-
435
-
assert_eq!(
436
-
create_res.status(),
437
-
reqwest::StatusCode::OK,
438
-
"Failed to create follow record"
439
-
);
440
-
let create_body: Value = create_res
441
-
.json()
442
-
.await
443
-
.expect("create follow response was not JSON");
444
-
let uri = create_body["uri"].as_str().unwrap().to_string();
445
-
let cid = create_body["cid"].as_str().unwrap().to_string();
446
-
(uri, cid)
447
-
}
448
-
449
-
#[tokio::test]
450
-
async fn test_social_flow_lifecycle() {
451
-
let client = client();
452
-
453
-
let (alice_did, alice_jwt) = setup_new_user("alice-social").await;
454
-
let (bob_did, bob_jwt) = setup_new_user("bob-social").await;
455
-
456
-
let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await;
457
-
458
-
create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
459
-
460
-
tokio::time::sleep(Duration::from_secs(1)).await;
461
-
462
-
let timeline_res_1 = client
463
-
.get(format!(
464
-
"{}/xrpc/app.bsky.feed.getTimeline",
465
-
base_url().await
466
-
))
467
-
.bearer_auth(&bob_jwt)
468
-
.send()
469
-
.await
470
-
.expect("Failed to get timeline (1)");
471
-
472
-
assert_eq!(
473
-
timeline_res_1.status(),
474
-
reqwest::StatusCode::OK,
475
-
"Failed to get timeline (1)"
476
-
);
477
-
let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON");
478
-
let feed_1 = timeline_body_1["feed"].as_array().unwrap();
479
-
assert_eq!(feed_1.len(), 1, "Timeline should have 1 post");
480
-
assert_eq!(
481
-
feed_1[0]["post"]["uri"], post1_uri,
482
-
"Post URI mismatch in timeline (1)"
483
-
);
484
-
485
-
let (post2_uri, _) = create_post(
486
-
&client,
487
-
&alice_did,
488
-
&alice_jwt,
489
-
"Alice's second post, so exciting!",
490
-
)
491
-
.await;
492
-
493
-
tokio::time::sleep(Duration::from_secs(1)).await;
494
-
495
-
let timeline_res_2 = client
496
-
.get(format!(
497
-
"{}/xrpc/app.bsky.feed.getTimeline",
498
-
base_url().await
499
-
))
500
-
.bearer_auth(&bob_jwt)
501
-
.send()
502
-
.await
503
-
.expect("Failed to get timeline (2)");
504
-
505
-
assert_eq!(
506
-
timeline_res_2.status(),
507
-
reqwest::StatusCode::OK,
508
-
"Failed to get timeline (2)"
509
-
);
510
-
let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON");
511
-
let feed_2 = timeline_body_2["feed"].as_array().unwrap();
512
-
assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts");
513
-
assert_eq!(
514
-
feed_2[0]["post"]["uri"], post2_uri,
515
-
"Post 2 should be first"
516
-
);
517
-
assert_eq!(
518
-
feed_2[1]["post"]["uri"], post1_uri,
519
-
"Post 1 should be second"
520
-
);
521
-
522
-
let delete_payload = json!({
523
-
"repo": alice_did,
524
-
"collection": "app.bsky.feed.post",
525
-
"rkey": post1_uri.split('/').last().unwrap()
526
-
});
527
-
let delete_res = client
528
-
.post(format!(
529
-
"{}/xrpc/com.atproto.repo.deleteRecord",
530
-
base_url().await
531
-
))
532
-
.bearer_auth(&alice_jwt)
533
-
.json(&delete_payload)
534
-
.send()
535
-
.await
536
-
.expect("Failed to send delete request");
537
-
assert_eq!(
538
-
delete_res.status(),
539
-
reqwest::StatusCode::OK,
540
-
"Failed to delete record"
541
-
);
542
-
543
-
tokio::time::sleep(Duration::from_secs(1)).await;
544
-
545
-
let timeline_res_3 = client
546
-
.get(format!(
547
-
"{}/xrpc/app.bsky.feed.getTimeline",
548
-
base_url().await
549
-
))
550
-
.bearer_auth(&bob_jwt)
551
-
.send()
552
-
.await
553
-
.expect("Failed to get timeline (3)");
554
-
555
-
assert_eq!(
556
-
timeline_res_3.status(),
557
-
reqwest::StatusCode::OK,
558
-
"Failed to get timeline (3)"
559
-
);
560
-
let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON");
561
-
let feed_3 = timeline_body_3["feed"].as_array().unwrap();
562
-
assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete");
563
-
assert_eq!(
564
-
feed_3[0]["post"]["uri"], post2_uri,
565
-
"Only post 2 should remain"
566
-
);
567
-
}
568
-
569
-
#[tokio::test]
570
-
async fn test_session_lifecycle_wrong_password() {
571
-
let client = client();
572
-
let (_, _) = setup_new_user("session-wrong-pw").await;
573
-
574
-
let login_payload = json!({
575
-
"identifier": format!("session-wrong-pw-{}.test", Utc::now().timestamp_millis()),
576
-
"password": "wrong-password"
577
-
});
578
-
579
-
let res = client
580
-
.post(format!(
581
-
"{}/xrpc/com.atproto.server.createSession",
582
-
base_url().await
583
-
))
584
-
.json(&login_payload)
585
-
.send()
586
-
.await
587
-
.expect("Failed to send request");
588
-
589
-
assert!(
590
-
res.status() == StatusCode::UNAUTHORIZED || res.status() == StatusCode::BAD_REQUEST,
591
-
"Expected 401 or 400 for wrong password, got {}",
592
-
res.status()
593
-
);
594
-
}
595
-
596
-
#[tokio::test]
597
-
async fn test_session_lifecycle_multiple_sessions() {
598
-
let client = client();
599
-
let ts = Utc::now().timestamp_millis();
600
-
let handle = format!("multi-session-{}.test", ts);
601
-
let email = format!("multi-session-{}@test.com", ts);
602
-
let password = "multi-session-pw";
603
-
604
-
let create_payload = json!({
605
-
"handle": handle,
606
-
"email": email,
607
-
"password": password
608
-
});
609
-
let create_res = client
610
-
.post(format!(
611
-
"{}/xrpc/com.atproto.server.createAccount",
612
-
base_url().await
613
-
))
614
-
.json(&create_payload)
615
-
.send()
616
-
.await
617
-
.expect("Failed to create account");
618
-
assert_eq!(create_res.status(), StatusCode::OK);
619
-
620
-
let login_payload = json!({
621
-
"identifier": handle,
622
-
"password": password
623
-
});
624
-
625
-
let session1_res = client
626
-
.post(format!(
627
-
"{}/xrpc/com.atproto.server.createSession",
628
-
base_url().await
629
-
))
630
-
.json(&login_payload)
631
-
.send()
632
-
.await
633
-
.expect("Failed session 1");
634
-
assert_eq!(session1_res.status(), StatusCode::OK);
635
-
let session1: Value = session1_res.json().await.unwrap();
636
-
let jwt1 = session1["accessJwt"].as_str().unwrap();
637
-
638
-
let session2_res = client
639
-
.post(format!(
640
-
"{}/xrpc/com.atproto.server.createSession",
641
-
base_url().await
642
-
))
643
-
.json(&login_payload)
644
-
.send()
645
-
.await
646
-
.expect("Failed session 2");
647
-
assert_eq!(session2_res.status(), StatusCode::OK);
648
-
let session2: Value = session2_res.json().await.unwrap();
649
-
let jwt2 = session2["accessJwt"].as_str().unwrap();
650
-
651
-
assert_ne!(jwt1, jwt2, "Sessions should have different tokens");
652
-
653
-
let get1 = client
654
-
.get(format!(
655
-
"{}/xrpc/com.atproto.server.getSession",
656
-
base_url().await
657
-
))
658
-
.bearer_auth(jwt1)
659
-
.send()
660
-
.await
661
-
.expect("Failed getSession 1");
662
-
assert_eq!(get1.status(), StatusCode::OK);
663
-
664
-
let get2 = client
665
-
.get(format!(
666
-
"{}/xrpc/com.atproto.server.getSession",
667
-
base_url().await
668
-
))
669
-
.bearer_auth(jwt2)
670
-
.send()
671
-
.await
672
-
.expect("Failed getSession 2");
673
-
assert_eq!(get2.status(), StatusCode::OK);
674
-
}
675
-
676
-
#[tokio::test]
677
-
async fn test_session_lifecycle_refresh_invalidates_old() {
678
-
let client = client();
679
-
let ts = Utc::now().timestamp_millis();
680
-
let handle = format!("refresh-inv-{}.test", ts);
681
-
let email = format!("refresh-inv-{}@test.com", ts);
682
-
let password = "refresh-inv-pw";
683
-
684
-
let create_payload = json!({
685
-
"handle": handle,
686
-
"email": email,
687
-
"password": password
688
-
});
689
-
client
690
-
.post(format!(
691
-
"{}/xrpc/com.atproto.server.createAccount",
692
-
base_url().await
693
-
))
694
-
.json(&create_payload)
695
-
.send()
696
-
.await
697
-
.expect("Failed to create account");
698
-
699
-
let login_payload = json!({
700
-
"identifier": handle,
701
-
"password": password
702
-
});
703
-
let login_res = client
704
-
.post(format!(
705
-
"{}/xrpc/com.atproto.server.createSession",
706
-
base_url().await
707
-
))
708
-
.json(&login_payload)
709
-
.send()
710
-
.await
711
-
.expect("Failed login");
712
-
let login_body: Value = login_res.json().await.unwrap();
713
-
let refresh_jwt = login_body["refreshJwt"].as_str().unwrap().to_string();
714
-
715
-
let refresh_res = client
716
-
.post(format!(
717
-
"{}/xrpc/com.atproto.server.refreshSession",
718
-
base_url().await
719
-
))
720
-
.bearer_auth(&refresh_jwt)
721
-
.send()
722
-
.await
723
-
.expect("Failed first refresh");
724
-
assert_eq!(refresh_res.status(), StatusCode::OK);
725
-
let refresh_body: Value = refresh_res.json().await.unwrap();
726
-
let new_refresh_jwt = refresh_body["refreshJwt"].as_str().unwrap();
727
-
728
-
assert_ne!(refresh_jwt, new_refresh_jwt, "Refresh tokens should differ");
729
-
730
-
let reuse_res = client
731
-
.post(format!(
732
-
"{}/xrpc/com.atproto.server.refreshSession",
733
-
base_url().await
734
-
))
735
-
.bearer_auth(&refresh_jwt)
736
-
.send()
737
-
.await
738
-
.expect("Failed reuse attempt");
739
-
740
-
assert!(
741
-
reuse_res.status() == StatusCode::UNAUTHORIZED || reuse_res.status() == StatusCode::BAD_REQUEST,
742
-
"Old refresh token should be invalid after use"
743
-
);
744
-
}
745
-
746
-
async fn create_like(
747
-
client: &reqwest::Client,
748
-
liker_did: &str,
749
-
liker_jwt: &str,
750
-
subject_uri: &str,
751
-
subject_cid: &str,
752
-
) -> (String, String) {
753
-
let collection = "app.bsky.feed.like";
754
-
let rkey = format!("e2e_like_{}", Utc::now().timestamp_millis());
755
-
let now = Utc::now().to_rfc3339();
756
-
757
-
let payload = json!({
758
-
"repo": liker_did,
759
-
"collection": collection,
760
-
"rkey": rkey,
761
-
"record": {
762
-
"$type": collection,
763
-
"subject": {
764
-
"uri": subject_uri,
765
-
"cid": subject_cid
766
-
},
767
-
"createdAt": now
768
-
}
769
-
});
770
-
771
-
let res = client
772
-
.post(format!(
773
-
"{}/xrpc/com.atproto.repo.putRecord",
774
-
base_url().await
775
-
))
776
-
.bearer_auth(liker_jwt)
777
-
.json(&payload)
778
-
.send()
779
-
.await
780
-
.expect("Failed to create like");
781
-
782
-
assert_eq!(res.status(), StatusCode::OK, "Failed to create like");
783
-
let body: Value = res.json().await.expect("Like response not JSON");
784
-
(
785
-
body["uri"].as_str().unwrap().to_string(),
786
-
body["cid"].as_str().unwrap().to_string(),
787
-
)
788
-
}
789
-
790
-
async fn create_repost(
791
-
client: &reqwest::Client,
792
-
reposter_did: &str,
793
-
reposter_jwt: &str,
794
-
subject_uri: &str,
795
-
subject_cid: &str,
796
-
) -> (String, String) {
797
-
let collection = "app.bsky.feed.repost";
798
-
let rkey = format!("e2e_repost_{}", Utc::now().timestamp_millis());
799
-
let now = Utc::now().to_rfc3339();
800
-
801
-
let payload = json!({
802
-
"repo": reposter_did,
803
-
"collection": collection,
804
-
"rkey": rkey,
805
-
"record": {
806
-
"$type": collection,
807
-
"subject": {
808
-
"uri": subject_uri,
809
-
"cid": subject_cid
810
-
},
811
-
"createdAt": now
812
-
}
813
-
});
814
-
815
-
let res = client
816
-
.post(format!(
817
-
"{}/xrpc/com.atproto.repo.putRecord",
818
-
base_url().await
819
-
))
820
-
.bearer_auth(reposter_jwt)
821
-
.json(&payload)
822
-
.send()
823
-
.await
824
-
.expect("Failed to create repost");
825
-
826
-
assert_eq!(res.status(), StatusCode::OK, "Failed to create repost");
827
-
let body: Value = res.json().await.expect("Repost response not JSON");
828
-
(
829
-
body["uri"].as_str().unwrap().to_string(),
830
-
body["cid"].as_str().unwrap().to_string(),
831
-
)
832
-
}
833
-
834
-
#[tokio::test]
835
-
async fn test_profile_lifecycle() {
836
-
let client = client();
837
-
let (did, jwt) = setup_new_user("profile-lifecycle").await;
838
-
839
-
let profile_payload = json!({
840
-
"repo": did,
841
-
"collection": "app.bsky.actor.profile",
842
-
"rkey": "self",
843
-
"record": {
844
-
"$type": "app.bsky.actor.profile",
845
-
"displayName": "Test User",
846
-
"description": "A test profile for lifecycle testing"
847
-
}
848
-
});
849
-
850
-
let create_res = client
851
-
.post(format!(
852
-
"{}/xrpc/com.atproto.repo.putRecord",
853
-
base_url().await
854
-
))
855
-
.bearer_auth(&jwt)
856
-
.json(&profile_payload)
857
-
.send()
858
-
.await
859
-
.expect("Failed to create profile");
860
-
861
-
assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile");
862
-
let create_body: Value = create_res.json().await.unwrap();
863
-
let initial_cid = create_body["cid"].as_str().unwrap().to_string();
864
-
865
-
let get_res = client
866
-
.get(format!(
867
-
"{}/xrpc/com.atproto.repo.getRecord",
868
-
base_url().await
869
-
))
870
-
.query(&[
871
-
("repo", did.as_str()),
872
-
("collection", "app.bsky.actor.profile"),
873
-
("rkey", "self"),
874
-
])
875
-
.send()
876
-
.await
877
-
.expect("Failed to get profile");
878
-
879
-
assert_eq!(get_res.status(), StatusCode::OK);
880
-
let get_body: Value = get_res.json().await.unwrap();
881
-
assert_eq!(get_body["value"]["displayName"], "Test User");
882
-
assert_eq!(get_body["value"]["description"], "A test profile for lifecycle testing");
883
-
884
-
let update_payload = json!({
885
-
"repo": did,
886
-
"collection": "app.bsky.actor.profile",
887
-
"rkey": "self",
888
-
"record": {
889
-
"$type": "app.bsky.actor.profile",
890
-
"displayName": "Updated User",
891
-
"description": "Profile has been updated"
892
-
},
893
-
"swapRecord": initial_cid
894
-
});
895
-
896
-
let update_res = client
897
-
.post(format!(
898
-
"{}/xrpc/com.atproto.repo.putRecord",
899
-
base_url().await
900
-
))
901
-
.bearer_auth(&jwt)
902
-
.json(&update_payload)
903
-
.send()
904
-
.await
905
-
.expect("Failed to update profile");
906
-
907
-
assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile");
908
-
909
-
let get_updated_res = client
910
-
.get(format!(
911
-
"{}/xrpc/com.atproto.repo.getRecord",
912
-
base_url().await
913
-
))
914
-
.query(&[
915
-
("repo", did.as_str()),
916
-
("collection", "app.bsky.actor.profile"),
917
-
("rkey", "self"),
918
-
])
919
-
.send()
920
-
.await
921
-
.expect("Failed to get updated profile");
922
-
923
-
let updated_body: Value = get_updated_res.json().await.unwrap();
924
-
assert_eq!(updated_body["value"]["displayName"], "Updated User");
925
-
}
926
-
927
-
#[tokio::test]
928
-
async fn test_reply_thread_lifecycle() {
929
-
let client = client();
930
-
931
-
let (alice_did, alice_jwt) = setup_new_user("alice-thread").await;
932
-
let (bob_did, bob_jwt) = setup_new_user("bob-thread").await;
933
-
934
-
let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await;
935
-
936
-
tokio::time::sleep(Duration::from_millis(100)).await;
937
-
938
-
let reply_collection = "app.bsky.feed.post";
939
-
let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis());
940
-
let now = Utc::now().to_rfc3339();
941
-
942
-
let reply_payload = json!({
943
-
"repo": bob_did,
944
-
"collection": reply_collection,
945
-
"rkey": reply_rkey,
946
-
"record": {
947
-
"$type": reply_collection,
948
-
"text": "This is Bob's reply to Alice",
949
-
"createdAt": now,
950
-
"reply": {
951
-
"root": {
952
-
"uri": root_uri,
953
-
"cid": root_cid
954
-
},
955
-
"parent": {
956
-
"uri": root_uri,
957
-
"cid": root_cid
958
-
}
959
-
}
960
-
}
961
-
});
962
-
963
-
let reply_res = client
964
-
.post(format!(
965
-
"{}/xrpc/com.atproto.repo.putRecord",
966
-
base_url().await
967
-
))
968
-
.bearer_auth(&bob_jwt)
969
-
.json(&reply_payload)
970
-
.send()
971
-
.await
972
-
.expect("Failed to create reply");
973
-
974
-
assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply");
975
-
let reply_body: Value = reply_res.json().await.unwrap();
976
-
let reply_uri = reply_body["uri"].as_str().unwrap();
977
-
let reply_cid = reply_body["cid"].as_str().unwrap();
978
-
979
-
let get_reply_res = client
980
-
.get(format!(
981
-
"{}/xrpc/com.atproto.repo.getRecord",
982
-
base_url().await
983
-
))
984
-
.query(&[
985
-
("repo", bob_did.as_str()),
986
-
("collection", reply_collection),
987
-
("rkey", reply_rkey.as_str()),
988
-
])
989
-
.send()
990
-
.await
991
-
.expect("Failed to get reply");
992
-
993
-
assert_eq!(get_reply_res.status(), StatusCode::OK);
994
-
let reply_record: Value = get_reply_res.json().await.unwrap();
995
-
assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri);
996
-
assert_eq!(reply_record["value"]["reply"]["parent"]["uri"], root_uri);
997
-
998
-
tokio::time::sleep(Duration::from_millis(100)).await;
999
-
1000
-
let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis());
1001
-
let nested_payload = json!({
1002
-
"repo": alice_did,
1003
-
"collection": reply_collection,
1004
-
"rkey": nested_reply_rkey,
1005
-
"record": {
1006
-
"$type": reply_collection,
1007
-
"text": "Alice replies to Bob's reply",
1008
-
"createdAt": Utc::now().to_rfc3339(),
1009
-
"reply": {
1010
-
"root": {
1011
-
"uri": root_uri,
1012
-
"cid": root_cid
1013
-
},
1014
-
"parent": {
1015
-
"uri": reply_uri,
1016
-
"cid": reply_cid
1017
-
}
1018
-
}
1019
-
}
1020
-
});
1021
-
1022
-
let nested_res = client
1023
-
.post(format!(
1024
-
"{}/xrpc/com.atproto.repo.putRecord",
1025
-
base_url().await
1026
-
))
1027
-
.bearer_auth(&alice_jwt)
1028
-
.json(&nested_payload)
1029
-
.send()
1030
-
.await
1031
-
.expect("Failed to create nested reply");
1032
-
1033
-
assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply");
1034
-
}
1035
-
1036
-
#[tokio::test]
1037
-
async fn test_like_lifecycle() {
1038
-
let client = client();
1039
-
1040
-
let (alice_did, alice_jwt) = setup_new_user("alice-like").await;
1041
-
let (bob_did, bob_jwt) = setup_new_user("bob-like").await;
1042
-
1043
-
let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Like this post!").await;
1044
-
1045
-
let (like_uri, _) = create_like(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
1046
-
1047
-
let like_rkey = like_uri.split('/').last().unwrap();
1048
-
let get_like_res = client
1049
-
.get(format!(
1050
-
"{}/xrpc/com.atproto.repo.getRecord",
1051
-
base_url().await
1052
-
))
1053
-
.query(&[
1054
-
("repo", bob_did.as_str()),
1055
-
("collection", "app.bsky.feed.like"),
1056
-
("rkey", like_rkey),
1057
-
])
1058
-
.send()
1059
-
.await
1060
-
.expect("Failed to get like");
1061
-
1062
-
assert_eq!(get_like_res.status(), StatusCode::OK);
1063
-
let like_body: Value = get_like_res.json().await.unwrap();
1064
-
assert_eq!(like_body["value"]["subject"]["uri"], post_uri);
1065
-
1066
-
let delete_payload = json!({
1067
-
"repo": bob_did,
1068
-
"collection": "app.bsky.feed.like",
1069
-
"rkey": like_rkey
1070
-
});
1071
-
1072
-
let delete_res = client
1073
-
.post(format!(
1074
-
"{}/xrpc/com.atproto.repo.deleteRecord",
1075
-
base_url().await
1076
-
))
1077
-
.bearer_auth(&bob_jwt)
1078
-
.json(&delete_payload)
1079
-
.send()
1080
-
.await
1081
-
.expect("Failed to delete like");
1082
-
1083
-
assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete like");
1084
-
1085
-
let get_deleted_res = client
1086
-
.get(format!(
1087
-
"{}/xrpc/com.atproto.repo.getRecord",
1088
-
base_url().await
1089
-
))
1090
-
.query(&[
1091
-
("repo", bob_did.as_str()),
1092
-
("collection", "app.bsky.feed.like"),
1093
-
("rkey", like_rkey),
1094
-
])
1095
-
.send()
1096
-
.await
1097
-
.expect("Failed to check deleted like");
1098
-
1099
-
assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Like should be deleted");
1100
-
}
1101
-
1102
-
#[tokio::test]
1103
-
async fn test_repost_lifecycle() {
1104
-
let client = client();
1105
-
1106
-
let (alice_did, alice_jwt) = setup_new_user("alice-repost").await;
1107
-
let (bob_did, bob_jwt) = setup_new_user("bob-repost").await;
1108
-
1109
-
let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Repost this!").await;
1110
-
1111
-
let (repost_uri, _) = create_repost(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
1112
-
1113
-
let repost_rkey = repost_uri.split('/').last().unwrap();
1114
-
let get_repost_res = client
1115
-
.get(format!(
1116
-
"{}/xrpc/com.atproto.repo.getRecord",
1117
-
base_url().await
1118
-
))
1119
-
.query(&[
1120
-
("repo", bob_did.as_str()),
1121
-
("collection", "app.bsky.feed.repost"),
1122
-
("rkey", repost_rkey),
1123
-
])
1124
-
.send()
1125
-
.await
1126
-
.expect("Failed to get repost");
1127
-
1128
-
assert_eq!(get_repost_res.status(), StatusCode::OK);
1129
-
let repost_body: Value = get_repost_res.json().await.unwrap();
1130
-
assert_eq!(repost_body["value"]["subject"]["uri"], post_uri);
1131
-
1132
-
let delete_payload = json!({
1133
-
"repo": bob_did,
1134
-
"collection": "app.bsky.feed.repost",
1135
-
"rkey": repost_rkey
1136
-
});
1137
-
1138
-
let delete_res = client
1139
-
.post(format!(
1140
-
"{}/xrpc/com.atproto.repo.deleteRecord",
1141
-
base_url().await
1142
-
))
1143
-
.bearer_auth(&bob_jwt)
1144
-
.json(&delete_payload)
1145
-
.send()
1146
-
.await
1147
-
.expect("Failed to delete repost");
1148
-
1149
-
assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete repost");
1150
-
}
1151
-
1152
-
#[tokio::test]
1153
-
async fn test_unfollow_lifecycle() {
1154
-
let client = client();
1155
-
1156
-
let (alice_did, _alice_jwt) = setup_new_user("alice-unfollow").await;
1157
-
let (bob_did, bob_jwt) = setup_new_user("bob-unfollow").await;
1158
-
1159
-
let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
1160
-
1161
-
let follow_rkey = follow_uri.split('/').last().unwrap();
1162
-
let get_follow_res = client
1163
-
.get(format!(
1164
-
"{}/xrpc/com.atproto.repo.getRecord",
1165
-
base_url().await
1166
-
))
1167
-
.query(&[
1168
-
("repo", bob_did.as_str()),
1169
-
("collection", "app.bsky.graph.follow"),
1170
-
("rkey", follow_rkey),
1171
-
])
1172
-
.send()
1173
-
.await
1174
-
.expect("Failed to get follow");
1175
-
1176
-
assert_eq!(get_follow_res.status(), StatusCode::OK);
1177
-
1178
-
let unfollow_payload = json!({
1179
-
"repo": bob_did,
1180
-
"collection": "app.bsky.graph.follow",
1181
-
"rkey": follow_rkey
1182
-
});
1183
-
1184
-
let unfollow_res = client
1185
-
.post(format!(
1186
-
"{}/xrpc/com.atproto.repo.deleteRecord",
1187
-
base_url().await
1188
-
))
1189
-
.bearer_auth(&bob_jwt)
1190
-
.json(&unfollow_payload)
1191
-
.send()
1192
-
.await
1193
-
.expect("Failed to unfollow");
1194
-
1195
-
assert_eq!(unfollow_res.status(), StatusCode::OK, "Failed to unfollow");
1196
-
1197
-
let get_deleted_res = client
1198
-
.get(format!(
1199
-
"{}/xrpc/com.atproto.repo.getRecord",
1200
-
base_url().await
1201
-
))
1202
-
.query(&[
1203
-
("repo", bob_did.as_str()),
1204
-
("collection", "app.bsky.graph.follow"),
1205
-
("rkey", follow_rkey),
1206
-
])
1207
-
.send()
1208
-
.await
1209
-
.expect("Failed to check deleted follow");
1210
-
1211
-
assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Follow should be deleted");
1212
-
}
1213
-
1214
-
#[tokio::test]
1215
-
async fn test_timeline_after_unfollow() {
1216
-
let client = client();
1217
-
1218
-
let (alice_did, alice_jwt) = setup_new_user("alice-tl-unfollow").await;
1219
-
let (bob_did, bob_jwt) = setup_new_user("bob-tl-unfollow").await;
1220
-
1221
-
let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
1222
-
1223
-
create_post(&client, &alice_did, &alice_jwt, "Post while following").await;
1224
-
1225
-
tokio::time::sleep(Duration::from_secs(1)).await;
1226
-
1227
-
let timeline_res = client
1228
-
.get(format!(
1229
-
"{}/xrpc/app.bsky.feed.getTimeline",
1230
-
base_url().await
1231
-
))
1232
-
.bearer_auth(&bob_jwt)
1233
-
.send()
1234
-
.await
1235
-
.expect("Failed to get timeline");
1236
-
1237
-
assert_eq!(timeline_res.status(), StatusCode::OK);
1238
-
let timeline_body: Value = timeline_res.json().await.unwrap();
1239
-
let feed = timeline_body["feed"].as_array().unwrap();
1240
-
assert_eq!(feed.len(), 1, "Should see 1 post from Alice");
1241
-
1242
-
let follow_rkey = follow_uri.split('/').last().unwrap();
1243
-
let unfollow_payload = json!({
1244
-
"repo": bob_did,
1245
-
"collection": "app.bsky.graph.follow",
1246
-
"rkey": follow_rkey
1247
-
});
1248
-
client
1249
-
.post(format!(
1250
-
"{}/xrpc/com.atproto.repo.deleteRecord",
1251
-
base_url().await
1252
-
))
1253
-
.bearer_auth(&bob_jwt)
1254
-
.json(&unfollow_payload)
1255
-
.send()
1256
-
.await
1257
-
.expect("Failed to unfollow");
1258
-
1259
-
tokio::time::sleep(Duration::from_secs(1)).await;
1260
-
1261
-
let timeline_after_res = client
1262
-
.get(format!(
1263
-
"{}/xrpc/app.bsky.feed.getTimeline",
1264
-
base_url().await
1265
-
))
1266
-
.bearer_auth(&bob_jwt)
1267
-
.send()
1268
-
.await
1269
-
.expect("Failed to get timeline after unfollow");
1270
-
1271
-
assert_eq!(timeline_after_res.status(), StatusCode::OK);
1272
-
let timeline_after: Value = timeline_after_res.json().await.unwrap();
1273
-
let feed_after = timeline_after["feed"].as_array().unwrap();
1274
-
assert_eq!(feed_after.len(), 0, "Should see 0 posts after unfollowing");
1275
-
}
1276
-
1277
-
#[tokio::test]
1278
-
async fn test_blob_in_record_lifecycle() {
1279
-
let client = client();
1280
-
let (did, jwt) = setup_new_user("blob-record").await;
1281
-
1282
-
let blob_data = b"This is test blob data for a profile avatar";
1283
-
let upload_res = client
1284
-
.post(format!(
1285
-
"{}/xrpc/com.atproto.repo.uploadBlob",
1286
-
base_url().await
1287
-
))
1288
-
.header(header::CONTENT_TYPE, "text/plain")
1289
-
.bearer_auth(&jwt)
1290
-
.body(blob_data.to_vec())
1291
-
.send()
1292
-
.await
1293
-
.expect("Failed to upload blob");
1294
-
1295
-
assert_eq!(upload_res.status(), StatusCode::OK);
1296
-
let upload_body: Value = upload_res.json().await.unwrap();
1297
-
let blob_ref = upload_body["blob"].clone();
1298
-
1299
-
let profile_payload = json!({
1300
-
"repo": did,
1301
-
"collection": "app.bsky.actor.profile",
1302
-
"rkey": "self",
1303
-
"record": {
1304
-
"$type": "app.bsky.actor.profile",
1305
-
"displayName": "User With Avatar",
1306
-
"avatar": blob_ref
1307
-
}
1308
-
});
1309
-
1310
-
let create_res = client
1311
-
.post(format!(
1312
-
"{}/xrpc/com.atproto.repo.putRecord",
1313
-
base_url().await
1314
-
))
1315
-
.bearer_auth(&jwt)
1316
-
.json(&profile_payload)
1317
-
.send()
1318
-
.await
1319
-
.expect("Failed to create profile with blob");
1320
-
1321
-
assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile with blob");
1322
-
1323
-
let get_res = client
1324
-
.get(format!(
1325
-
"{}/xrpc/com.atproto.repo.getRecord",
1326
-
base_url().await
1327
-
))
1328
-
.query(&[
1329
-
("repo", did.as_str()),
1330
-
("collection", "app.bsky.actor.profile"),
1331
-
("rkey", "self"),
1332
-
])
1333
-
.send()
1334
-
.await
1335
-
.expect("Failed to get profile");
1336
-
1337
-
assert_eq!(get_res.status(), StatusCode::OK);
1338
-
let profile: Value = get_res.json().await.unwrap();
1339
-
assert!(profile["value"]["avatar"]["ref"]["$link"].is_string());
1340
-
}
1341
-
1342
-
#[tokio::test]
1343
-
async fn test_authorization_cannot_modify_other_repo() {
1344
-
let client = client();
1345
-
1346
-
let (alice_did, _alice_jwt) = setup_new_user("alice-auth").await;
1347
-
let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await;
1348
-
1349
-
let post_payload = json!({
1350
-
"repo": alice_did,
1351
-
"collection": "app.bsky.feed.post",
1352
-
"rkey": "unauthorized-post",
1353
-
"record": {
1354
-
"$type": "app.bsky.feed.post",
1355
-
"text": "Bob trying to post as Alice",
1356
-
"createdAt": Utc::now().to_rfc3339()
1357
-
}
1358
-
});
1359
-
1360
-
let res = client
1361
-
.post(format!(
1362
-
"{}/xrpc/com.atproto.repo.putRecord",
1363
-
base_url().await
1364
-
))
1365
-
.bearer_auth(&bob_jwt)
1366
-
.json(&post_payload)
1367
-
.send()
1368
-
.await
1369
-
.expect("Failed to send request");
1370
-
1371
-
assert!(
1372
-
res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED,
1373
-
"Expected 403 or 401 when writing to another user's repo, got {}",
1374
-
res.status()
1375
-
);
1376
-
}
1377
-
1378
-
#[tokio::test]
1379
-
async fn test_authorization_cannot_delete_other_record() {
1380
-
let client = client();
1381
-
1382
-
let (alice_did, alice_jwt) = setup_new_user("alice-del-auth").await;
1383
-
let (_bob_did, bob_jwt) = setup_new_user("bob-del-auth").await;
1384
-
1385
-
let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await;
1386
-
let post_rkey = post_uri.split('/').last().unwrap();
1387
-
1388
-
let delete_payload = json!({
1389
-
"repo": alice_did,
1390
-
"collection": "app.bsky.feed.post",
1391
-
"rkey": post_rkey
1392
-
});
1393
-
1394
-
let res = client
1395
-
.post(format!(
1396
-
"{}/xrpc/com.atproto.repo.deleteRecord",
1397
-
base_url().await
1398
-
))
1399
-
.bearer_auth(&bob_jwt)
1400
-
.json(&delete_payload)
1401
-
.send()
1402
-
.await
1403
-
.expect("Failed to send request");
1404
-
1405
-
assert!(
1406
-
res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED,
1407
-
"Expected 403 or 401 when deleting another user's record, got {}",
1408
-
res.status()
1409
-
);
1410
-
1411
-
let get_res = client
1412
-
.get(format!(
1413
-
"{}/xrpc/com.atproto.repo.getRecord",
1414
-
base_url().await
1415
-
))
1416
-
.query(&[
1417
-
("repo", alice_did.as_str()),
1418
-
("collection", "app.bsky.feed.post"),
1419
-
("rkey", post_rkey),
1420
-
])
1421
-
.send()
1422
-
.await
1423
-
.expect("Failed to verify record exists");
1424
-
1425
-
assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist");
1426
-
}
1427
-
1428
-
#[tokio::test]
1429
-
async fn test_list_records_pagination() {
1430
-
let client = client();
1431
-
let (did, jwt) = setup_new_user("list-pagination").await;
1432
-
1433
-
for i in 0..5 {
1434
-
tokio::time::sleep(Duration::from_millis(50)).await;
1435
-
create_post(&client, &did, &jwt, &format!("Post number {}", i)).await;
1436
-
}
1437
-
1438
-
let list_res = client
1439
-
.get(format!(
1440
-
"{}/xrpc/com.atproto.repo.listRecords",
1441
-
base_url().await
1442
-
))
1443
-
.query(&[
1444
-
("repo", did.as_str()),
1445
-
("collection", "app.bsky.feed.post"),
1446
-
("limit", "2"),
1447
-
])
1448
-
.send()
1449
-
.await
1450
-
.expect("Failed to list records");
1451
-
1452
-
assert_eq!(list_res.status(), StatusCode::OK);
1453
-
let list_body: Value = list_res.json().await.unwrap();
1454
-
let records = list_body["records"].as_array().unwrap();
1455
-
assert_eq!(records.len(), 2, "Should return 2 records with limit=2");
1456
-
1457
-
if let Some(cursor) = list_body["cursor"].as_str() {
1458
-
let list_page2_res = client
1459
-
.get(format!(
1460
-
"{}/xrpc/com.atproto.repo.listRecords",
1461
-
base_url().await
1462
-
))
1463
-
.query(&[
1464
-
("repo", did.as_str()),
1465
-
("collection", "app.bsky.feed.post"),
1466
-
("limit", "2"),
1467
-
("cursor", cursor),
1468
-
])
1469
-
.send()
1470
-
.await
1471
-
.expect("Failed to list records page 2");
1472
-
1473
-
assert_eq!(list_page2_res.status(), StatusCode::OK);
1474
-
let page2_body: Value = list_page2_res.json().await.unwrap();
1475
-
let page2_records = page2_body["records"].as_array().unwrap();
1476
-
assert_eq!(page2_records.len(), 2, "Page 2 should have 2 more records");
1477
-
}
1478
-
}
1479
-
1480
-
#[tokio::test]
1481
-
async fn test_mutual_follow_lifecycle() {
1482
-
let client = client();
1483
-
1484
-
let (alice_did, alice_jwt) = setup_new_user("alice-mutual").await;
1485
-
let (bob_did, bob_jwt) = setup_new_user("bob-mutual").await;
1486
-
1487
-
create_follow(&client, &alice_did, &alice_jwt, &bob_did).await;
1488
-
create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
1489
-
1490
-
create_post(&client, &alice_did, &alice_jwt, "Alice's post for mutual").await;
1491
-
create_post(&client, &bob_did, &bob_jwt, "Bob's post for mutual").await;
1492
-
1493
-
tokio::time::sleep(Duration::from_secs(1)).await;
1494
-
1495
-
let alice_timeline_res = client
1496
-
.get(format!(
1497
-
"{}/xrpc/app.bsky.feed.getTimeline",
1498
-
base_url().await
1499
-
))
1500
-
.bearer_auth(&alice_jwt)
1501
-
.send()
1502
-
.await
1503
-
.expect("Failed to get Alice's timeline");
1504
-
1505
-
assert_eq!(alice_timeline_res.status(), StatusCode::OK);
1506
-
let alice_tl: Value = alice_timeline_res.json().await.unwrap();
1507
-
let alice_feed = alice_tl["feed"].as_array().unwrap();
1508
-
assert_eq!(alice_feed.len(), 1, "Alice should see Bob's 1 post");
1509
-
1510
-
let bob_timeline_res = client
1511
-
.get(format!(
1512
-
"{}/xrpc/app.bsky.feed.getTimeline",
1513
-
base_url().await
1514
-
))
1515
-
.bearer_auth(&bob_jwt)
1516
-
.send()
1517
-
.await
1518
-
.expect("Failed to get Bob's timeline");
1519
-
1520
-
assert_eq!(bob_timeline_res.status(), StatusCode::OK);
1521
-
let bob_tl: Value = bob_timeline_res.json().await.unwrap();
1522
-
let bob_feed = bob_tl["feed"].as_array().unwrap();
1523
-
assert_eq!(bob_feed.len(), 1, "Bob should see Alice's 1 post");
1524
-
}
1525
-
1526
-
#[tokio::test]
1527
-
async fn test_account_to_post_full_lifecycle() {
1528
-
let client = client();
1529
-
let ts = Utc::now().timestamp_millis();
1530
-
let handle = format!("fullcycle-{}.test", ts);
1531
-
let email = format!("fullcycle-{}@test.com", ts);
1532
-
let password = "fullcycle-password";
1533
-
1534
-
let create_account_res = client
1535
-
.post(format!(
1536
-
"{}/xrpc/com.atproto.server.createAccount",
1537
-
base_url().await
1538
-
))
1539
-
.json(&json!({
1540
-
"handle": handle,
1541
-
"email": email,
1542
-
"password": password
1543
-
}))
1544
-
.send()
1545
-
.await
1546
-
.expect("Failed to create account");
1547
-
1548
-
assert_eq!(create_account_res.status(), StatusCode::OK);
1549
-
let account_body: Value = create_account_res.json().await.unwrap();
1550
-
let did = account_body["did"].as_str().unwrap().to_string();
1551
-
let access_jwt = account_body["accessJwt"].as_str().unwrap().to_string();
1552
-
1553
-
let get_session_res = client
1554
-
.get(format!(
1555
-
"{}/xrpc/com.atproto.server.getSession",
1556
-
base_url().await
1557
-
))
1558
-
.bearer_auth(&access_jwt)
1559
-
.send()
1560
-
.await
1561
-
.expect("Failed to get session");
1562
-
1563
-
assert_eq!(get_session_res.status(), StatusCode::OK);
1564
-
let session_body: Value = get_session_res.json().await.unwrap();
1565
-
assert_eq!(session_body["did"], did);
1566
-
assert_eq!(session_body["handle"], handle);
1567
-
1568
-
let profile_res = client
1569
-
.post(format!(
1570
-
"{}/xrpc/com.atproto.repo.putRecord",
1571
-
base_url().await
1572
-
))
1573
-
.bearer_auth(&access_jwt)
1574
-
.json(&json!({
1575
-
"repo": did,
1576
-
"collection": "app.bsky.actor.profile",
1577
-
"rkey": "self",
1578
-
"record": {
1579
-
"$type": "app.bsky.actor.profile",
1580
-
"displayName": "Full Cycle User"
1581
-
}
1582
-
}))
1583
-
.send()
1584
-
.await
1585
-
.expect("Failed to create profile");
1586
-
1587
-
assert_eq!(profile_res.status(), StatusCode::OK);
1588
-
1589
-
let (post_uri, post_cid) = create_post(&client, &did, &access_jwt, "My first post!").await;
1590
-
1591
-
let get_post_res = client
1592
-
.get(format!(
1593
-
"{}/xrpc/com.atproto.repo.getRecord",
1594
-
base_url().await
1595
-
))
1596
-
.query(&[
1597
-
("repo", did.as_str()),
1598
-
("collection", "app.bsky.feed.post"),
1599
-
("rkey", post_uri.split('/').last().unwrap()),
1600
-
])
1601
-
.send()
1602
-
.await
1603
-
.expect("Failed to get post");
1604
-
1605
-
assert_eq!(get_post_res.status(), StatusCode::OK);
1606
-
1607
-
create_like(&client, &did, &access_jwt, &post_uri, &post_cid).await;
1608
-
1609
-
let describe_res = client
1610
-
.get(format!(
1611
-
"{}/xrpc/com.atproto.repo.describeRepo",
1612
-
base_url().await
1613
-
))
1614
-
.query(&[("repo", did.as_str())])
1615
-
.send()
1616
-
.await
1617
-
.expect("Failed to describe repo");
1618
-
1619
-
assert_eq!(describe_res.status(), StatusCode::OK);
1620
-
let describe_body: Value = describe_res.json().await.unwrap();
1621
-
assert_eq!(describe_body["did"], did);
1622
-
assert_eq!(describe_body["handle"], handle);
1623
-
}
1624
-
1625
-
#[tokio::test]
1626
-
async fn test_app_password_lifecycle() {
1627
-
let client = client();
1628
-
let ts = Utc::now().timestamp_millis();
1629
-
let handle = format!("apppass-{}.test", ts);
1630
-
let email = format!("apppass-{}@test.com", ts);
1631
-
let password = "apppass-password";
1632
-
1633
-
let create_res = client
1634
-
.post(format!(
1635
-
"{}/xrpc/com.atproto.server.createAccount",
1636
-
base_url().await
1637
-
))
1638
-
.json(&json!({
1639
-
"handle": handle,
1640
-
"email": email,
1641
-
"password": password
1642
-
}))
1643
-
.send()
1644
-
.await
1645
-
.expect("Failed to create account");
1646
-
1647
-
assert_eq!(create_res.status(), StatusCode::OK);
1648
-
let account: Value = create_res.json().await.unwrap();
1649
-
let jwt = account["accessJwt"].as_str().unwrap();
1650
-
1651
-
let create_app_pass_res = client
1652
-
.post(format!(
1653
-
"{}/xrpc/com.atproto.server.createAppPassword",
1654
-
base_url().await
1655
-
))
1656
-
.bearer_auth(jwt)
1657
-
.json(&json!({ "name": "Test App" }))
1658
-
.send()
1659
-
.await
1660
-
.expect("Failed to create app password");
1661
-
1662
-
assert_eq!(create_app_pass_res.status(), StatusCode::OK);
1663
-
let app_pass: Value = create_app_pass_res.json().await.unwrap();
1664
-
let app_password = app_pass["password"].as_str().unwrap().to_string();
1665
-
assert_eq!(app_pass["name"], "Test App");
1666
-
1667
-
let list_res = client
1668
-
.get(format!(
1669
-
"{}/xrpc/com.atproto.server.listAppPasswords",
1670
-
base_url().await
1671
-
))
1672
-
.bearer_auth(jwt)
1673
-
.send()
1674
-
.await
1675
-
.expect("Failed to list app passwords");
1676
-
1677
-
assert_eq!(list_res.status(), StatusCode::OK);
1678
-
let list_body: Value = list_res.json().await.unwrap();
1679
-
let passwords = list_body["passwords"].as_array().unwrap();
1680
-
assert_eq!(passwords.len(), 1);
1681
-
assert_eq!(passwords[0]["name"], "Test App");
1682
-
1683
-
let login_res = client
1684
-
.post(format!(
1685
-
"{}/xrpc/com.atproto.server.createSession",
1686
-
base_url().await
1687
-
))
1688
-
.json(&json!({
1689
-
"identifier": handle,
1690
-
"password": app_password
1691
-
}))
1692
-
.send()
1693
-
.await
1694
-
.expect("Failed to login with app password");
1695
-
1696
-
assert_eq!(login_res.status(), StatusCode::OK, "App password login should work");
1697
-
1698
-
let revoke_res = client
1699
-
.post(format!(
1700
-
"{}/xrpc/com.atproto.server.revokeAppPassword",
1701
-
base_url().await
1702
-
))
1703
-
.bearer_auth(jwt)
1704
-
.json(&json!({ "name": "Test App" }))
1705
-
.send()
1706
-
.await
1707
-
.expect("Failed to revoke app password");
1708
-
1709
-
assert_eq!(revoke_res.status(), StatusCode::OK);
1710
-
1711
-
let login_after_revoke = client
1712
-
.post(format!(
1713
-
"{}/xrpc/com.atproto.server.createSession",
1714
-
base_url().await
1715
-
))
1716
-
.json(&json!({
1717
-
"identifier": handle,
1718
-
"password": app_password
1719
-
}))
1720
-
.send()
1721
-
.await
1722
-
.expect("Failed to attempt login after revoke");
1723
-
1724
-
assert!(
1725
-
login_after_revoke.status() == StatusCode::UNAUTHORIZED
1726
-
|| login_after_revoke.status() == StatusCode::BAD_REQUEST,
1727
-
"Revoked app password should not work"
1728
-
);
1729
-
1730
-
let list_after_revoke = client
1731
-
.get(format!(
1732
-
"{}/xrpc/com.atproto.server.listAppPasswords",
1733
-
base_url().await
1734
-
))
1735
-
.bearer_auth(jwt)
1736
-
.send()
1737
-
.await
1738
-
.expect("Failed to list after revoke");
1739
-
1740
-
let list_after: Value = list_after_revoke.json().await.unwrap();
1741
-
let passwords_after = list_after["passwords"].as_array().unwrap();
1742
-
assert_eq!(passwords_after.len(), 0, "No app passwords should remain");
1743
-
}
1744
-
1745
-
#[tokio::test]
1746
-
async fn test_account_deactivation_lifecycle() {
1747
-
let client = client();
1748
-
let ts = Utc::now().timestamp_millis();
1749
-
let handle = format!("deactivate-{}.test", ts);
1750
-
let email = format!("deactivate-{}@test.com", ts);
1751
-
let password = "deactivate-password";
1752
-
1753
-
let create_res = client
1754
-
.post(format!(
1755
-
"{}/xrpc/com.atproto.server.createAccount",
1756
-
base_url().await
1757
-
))
1758
-
.json(&json!({
1759
-
"handle": handle,
1760
-
"email": email,
1761
-
"password": password
1762
-
}))
1763
-
.send()
1764
-
.await
1765
-
.expect("Failed to create account");
1766
-
1767
-
assert_eq!(create_res.status(), StatusCode::OK);
1768
-
let account: Value = create_res.json().await.unwrap();
1769
-
let did = account["did"].as_str().unwrap().to_string();
1770
-
let jwt = account["accessJwt"].as_str().unwrap().to_string();
1771
-
1772
-
let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await;
1773
-
let post_rkey = post_uri.split('/').last().unwrap();
1774
-
1775
-
let status_before = client
1776
-
.get(format!(
1777
-
"{}/xrpc/com.atproto.server.checkAccountStatus",
1778
-
base_url().await
1779
-
))
1780
-
.bearer_auth(&jwt)
1781
-
.send()
1782
-
.await
1783
-
.expect("Failed to check status");
1784
-
1785
-
assert_eq!(status_before.status(), StatusCode::OK);
1786
-
let status_body: Value = status_before.json().await.unwrap();
1787
-
assert_eq!(status_body["activated"], true);
1788
-
1789
-
let deactivate_res = client
1790
-
.post(format!(
1791
-
"{}/xrpc/com.atproto.server.deactivateAccount",
1792
-
base_url().await
1793
-
))
1794
-
.bearer_auth(&jwt)
1795
-
.json(&json!({}))
1796
-
.send()
1797
-
.await
1798
-
.expect("Failed to deactivate");
1799
-
1800
-
assert_eq!(deactivate_res.status(), StatusCode::OK);
1801
-
1802
-
let get_post_res = client
1803
-
.get(format!(
1804
-
"{}/xrpc/com.atproto.repo.getRecord",
1805
-
base_url().await
1806
-
))
1807
-
.query(&[
1808
-
("repo", did.as_str()),
1809
-
("collection", "app.bsky.feed.post"),
1810
-
("rkey", post_rkey),
1811
-
])
1812
-
.send()
1813
-
.await
1814
-
.expect("Failed to get post while deactivated");
1815
-
1816
-
assert_eq!(get_post_res.status(), StatusCode::OK, "Records should still be readable");
1817
-
1818
-
let activate_res = client
1819
-
.post(format!(
1820
-
"{}/xrpc/com.atproto.server.activateAccount",
1821
-
base_url().await
1822
-
))
1823
-
.bearer_auth(&jwt)
1824
-
.json(&json!({}))
1825
-
.send()
1826
-
.await
1827
-
.expect("Failed to reactivate");
1828
-
1829
-
assert_eq!(activate_res.status(), StatusCode::OK);
1830
-
1831
-
let status_after_activate = client
1832
-
.get(format!(
1833
-
"{}/xrpc/com.atproto.server.checkAccountStatus",
1834
-
base_url().await
1835
-
))
1836
-
.bearer_auth(&jwt)
1837
-
.send()
1838
-
.await
1839
-
.expect("Failed to check status after activate");
1840
-
1841
-
assert_eq!(status_after_activate.status(), StatusCode::OK);
1842
-
1843
-
let (new_post_uri, _) = create_post(&client, &did, &jwt, "Post after reactivation").await;
1844
-
assert!(!new_post_uri.is_empty(), "Should be able to post after reactivation");
1845
-
}
1846
-
1847
-
#[tokio::test]
1848
-
async fn test_sync_record_lifecycle() {
1849
-
let client = client();
1850
-
let (did, jwt) = setup_new_user("sync-record-lifecycle").await;
1851
-
1852
-
let (post_uri, _post_cid) =
1853
-
create_post(&client, &did, &jwt, "Post for sync record test").await;
1854
-
let post_rkey = post_uri.split('/').last().unwrap();
1855
-
1856
-
let sync_record_res = client
1857
-
.get(format!(
1858
-
"{}/xrpc/com.atproto.sync.getRecord",
1859
-
base_url().await
1860
-
))
1861
-
.query(&[
1862
-
("did", did.as_str()),
1863
-
("collection", "app.bsky.feed.post"),
1864
-
("rkey", post_rkey),
1865
-
])
1866
-
.send()
1867
-
.await
1868
-
.expect("Failed to get sync record");
1869
-
1870
-
assert_eq!(sync_record_res.status(), StatusCode::OK);
1871
-
assert_eq!(
1872
-
sync_record_res
1873
-
.headers()
1874
-
.get("content-type")
1875
-
.and_then(|h| h.to_str().ok()),
1876
-
Some("application/vnd.ipld.car")
1877
-
);
1878
-
let car_bytes = sync_record_res.bytes().await.unwrap();
1879
-
assert!(!car_bytes.is_empty(), "CAR data should not be empty");
1880
-
1881
-
let latest_before = client
1882
-
.get(format!(
1883
-
"{}/xrpc/com.atproto.sync.getLatestCommit",
1884
-
base_url().await
1885
-
))
1886
-
.query(&[("did", did.as_str())])
1887
-
.send()
1888
-
.await
1889
-
.expect("Failed to get latest commit");
1890
-
let latest_before_body: Value = latest_before.json().await.unwrap();
1891
-
let rev_before = latest_before_body["rev"].as_str().unwrap().to_string();
1892
-
1893
-
let (post2_uri, _) = create_post(&client, &did, &jwt, "Second post for sync test").await;
1894
-
1895
-
let latest_after = client
1896
-
.get(format!(
1897
-
"{}/xrpc/com.atproto.sync.getLatestCommit",
1898
-
base_url().await
1899
-
))
1900
-
.query(&[("did", did.as_str())])
1901
-
.send()
1902
-
.await
1903
-
.expect("Failed to get latest commit after");
1904
-
let latest_after_body: Value = latest_after.json().await.unwrap();
1905
-
let rev_after = latest_after_body["rev"].as_str().unwrap().to_string();
1906
-
assert_ne!(rev_before, rev_after, "Revision should change after new record");
1907
-
1908
-
let delete_payload = json!({
1909
-
"repo": did,
1910
-
"collection": "app.bsky.feed.post",
1911
-
"rkey": post_rkey
1912
-
});
1913
-
let delete_res = client
1914
-
.post(format!(
1915
-
"{}/xrpc/com.atproto.repo.deleteRecord",
1916
-
base_url().await
1917
-
))
1918
-
.bearer_auth(&jwt)
1919
-
.json(&delete_payload)
1920
-
.send()
1921
-
.await
1922
-
.expect("Failed to delete record");
1923
-
assert_eq!(delete_res.status(), StatusCode::OK);
1924
-
1925
-
let sync_deleted_res = client
1926
-
.get(format!(
1927
-
"{}/xrpc/com.atproto.sync.getRecord",
1928
-
base_url().await
1929
-
))
1930
-
.query(&[
1931
-
("did", did.as_str()),
1932
-
("collection", "app.bsky.feed.post"),
1933
-
("rkey", post_rkey),
1934
-
])
1935
-
.send()
1936
-
.await
1937
-
.expect("Failed to check deleted record via sync");
1938
-
assert_eq!(
1939
-
sync_deleted_res.status(),
1940
-
StatusCode::NOT_FOUND,
1941
-
"Deleted record should return 404 via sync.getRecord"
1942
-
);
1943
-
1944
-
let post2_rkey = post2_uri.split('/').last().unwrap();
1945
-
let sync_post2_res = client
1946
-
.get(format!(
1947
-
"{}/xrpc/com.atproto.sync.getRecord",
1948
-
base_url().await
1949
-
))
1950
-
.query(&[
1951
-
("did", did.as_str()),
1952
-
("collection", "app.bsky.feed.post"),
1953
-
("rkey", post2_rkey),
1954
-
])
1955
-
.send()
1956
-
.await
1957
-
.expect("Failed to get second post via sync");
1958
-
assert_eq!(
1959
-
sync_post2_res.status(),
1960
-
StatusCode::OK,
1961
-
"Second post should still be accessible"
1962
-
);
1963
-
}
1964
-
1965
-
#[tokio::test]
1966
-
async fn test_sync_repo_export_lifecycle() {
1967
-
let client = client();
1968
-
let (did, jwt) = setup_new_user("sync-repo-export").await;
1969
-
1970
-
let profile_payload = json!({
1971
-
"repo": did,
1972
-
"collection": "app.bsky.actor.profile",
1973
-
"rkey": "self",
1974
-
"record": {
1975
-
"$type": "app.bsky.actor.profile",
1976
-
"displayName": "Sync Export User"
1977
-
}
1978
-
});
1979
-
let profile_res = client
1980
-
.post(format!(
1981
-
"{}/xrpc/com.atproto.repo.putRecord",
1982
-
base_url().await
1983
-
))
1984
-
.bearer_auth(&jwt)
1985
-
.json(&profile_payload)
1986
-
.send()
1987
-
.await
1988
-
.expect("Failed to create profile");
1989
-
assert_eq!(profile_res.status(), StatusCode::OK);
1990
-
1991
-
for i in 0..3 {
1992
-
tokio::time::sleep(Duration::from_millis(50)).await;
1993
-
create_post(&client, &did, &jwt, &format!("Export test post {}", i)).await;
1994
-
}
1995
-
1996
-
let blob_data = b"blob data for sync export test";
1997
-
let upload_res = client
1998
-
.post(format!(
1999
-
"{}/xrpc/com.atproto.repo.uploadBlob",
2000
-
base_url().await
2001
-
))
2002
-
.header(header::CONTENT_TYPE, "application/octet-stream")
2003
-
.bearer_auth(&jwt)
2004
-
.body(blob_data.to_vec())
2005
-
.send()
2006
-
.await
2007
-
.expect("Failed to upload blob");
2008
-
assert_eq!(upload_res.status(), StatusCode::OK);
2009
-
let blob_body: Value = upload_res.json().await.unwrap();
2010
-
let blob_cid = blob_body["blob"]["ref"]["$link"].as_str().unwrap().to_string();
2011
-
2012
-
let repo_status_res = client
2013
-
.get(format!(
2014
-
"{}/xrpc/com.atproto.sync.getRepoStatus",
2015
-
base_url().await
2016
-
))
2017
-
.query(&[("did", did.as_str())])
2018
-
.send()
2019
-
.await
2020
-
.expect("Failed to get repo status");
2021
-
assert_eq!(repo_status_res.status(), StatusCode::OK);
2022
-
let status_body: Value = repo_status_res.json().await.unwrap();
2023
-
assert_eq!(status_body["did"], did);
2024
-
assert_eq!(status_body["active"], true);
2025
-
2026
-
let get_repo_res = client
2027
-
.get(format!(
2028
-
"{}/xrpc/com.atproto.sync.getRepo",
2029
-
base_url().await
2030
-
))
2031
-
.query(&[("did", did.as_str())])
2032
-
.send()
2033
-
.await
2034
-
.expect("Failed to get full repo");
2035
-
assert_eq!(get_repo_res.status(), StatusCode::OK);
2036
-
assert_eq!(
2037
-
get_repo_res
2038
-
.headers()
2039
-
.get("content-type")
2040
-
.and_then(|h| h.to_str().ok()),
2041
-
Some("application/vnd.ipld.car")
2042
-
);
2043
-
let repo_car = get_repo_res.bytes().await.unwrap();
2044
-
assert!(repo_car.len() > 100, "Repo CAR should have substantial data");
2045
-
2046
-
let list_blobs_res = client
2047
-
.get(format!(
2048
-
"{}/xrpc/com.atproto.sync.listBlobs",
2049
-
base_url().await
2050
-
))
2051
-
.query(&[("did", did.as_str())])
2052
-
.send()
2053
-
.await
2054
-
.expect("Failed to list blobs");
2055
-
assert_eq!(list_blobs_res.status(), StatusCode::OK);
2056
-
let blobs_body: Value = list_blobs_res.json().await.unwrap();
2057
-
let cids = blobs_body["cids"].as_array().unwrap();
2058
-
assert!(!cids.is_empty(), "Should have at least one blob");
2059
-
2060
-
let get_blob_res = client
2061
-
.get(format!(
2062
-
"{}/xrpc/com.atproto.sync.getBlob",
2063
-
base_url().await
2064
-
))
2065
-
.query(&[("did", did.as_str()), ("cid", &blob_cid)])
2066
-
.send()
2067
-
.await
2068
-
.expect("Failed to get blob");
2069
-
assert_eq!(get_blob_res.status(), StatusCode::OK);
2070
-
let retrieved_blob = get_blob_res.bytes().await.unwrap();
2071
-
assert_eq!(
2072
-
retrieved_blob.as_ref(),
2073
-
blob_data,
2074
-
"Retrieved blob should match uploaded data"
2075
-
);
2076
-
2077
-
let latest_commit_res = client
2078
-
.get(format!(
2079
-
"{}/xrpc/com.atproto.sync.getLatestCommit",
2080
-
base_url().await
2081
-
))
2082
-
.query(&[("did", did.as_str())])
2083
-
.send()
2084
-
.await
2085
-
.expect("Failed to get latest commit");
2086
-
assert_eq!(latest_commit_res.status(), StatusCode::OK);
2087
-
let commit_body: Value = latest_commit_res.json().await.unwrap();
2088
-
let root_cid = commit_body["cid"].as_str().unwrap();
2089
-
2090
-
let get_blocks_url = format!(
2091
-
"{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}",
2092
-
base_url().await,
2093
-
did,
2094
-
root_cid
2095
-
);
2096
-
let get_blocks_res = client
2097
-
.get(&get_blocks_url)
2098
-
.send()
2099
-
.await
2100
-
.expect("Failed to get blocks");
2101
-
assert_eq!(get_blocks_res.status(), StatusCode::OK);
2102
-
assert_eq!(
2103
-
get_blocks_res
2104
-
.headers()
2105
-
.get("content-type")
2106
-
.and_then(|h| h.to_str().ok()),
2107
-
Some("application/vnd.ipld.car")
2108
-
);
2109
-
}
2110
-
2111
-
#[tokio::test]
2112
-
async fn test_apply_writes_batch_lifecycle() {
2113
-
let client = client();
2114
-
let (did, jwt) = setup_new_user("apply-writes-batch").await;
2115
-
2116
-
let now = Utc::now().to_rfc3339();
2117
-
let writes_payload = json!({
2118
-
"repo": did,
2119
-
"writes": [
2120
-
{
2121
-
"$type": "com.atproto.repo.applyWrites#create",
2122
-
"collection": "app.bsky.feed.post",
2123
-
"rkey": "batch-post-1",
2124
-
"value": {
2125
-
"$type": "app.bsky.feed.post",
2126
-
"text": "First batch post",
2127
-
"createdAt": now
2128
-
}
2129
-
},
2130
-
{
2131
-
"$type": "com.atproto.repo.applyWrites#create",
2132
-
"collection": "app.bsky.feed.post",
2133
-
"rkey": "batch-post-2",
2134
-
"value": {
2135
-
"$type": "app.bsky.feed.post",
2136
-
"text": "Second batch post",
2137
-
"createdAt": now
2138
-
}
2139
-
},
2140
-
{
2141
-
"$type": "com.atproto.repo.applyWrites#create",
2142
-
"collection": "app.bsky.actor.profile",
2143
-
"rkey": "self",
2144
-
"value": {
2145
-
"$type": "app.bsky.actor.profile",
2146
-
"displayName": "Batch User"
2147
-
}
2148
-
}
2149
-
]
2150
-
});
2151
-
2152
-
let apply_res = client
2153
-
.post(format!(
2154
-
"{}/xrpc/com.atproto.repo.applyWrites",
2155
-
base_url().await
2156
-
))
2157
-
.bearer_auth(&jwt)
2158
-
.json(&writes_payload)
2159
-
.send()
2160
-
.await
2161
-
.expect("Failed to apply writes");
2162
-
2163
-
assert_eq!(apply_res.status(), StatusCode::OK);
2164
-
2165
-
let get_post1 = client
2166
-
.get(format!(
2167
-
"{}/xrpc/com.atproto.repo.getRecord",
2168
-
base_url().await
2169
-
))
2170
-
.query(&[
2171
-
("repo", did.as_str()),
2172
-
("collection", "app.bsky.feed.post"),
2173
-
("rkey", "batch-post-1"),
2174
-
])
2175
-
.send()
2176
-
.await
2177
-
.expect("Failed to get post 1");
2178
-
assert_eq!(get_post1.status(), StatusCode::OK);
2179
-
let post1_body: Value = get_post1.json().await.unwrap();
2180
-
assert_eq!(post1_body["value"]["text"], "First batch post");
2181
-
2182
-
let get_post2 = client
2183
-
.get(format!(
2184
-
"{}/xrpc/com.atproto.repo.getRecord",
2185
-
base_url().await
2186
-
))
2187
-
.query(&[
2188
-
("repo", did.as_str()),
2189
-
("collection", "app.bsky.feed.post"),
2190
-
("rkey", "batch-post-2"),
2191
-
])
2192
-
.send()
2193
-
.await
2194
-
.expect("Failed to get post 2");
2195
-
assert_eq!(get_post2.status(), StatusCode::OK);
2196
-
2197
-
let get_profile = client
2198
-
.get(format!(
2199
-
"{}/xrpc/com.atproto.repo.getRecord",
2200
-
base_url().await
2201
-
))
2202
-
.query(&[
2203
-
("repo", did.as_str()),
2204
-
("collection", "app.bsky.actor.profile"),
2205
-
("rkey", "self"),
2206
-
])
2207
-
.send()
2208
-
.await
2209
-
.expect("Failed to get profile");
2210
-
assert_eq!(get_profile.status(), StatusCode::OK);
2211
-
let profile_body: Value = get_profile.json().await.unwrap();
2212
-
assert_eq!(profile_body["value"]["displayName"], "Batch User");
2213
-
2214
-
let update_writes = json!({
2215
-
"repo": did,
2216
-
"writes": [
2217
-
{
2218
-
"$type": "com.atproto.repo.applyWrites#update",
2219
-
"collection": "app.bsky.actor.profile",
2220
-
"rkey": "self",
2221
-
"value": {
2222
-
"$type": "app.bsky.actor.profile",
2223
-
"displayName": "Updated Batch User"
2224
-
}
2225
-
},
2226
-
{
2227
-
"$type": "com.atproto.repo.applyWrites#delete",
2228
-
"collection": "app.bsky.feed.post",
2229
-
"rkey": "batch-post-1"
2230
-
}
2231
-
]
2232
-
});
2233
-
2234
-
let update_res = client
2235
-
.post(format!(
2236
-
"{}/xrpc/com.atproto.repo.applyWrites",
2237
-
base_url().await
2238
-
))
2239
-
.bearer_auth(&jwt)
2240
-
.json(&update_writes)
2241
-
.send()
2242
-
.await
2243
-
.expect("Failed to apply update writes");
2244
-
assert_eq!(update_res.status(), StatusCode::OK);
2245
-
2246
-
let get_updated_profile = client
2247
-
.get(format!(
2248
-
"{}/xrpc/com.atproto.repo.getRecord",
2249
-
base_url().await
2250
-
))
2251
-
.query(&[
2252
-
("repo", did.as_str()),
2253
-
("collection", "app.bsky.actor.profile"),
2254
-
("rkey", "self"),
2255
-
])
2256
-
.send()
2257
-
.await
2258
-
.expect("Failed to get updated profile");
2259
-
let updated_profile: Value = get_updated_profile.json().await.unwrap();
2260
-
assert_eq!(updated_profile["value"]["displayName"], "Updated Batch User");
2261
-
2262
-
let get_deleted_post = client
2263
-
.get(format!(
2264
-
"{}/xrpc/com.atproto.repo.getRecord",
2265
-
base_url().await
2266
-
))
2267
-
.query(&[
2268
-
("repo", did.as_str()),
2269
-
("collection", "app.bsky.feed.post"),
2270
-
("rkey", "batch-post-1"),
2271
-
])
2272
-
.send()
2273
-
.await
2274
-
.expect("Failed to check deleted post");
2275
-
assert_eq!(
2276
-
get_deleted_post.status(),
2277
-
StatusCode::NOT_FOUND,
2278
-
"Batch-deleted post should be gone"
2279
-
);
2280
-
}
2281
-
2282
-
#[tokio::test]
2283
-
async fn test_resolve_handle_lifecycle() {
2284
-
let client = client();
2285
-
let ts = Utc::now().timestamp_millis();
2286
-
let handle = format!("resolve-test-{}.test", ts);
2287
-
let email = format!("resolve-test-{}@test.com", ts);
2288
-
2289
-
let create_res = client
2290
-
.post(format!(
2291
-
"{}/xrpc/com.atproto.server.createAccount",
2292
-
base_url().await
2293
-
))
2294
-
.json(&json!({
2295
-
"handle": handle,
2296
-
"email": email,
2297
-
"password": "resolve-test-pw"
2298
-
}))
2299
-
.send()
2300
-
.await
2301
-
.expect("Failed to create account");
2302
-
assert_eq!(create_res.status(), StatusCode::OK);
2303
-
let account: Value = create_res.json().await.unwrap();
2304
-
let did = account["did"].as_str().unwrap();
2305
-
2306
-
let resolve_res = client
2307
-
.get(format!(
2308
-
"{}/xrpc/com.atproto.identity.resolveHandle",
2309
-
base_url().await
2310
-
))
2311
-
.query(&[("handle", handle.as_str())])
2312
-
.send()
2313
-
.await
2314
-
.expect("Failed to resolve handle");
2315
-
2316
-
assert_eq!(resolve_res.status(), StatusCode::OK);
2317
-
let resolve_body: Value = resolve_res.json().await.unwrap();
2318
-
assert_eq!(resolve_body["did"], did);
2319
-
}
2320
-
2321
-
#[tokio::test]
2322
-
async fn test_service_auth_lifecycle() {
2323
-
let client = client();
2324
-
let (did, jwt) = setup_new_user("service-auth-test").await;
2325
-
2326
-
let service_auth_res = client
2327
-
.get(format!(
2328
-
"{}/xrpc/com.atproto.server.getServiceAuth",
2329
-
base_url().await
2330
-
))
2331
-
.query(&[
2332
-
("aud", "did:web:api.bsky.app"),
2333
-
("lxm", "com.atproto.repo.uploadBlob"),
2334
-
])
2335
-
.bearer_auth(&jwt)
2336
-
.send()
2337
-
.await
2338
-
.expect("Failed to get service auth");
2339
-
2340
-
assert_eq!(service_auth_res.status(), StatusCode::OK);
2341
-
let auth_body: Value = service_auth_res.json().await.unwrap();
2342
-
let service_token = auth_body["token"].as_str().expect("No token in response");
2343
-
2344
-
let parts: Vec<&str> = service_token.split('.').collect();
2345
-
assert_eq!(parts.len(), 3, "Service token should be a valid JWT");
2346
-
2347
-
let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
2348
-
.decode(parts[1])
2349
-
.expect("Failed to decode JWT payload");
2350
-
let claims: Value = serde_json::from_slice(&payload_bytes).expect("Invalid JWT payload");
2351
-
2352
-
assert_eq!(claims["iss"], did);
2353
-
assert_eq!(claims["aud"], "did:web:api.bsky.app");
2354
-
assert_eq!(claims["lxm"], "com.atproto.repo.uploadBlob");
2355
-
}
2356
-
2357
-
#[tokio::test]
2358
-
async fn test_moderation_report_lifecycle() {
2359
-
let client = client();
2360
-
let (alice_did, alice_jwt) = setup_new_user("alice-report").await;
2361
-
let (bob_did, bob_jwt) = setup_new_user("bob-report").await;
2362
-
2363
-
let (post_uri, post_cid) =
2364
-
create_post(&client, &bob_did, &bob_jwt, "This is a reportable post").await;
2365
-
2366
-
let report_payload = json!({
2367
-
"reasonType": "com.atproto.moderation.defs#reasonSpam",
2368
-
"reason": "This looks like spam to me",
2369
-
"subject": {
2370
-
"$type": "com.atproto.repo.strongRef",
2371
-
"uri": post_uri,
2372
-
"cid": post_cid
2373
-
}
2374
-
});
2375
-
2376
-
let report_res = client
2377
-
.post(format!(
2378
-
"{}/xrpc/com.atproto.moderation.createReport",
2379
-
base_url().await
2380
-
))
2381
-
.bearer_auth(&alice_jwt)
2382
-
.json(&report_payload)
2383
-
.send()
2384
-
.await
2385
-
.expect("Failed to create report");
2386
-
2387
-
assert_eq!(report_res.status(), StatusCode::OK);
2388
-
let report_body: Value = report_res.json().await.unwrap();
2389
-
assert!(report_body["id"].is_number(), "Report should have an ID");
2390
-
assert_eq!(report_body["reasonType"], "com.atproto.moderation.defs#reasonSpam");
2391
-
assert_eq!(report_body["reportedBy"], alice_did);
2392
-
2393
-
let account_report_payload = json!({
2394
-
"reasonType": "com.atproto.moderation.defs#reasonOther",
2395
-
"reason": "Suspicious account activity",
2396
-
"subject": {
2397
-
"$type": "com.atproto.admin.defs#repoRef",
2398
-
"did": bob_did
2399
-
}
2400
-
});
2401
-
2402
-
let account_report_res = client
2403
-
.post(format!(
2404
-
"{}/xrpc/com.atproto.moderation.createReport",
2405
-
base_url().await
2406
-
))
2407
-
.bearer_auth(&alice_jwt)
2408
-
.json(&account_report_payload)
2409
-
.send()
2410
-
.await
2411
-
.expect("Failed to create account report");
2412
-
2413
-
assert_eq!(account_report_res.status(), StatusCode::OK);
2414
-
}
+139
tests/lifecycle_session.rs
+139
tests/lifecycle_session.rs
···
304
304
let passwords_after = list_after["passwords"].as_array().unwrap();
305
305
assert_eq!(passwords_after.len(), 0, "No app passwords should remain");
306
306
}
307
+
308
+
#[tokio::test]
309
+
async fn test_account_deactivation_lifecycle() {
310
+
let client = client();
311
+
let ts = Utc::now().timestamp_millis();
312
+
let handle = format!("deactivate-{}.test", ts);
313
+
let email = format!("deactivate-{}@test.com", ts);
314
+
let password = "deactivate-password";
315
+
316
+
let create_res = client
317
+
.post(format!(
318
+
"{}/xrpc/com.atproto.server.createAccount",
319
+
base_url().await
320
+
))
321
+
.json(&json!({
322
+
"handle": handle,
323
+
"email": email,
324
+
"password": password
325
+
}))
326
+
.send()
327
+
.await
328
+
.expect("Failed to create account");
329
+
330
+
assert_eq!(create_res.status(), StatusCode::OK);
331
+
let account: Value = create_res.json().await.unwrap();
332
+
let did = account["did"].as_str().unwrap().to_string();
333
+
let jwt = account["accessJwt"].as_str().unwrap().to_string();
334
+
335
+
let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await;
336
+
let post_rkey = post_uri.split('/').last().unwrap();
337
+
338
+
let status_before = client
339
+
.get(format!(
340
+
"{}/xrpc/com.atproto.server.checkAccountStatus",
341
+
base_url().await
342
+
))
343
+
.bearer_auth(&jwt)
344
+
.send()
345
+
.await
346
+
.expect("Failed to check status");
347
+
348
+
assert_eq!(status_before.status(), StatusCode::OK);
349
+
let status_body: Value = status_before.json().await.unwrap();
350
+
assert_eq!(status_body["activated"], true);
351
+
352
+
let deactivate_res = client
353
+
.post(format!(
354
+
"{}/xrpc/com.atproto.server.deactivateAccount",
355
+
base_url().await
356
+
))
357
+
.bearer_auth(&jwt)
358
+
.json(&json!({}))
359
+
.send()
360
+
.await
361
+
.expect("Failed to deactivate");
362
+
363
+
assert_eq!(deactivate_res.status(), StatusCode::OK);
364
+
365
+
let get_post_res = client
366
+
.get(format!(
367
+
"{}/xrpc/com.atproto.repo.getRecord",
368
+
base_url().await
369
+
))
370
+
.query(&[
371
+
("repo", did.as_str()),
372
+
("collection", "app.bsky.feed.post"),
373
+
("rkey", post_rkey),
374
+
])
375
+
.send()
376
+
.await
377
+
.expect("Failed to get post while deactivated");
378
+
379
+
assert_eq!(get_post_res.status(), StatusCode::OK, "Records should still be readable");
380
+
381
+
let activate_res = client
382
+
.post(format!(
383
+
"{}/xrpc/com.atproto.server.activateAccount",
384
+
base_url().await
385
+
))
386
+
.bearer_auth(&jwt)
387
+
.json(&json!({}))
388
+
.send()
389
+
.await
390
+
.expect("Failed to reactivate");
391
+
392
+
assert_eq!(activate_res.status(), StatusCode::OK);
393
+
394
+
let status_after_activate = client
395
+
.get(format!(
396
+
"{}/xrpc/com.atproto.server.checkAccountStatus",
397
+
base_url().await
398
+
))
399
+
.bearer_auth(&jwt)
400
+
.send()
401
+
.await
402
+
.expect("Failed to check status after activate");
403
+
404
+
assert_eq!(status_after_activate.status(), StatusCode::OK);
405
+
406
+
let (new_post_uri, _) = create_post(&client, &did, &jwt, "Post after reactivation").await;
407
+
assert!(!new_post_uri.is_empty(), "Should be able to post after reactivation");
408
+
}
409
+
410
+
#[tokio::test]
411
+
async fn test_service_auth_lifecycle() {
412
+
let client = client();
413
+
let (did, jwt) = setup_new_user("service-auth-test").await;
414
+
415
+
let service_auth_res = client
416
+
.get(format!(
417
+
"{}/xrpc/com.atproto.server.getServiceAuth",
418
+
base_url().await
419
+
))
420
+
.query(&[
421
+
("aud", "did:web:api.bsky.app"),
422
+
("lxm", "com.atproto.repo.uploadBlob"),
423
+
])
424
+
.bearer_auth(&jwt)
425
+
.send()
426
+
.await
427
+
.expect("Failed to get service auth");
428
+
429
+
assert_eq!(service_auth_res.status(), StatusCode::OK);
430
+
let auth_body: Value = service_auth_res.json().await.unwrap();
431
+
let service_token = auth_body["token"].as_str().expect("No token in response");
432
+
433
+
let parts: Vec<&str> = service_token.split('.').collect();
434
+
assert_eq!(parts.len(), 3, "Service token should be a valid JWT");
435
+
436
+
use base64::Engine;
437
+
let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
438
+
.decode(parts[1])
439
+
.expect("Failed to decode JWT payload");
440
+
let claims: Value = serde_json::from_slice(&payload_bytes).expect("Invalid JWT payload");
441
+
442
+
assert_eq!(claims["iss"], did);
443
+
assert_eq!(claims["aud"], "did:web:api.bsky.app");
444
+
assert_eq!(claims["lxm"], "com.atproto.repo.uploadBlob");
445
+
}
+67
tests/moderation.rs
+67
tests/moderation.rs
···
1
+
mod common;
2
+
mod helpers;
3
+
4
+
use common::*;
5
+
use helpers::*;
6
+
7
+
use reqwest::StatusCode;
8
+
use serde_json::{Value, json};
9
+
10
+
#[tokio::test]
11
+
async fn test_moderation_report_lifecycle() {
12
+
let client = client();
13
+
let (alice_did, alice_jwt) = setup_new_user("alice-report").await;
14
+
let (bob_did, bob_jwt) = setup_new_user("bob-report").await;
15
+
16
+
let (post_uri, post_cid) =
17
+
create_post(&client, &bob_did, &bob_jwt, "This is a reportable post").await;
18
+
19
+
let report_payload = json!({
20
+
"reasonType": "com.atproto.moderation.defs#reasonSpam",
21
+
"reason": "This looks like spam to me",
22
+
"subject": {
23
+
"$type": "com.atproto.repo.strongRef",
24
+
"uri": post_uri,
25
+
"cid": post_cid
26
+
}
27
+
});
28
+
29
+
let report_res = client
30
+
.post(format!(
31
+
"{}/xrpc/com.atproto.moderation.createReport",
32
+
base_url().await
33
+
))
34
+
.bearer_auth(&alice_jwt)
35
+
.json(&report_payload)
36
+
.send()
37
+
.await
38
+
.expect("Failed to create report");
39
+
40
+
assert_eq!(report_res.status(), StatusCode::OK);
41
+
let report_body: Value = report_res.json().await.unwrap();
42
+
assert!(report_body["id"].is_number(), "Report should have an ID");
43
+
assert_eq!(report_body["reasonType"], "com.atproto.moderation.defs#reasonSpam");
44
+
assert_eq!(report_body["reportedBy"], alice_did);
45
+
46
+
let account_report_payload = json!({
47
+
"reasonType": "com.atproto.moderation.defs#reasonOther",
48
+
"reason": "Suspicious account activity",
49
+
"subject": {
50
+
"$type": "com.atproto.admin.defs#repoRef",
51
+
"did": bob_did
52
+
}
53
+
});
54
+
55
+
let account_report_res = client
56
+
.post(format!(
57
+
"{}/xrpc/com.atproto.moderation.createReport",
58
+
base_url().await
59
+
))
60
+
.bearer_auth(&alice_jwt)
61
+
.json(&account_report_payload)
62
+
.send()
63
+
.await
64
+
.expect("Failed to create account report");
65
+
66
+
assert_eq!(account_report_res.status(), StatusCode::OK);
67
+
}
+270
-1
tests/sync_repo.rs
+270
-1
tests/sync_repo.rs
···
1
1
mod common;
2
+
mod helpers;
2
3
use common::*;
4
+
use helpers::*;
5
+
3
6
use reqwest::StatusCode;
4
-
use serde_json::Value;
7
+
use reqwest::header;
8
+
use serde_json::{Value, json};
9
+
use chrono::Utc;
5
10
6
11
#[tokio::test]
7
12
async fn test_get_latest_commit_success() {
···
429
434
430
435
assert_eq!(res.status(), StatusCode::NOT_FOUND);
431
436
}
437
+
438
+
#[tokio::test]
439
+
async fn test_sync_record_lifecycle() {
440
+
let client = client();
441
+
let (did, jwt) = setup_new_user("sync-record-lifecycle").await;
442
+
443
+
let (post_uri, _post_cid) =
444
+
create_post(&client, &did, &jwt, "Post for sync record test").await;
445
+
let post_rkey = post_uri.split('/').last().unwrap();
446
+
447
+
let sync_record_res = client
448
+
.get(format!(
449
+
"{}/xrpc/com.atproto.sync.getRecord",
450
+
base_url().await
451
+
))
452
+
.query(&[
453
+
("did", did.as_str()),
454
+
("collection", "app.bsky.feed.post"),
455
+
("rkey", post_rkey),
456
+
])
457
+
.send()
458
+
.await
459
+
.expect("Failed to get sync record");
460
+
461
+
assert_eq!(sync_record_res.status(), StatusCode::OK);
462
+
assert_eq!(
463
+
sync_record_res
464
+
.headers()
465
+
.get("content-type")
466
+
.and_then(|h| h.to_str().ok()),
467
+
Some("application/vnd.ipld.car")
468
+
);
469
+
let car_bytes = sync_record_res.bytes().await.unwrap();
470
+
assert!(!car_bytes.is_empty(), "CAR data should not be empty");
471
+
472
+
let latest_before = client
473
+
.get(format!(
474
+
"{}/xrpc/com.atproto.sync.getLatestCommit",
475
+
base_url().await
476
+
))
477
+
.query(&[("did", did.as_str())])
478
+
.send()
479
+
.await
480
+
.expect("Failed to get latest commit");
481
+
let latest_before_body: Value = latest_before.json().await.unwrap();
482
+
let rev_before = latest_before_body["rev"].as_str().unwrap().to_string();
483
+
484
+
let (post2_uri, _) = create_post(&client, &did, &jwt, "Second post for sync test").await;
485
+
486
+
let latest_after = client
487
+
.get(format!(
488
+
"{}/xrpc/com.atproto.sync.getLatestCommit",
489
+
base_url().await
490
+
))
491
+
.query(&[("did", did.as_str())])
492
+
.send()
493
+
.await
494
+
.expect("Failed to get latest commit after");
495
+
let latest_after_body: Value = latest_after.json().await.unwrap();
496
+
let rev_after = latest_after_body["rev"].as_str().unwrap().to_string();
497
+
assert_ne!(rev_before, rev_after, "Revision should change after new record");
498
+
499
+
let delete_payload = json!({
500
+
"repo": did,
501
+
"collection": "app.bsky.feed.post",
502
+
"rkey": post_rkey
503
+
});
504
+
let delete_res = client
505
+
.post(format!(
506
+
"{}/xrpc/com.atproto.repo.deleteRecord",
507
+
base_url().await
508
+
))
509
+
.bearer_auth(&jwt)
510
+
.json(&delete_payload)
511
+
.send()
512
+
.await
513
+
.expect("Failed to delete record");
514
+
assert_eq!(delete_res.status(), StatusCode::OK);
515
+
516
+
let sync_deleted_res = client
517
+
.get(format!(
518
+
"{}/xrpc/com.atproto.sync.getRecord",
519
+
base_url().await
520
+
))
521
+
.query(&[
522
+
("did", did.as_str()),
523
+
("collection", "app.bsky.feed.post"),
524
+
("rkey", post_rkey),
525
+
])
526
+
.send()
527
+
.await
528
+
.expect("Failed to check deleted record via sync");
529
+
assert_eq!(
530
+
sync_deleted_res.status(),
531
+
StatusCode::NOT_FOUND,
532
+
"Deleted record should return 404 via sync.getRecord"
533
+
);
534
+
535
+
let post2_rkey = post2_uri.split('/').last().unwrap();
536
+
let sync_post2_res = client
537
+
.get(format!(
538
+
"{}/xrpc/com.atproto.sync.getRecord",
539
+
base_url().await
540
+
))
541
+
.query(&[
542
+
("did", did.as_str()),
543
+
("collection", "app.bsky.feed.post"),
544
+
("rkey", post2_rkey),
545
+
])
546
+
.send()
547
+
.await
548
+
.expect("Failed to get second post via sync");
549
+
assert_eq!(
550
+
sync_post2_res.status(),
551
+
StatusCode::OK,
552
+
"Second post should still be accessible"
553
+
);
554
+
}
555
+
556
+
#[tokio::test]
557
+
async fn test_sync_repo_export_lifecycle() {
558
+
let client = client();
559
+
let (did, jwt) = setup_new_user("sync-repo-export").await;
560
+
561
+
let profile_payload = json!({
562
+
"repo": did,
563
+
"collection": "app.bsky.actor.profile",
564
+
"rkey": "self",
565
+
"record": {
566
+
"$type": "app.bsky.actor.profile",
567
+
"displayName": "Sync Export User"
568
+
}
569
+
});
570
+
let profile_res = client
571
+
.post(format!(
572
+
"{}/xrpc/com.atproto.repo.putRecord",
573
+
base_url().await
574
+
))
575
+
.bearer_auth(&jwt)
576
+
.json(&profile_payload)
577
+
.send()
578
+
.await
579
+
.expect("Failed to create profile");
580
+
assert_eq!(profile_res.status(), StatusCode::OK);
581
+
582
+
for i in 0..3 {
583
+
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
584
+
create_post(&client, &did, &jwt, &format!("Export test post {}", i)).await;
585
+
}
586
+
587
+
let blob_data = b"blob data for sync export test";
588
+
let upload_res = client
589
+
.post(format!(
590
+
"{}/xrpc/com.atproto.repo.uploadBlob",
591
+
base_url().await
592
+
))
593
+
.header(header::CONTENT_TYPE, "application/octet-stream")
594
+
.bearer_auth(&jwt)
595
+
.body(blob_data.to_vec())
596
+
.send()
597
+
.await
598
+
.expect("Failed to upload blob");
599
+
assert_eq!(upload_res.status(), StatusCode::OK);
600
+
let blob_body: Value = upload_res.json().await.unwrap();
601
+
let blob_cid = blob_body["blob"]["ref"]["$link"].as_str().unwrap().to_string();
602
+
603
+
let repo_status_res = client
604
+
.get(format!(
605
+
"{}/xrpc/com.atproto.sync.getRepoStatus",
606
+
base_url().await
607
+
))
608
+
.query(&[("did", did.as_str())])
609
+
.send()
610
+
.await
611
+
.expect("Failed to get repo status");
612
+
assert_eq!(repo_status_res.status(), StatusCode::OK);
613
+
let status_body: Value = repo_status_res.json().await.unwrap();
614
+
assert_eq!(status_body["did"], did);
615
+
assert_eq!(status_body["active"], true);
616
+
617
+
let get_repo_res = client
618
+
.get(format!(
619
+
"{}/xrpc/com.atproto.sync.getRepo",
620
+
base_url().await
621
+
))
622
+
.query(&[("did", did.as_str())])
623
+
.send()
624
+
.await
625
+
.expect("Failed to get full repo");
626
+
assert_eq!(get_repo_res.status(), StatusCode::OK);
627
+
assert_eq!(
628
+
get_repo_res
629
+
.headers()
630
+
.get("content-type")
631
+
.and_then(|h| h.to_str().ok()),
632
+
Some("application/vnd.ipld.car")
633
+
);
634
+
let repo_car = get_repo_res.bytes().await.unwrap();
635
+
assert!(repo_car.len() > 100, "Repo CAR should have substantial data");
636
+
637
+
let list_blobs_res = client
638
+
.get(format!(
639
+
"{}/xrpc/com.atproto.sync.listBlobs",
640
+
base_url().await
641
+
))
642
+
.query(&[("did", did.as_str())])
643
+
.send()
644
+
.await
645
+
.expect("Failed to list blobs");
646
+
assert_eq!(list_blobs_res.status(), StatusCode::OK);
647
+
let blobs_body: Value = list_blobs_res.json().await.unwrap();
648
+
let cids = blobs_body["cids"].as_array().unwrap();
649
+
assert!(!cids.is_empty(), "Should have at least one blob");
650
+
651
+
let get_blob_res = client
652
+
.get(format!(
653
+
"{}/xrpc/com.atproto.sync.getBlob",
654
+
base_url().await
655
+
))
656
+
.query(&[("did", did.as_str()), ("cid", &blob_cid)])
657
+
.send()
658
+
.await
659
+
.expect("Failed to get blob");
660
+
assert_eq!(get_blob_res.status(), StatusCode::OK);
661
+
let retrieved_blob = get_blob_res.bytes().await.unwrap();
662
+
assert_eq!(
663
+
retrieved_blob.as_ref(),
664
+
blob_data,
665
+
"Retrieved blob should match uploaded data"
666
+
);
667
+
668
+
let latest_commit_res = client
669
+
.get(format!(
670
+
"{}/xrpc/com.atproto.sync.getLatestCommit",
671
+
base_url().await
672
+
))
673
+
.query(&[("did", did.as_str())])
674
+
.send()
675
+
.await
676
+
.expect("Failed to get latest commit");
677
+
assert_eq!(latest_commit_res.status(), StatusCode::OK);
678
+
let commit_body: Value = latest_commit_res.json().await.unwrap();
679
+
let root_cid = commit_body["cid"].as_str().unwrap();
680
+
681
+
let get_blocks_url = format!(
682
+
"{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}",
683
+
base_url().await,
684
+
did,
685
+
root_cid
686
+
);
687
+
let get_blocks_res = client
688
+
.get(&get_blocks_url)
689
+
.send()
690
+
.await
691
+
.expect("Failed to get blocks");
692
+
assert_eq!(get_blocks_res.status(), StatusCode::OK);
693
+
assert_eq!(
694
+
get_blocks_res
695
+
.headers()
696
+
.get("content-type")
697
+
.and_then(|h| h.to_str().ok()),
698
+
Some("application/vnd.ipld.car")
699
+
);
700
+
}