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