+1
.gitignore
+1
.gitignore
+56
-13
src/api/actor/preferences.rs
+56
-13
src/api/actor/preferences.rs
···
5
5
http::StatusCode,
6
6
response::{IntoResponse, Response},
7
7
};
8
+
use chrono::{Datelike, NaiveDate, Utc};
8
9
use serde::{Deserialize, Serialize};
9
10
use serde_json::{Value, json};
10
11
11
12
const APP_BSKY_NAMESPACE: &str = "app.bsky";
12
13
const MAX_PREFERENCES_COUNT: usize = 100;
13
14
const MAX_PREFERENCE_SIZE: usize = 10_000;
15
+
const PERSONAL_DETAILS_PREF: &str = "app.bsky.actor.defs#personalDetailsPref";
16
+
const DECLARED_AGE_PREF: &str = "app.bsky.actor.defs#declaredAgePref";
17
+
18
+
fn get_age_from_datestring(birth_date: &str) -> Option<i32> {
19
+
let bday = NaiveDate::parse_from_str(birth_date, "%Y-%m-%d").ok()?;
20
+
let today = Utc::now().date_naive();
21
+
let mut age = today.year() - bday.year();
22
+
let m = today.month() as i32 - bday.month() as i32;
23
+
if m < 0 || (m == 0 && today.day() < bday.day()) {
24
+
age -= 1;
25
+
}
26
+
Some(age)
27
+
}
14
28
15
29
#[derive(Serialize)]
16
30
pub struct GetPreferencesOutput {
···
43
57
.into_response();
44
58
}
45
59
};
60
+
let has_full_access = auth_user.permissions().has_full_access();
46
61
let user_id: uuid::Uuid =
47
62
match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
48
63
.fetch_optional(&state.db)
···
73
88
.into_response();
74
89
}
75
90
};
76
-
let preferences: Vec<Value> = prefs
91
+
let mut personal_details_pref: Option<Value> = None;
92
+
let mut preferences: Vec<Value> = prefs
77
93
.into_iter()
78
94
.filter(|row| {
79
95
row.name == APP_BSKY_NAMESPACE
80
96
|| row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE))
81
97
})
82
98
.filter_map(|row| {
83
-
if row.name == "app.bsky.actor.defs#declaredAgePref" {
99
+
if row.name == DECLARED_AGE_PREF {
84
100
return None;
85
101
}
102
+
if row.name == PERSONAL_DETAILS_PREF {
103
+
if !has_full_access {
104
+
return None;
105
+
}
106
+
personal_details_pref = serde_json::from_value(row.value_json.clone()).ok();
107
+
}
86
108
serde_json::from_value(row.value_json).ok()
87
109
})
88
110
.collect();
111
+
if let Some(ref pref) = personal_details_pref {
112
+
if let Some(birth_date) = pref.get("birthDate").and_then(|v| v.as_str()) {
113
+
if let Some(age) = get_age_from_datestring(birth_date) {
114
+
let declared_age_pref = json!({
115
+
"$type": DECLARED_AGE_PREF,
116
+
"isOverAge13": age >= 13,
117
+
"isOverAge16": age >= 16,
118
+
"isOverAge18": age >= 18,
119
+
});
120
+
preferences.push(declared_age_pref);
121
+
}
122
+
}
123
+
}
89
124
(StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response()
90
125
}
91
126
···
121
156
.into_response();
122
157
}
123
158
};
124
-
let (user_id, is_migration): (uuid::Uuid, bool) = match sqlx::query!(
125
-
"SELECT id, deactivated_at FROM users WHERE did = $1",
159
+
let has_full_access = auth_user.permissions().has_full_access();
160
+
let user_id: uuid::Uuid = match sqlx::query_scalar!(
161
+
"SELECT id FROM users WHERE did = $1",
126
162
auth_user.did
127
163
)
128
164
.fetch_optional(&state.db)
129
165
.await
130
166
{
131
-
Ok(Some(row)) => (row.id, row.deactivated_at.is_some()),
167
+
Ok(Some(id)) => id,
132
168
_ => {
133
169
return (
134
170
StatusCode::INTERNAL_SERVER_ERROR,
···
144
180
)
145
181
.into_response();
146
182
}
183
+
let mut forbidden_prefs: Vec<String> = Vec::new();
147
184
for pref in &input.preferences {
148
185
let pref_str = serde_json::to_string(pref).unwrap_or_default();
149
186
if pref_str.len() > MAX_PREFERENCE_SIZE {
···
158
195
None => {
159
196
return (
160
197
StatusCode::BAD_REQUEST,
161
-
Json(json!({"error": "InvalidRequest", "message": "Preference missing $type field"})),
198
+
Json(json!({"error": "InvalidRequest", "message": "Preference is missing a $type"})),
162
199
)
163
200
.into_response();
164
201
}
···
166
203
if !pref_type.starts_with(APP_BSKY_NAMESPACE) {
167
204
return (
168
205
StatusCode::BAD_REQUEST,
169
-
Json(json!({"error": "InvalidRequest", "message": format!("Invalid preference namespace: {}", pref_type)})),
206
+
Json(json!({"error": "InvalidRequest", "message": format!("Some preferences are not in the {} namespace", APP_BSKY_NAMESPACE)})),
170
207
)
171
208
.into_response();
172
209
}
173
-
if pref_type == "app.bsky.actor.defs#declaredAgePref" && !is_migration {
174
-
return (
175
-
StatusCode::BAD_REQUEST,
176
-
Json(json!({"error": "InvalidRequest", "message": "declaredAgePref is read-only"})),
177
-
)
178
-
.into_response();
210
+
if pref_type == PERSONAL_DETAILS_PREF && !has_full_access {
211
+
forbidden_prefs.push(pref_type.to_string());
179
212
}
213
+
}
214
+
if !forbidden_prefs.is_empty() {
215
+
return (
216
+
StatusCode::BAD_REQUEST,
217
+
Json(json!({"error": "InvalidRequest", "message": format!("Do not have authorization to set preferences: {}", forbidden_prefs.join(", "))})),
218
+
)
219
+
.into_response();
180
220
}
181
221
let mut tx = match state.db.begin().await {
182
222
Ok(tx) => tx,
···
209
249
Some(t) => t,
210
250
None => continue,
211
251
};
252
+
if pref_type == DECLARED_AGE_PREF {
253
+
continue;
254
+
}
212
255
let insert_result = sqlx::query!(
213
256
"INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)",
214
257
user_id,
+7
src/api/identity/account.rs
+7
src/api/identity/account.rs
···
188
188
};
189
189
match crate::api::validation::validate_short_handle(handle_to_validate) {
190
190
Ok(h) => h,
191
+
Err(crate::api::validation::HandleValidationError::Reserved) => {
192
+
return (
193
+
StatusCode::BAD_REQUEST,
194
+
Json(json!({"error": "HandleNotAvailable", "message": "Reserved handle"})),
195
+
)
196
+
.into_response();
197
+
}
191
198
Err(e) => {
192
199
return (
193
200
StatusCode::BAD_REQUEST,
+35
src/api/proxy.rs
+35
src/api/proxy.rs
···
10
10
use serde_json::json;
11
11
use tracing::{error, info, warn};
12
12
13
+
const PROTECTED_METHODS: &[&str] = &[
14
+
"com.atproto.admin.sendEmail",
15
+
"com.atproto.identity.requestPlcOperationSignature",
16
+
"com.atproto.identity.signPlcOperation",
17
+
"com.atproto.identity.updateHandle",
18
+
"com.atproto.server.activateAccount",
19
+
"com.atproto.server.confirmEmail",
20
+
"com.atproto.server.createAppPassword",
21
+
"com.atproto.server.deactivateAccount",
22
+
"com.atproto.server.getAccountInviteCodes",
23
+
"com.atproto.server.getSession",
24
+
"com.atproto.server.listAppPasswords",
25
+
"com.atproto.server.requestAccountDelete",
26
+
"com.atproto.server.requestEmailConfirmation",
27
+
"com.atproto.server.requestEmailUpdate",
28
+
"com.atproto.server.revokeAppPassword",
29
+
"com.atproto.server.updateEmail",
30
+
];
31
+
32
+
fn is_protected_method(method: &str) -> bool {
33
+
PROTECTED_METHODS.contains(&method)
34
+
}
35
+
13
36
pub async fn proxy_handler(
14
37
State(state): State<AppState>,
15
38
Path(method): Path<String>,
···
18
41
RawQuery(query): RawQuery,
19
42
body: Bytes,
20
43
) -> Response {
44
+
if is_protected_method(&method) {
45
+
warn!(method = %method, "Attempted to proxy protected method");
46
+
return (
47
+
StatusCode::BAD_REQUEST,
48
+
Json(json!({
49
+
"error": "InvalidRequest",
50
+
"message": format!("Cannot proxy protected method: {}", method)
51
+
})),
52
+
)
53
+
.into_response();
54
+
}
55
+
21
56
let proxy_header = match headers.get("atproto-proxy").and_then(|h| h.to_str().ok()) {
22
57
Some(h) => h.to_string(),
23
58
None => {
+78
-2
src/api/validation.rs
+78
-2
src/api/validation.rs
···
6
6
7
7
pub const MIN_HANDLE_LENGTH: usize = 3;
8
8
pub const MAX_HANDLE_LENGTH: usize = 253;
9
+
pub const MAX_SERVICE_HANDLE_LOCAL_PART: usize = 18;
9
10
10
11
#[derive(Debug, PartialEq)]
11
12
pub enum HandleValidationError {
···
17
18
EndsWithInvalidChar,
18
19
ContainsSpaces,
19
20
BannedWord,
21
+
Reserved,
20
22
}
21
23
22
24
impl std::fmt::Display for HandleValidationError {
···
31
33
Self::TooLong => write!(
32
34
f,
33
35
"Handle exceeds maximum length of {} characters",
34
-
MAX_HANDLE_LENGTH
36
+
MAX_SERVICE_HANDLE_LOCAL_PART
35
37
),
36
38
Self::InvalidCharacters => write!(
37
39
f,
···
43
45
Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen"),
44
46
Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"),
45
47
Self::BannedWord => write!(f, "Inappropriate language in handle"),
48
+
Self::Reserved => write!(f, "Reserved handle"),
46
49
}
47
50
}
48
51
}
49
52
50
53
pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationError> {
54
+
validate_service_handle(handle, false)
55
+
}
56
+
57
+
pub fn validate_service_handle(
58
+
handle: &str,
59
+
allow_reserved: bool,
60
+
) -> Result<String, HandleValidationError> {
51
61
let handle = handle.trim();
52
62
53
63
if handle.is_empty() {
···
62
72
return Err(HandleValidationError::TooShort);
63
73
}
64
74
65
-
if handle.len() > MAX_HANDLE_LENGTH {
75
+
if handle.len() > MAX_SERVICE_HANDLE_LOCAL_PART {
66
76
return Err(HandleValidationError::TooLong);
67
77
}
68
78
···
86
96
87
97
if crate::moderation::has_explicit_slur(handle) {
88
98
return Err(HandleValidationError::BannedWord);
99
+
}
100
+
101
+
if !allow_reserved && crate::handle::reserved::is_reserved_subdomain(handle) {
102
+
return Err(HandleValidationError::Reserved);
89
103
}
90
104
91
105
Ok(handle.to_lowercase())
···
221
235
#[test]
222
236
fn test_handle_trimming() {
223
237
assert_eq!(validate_short_handle(" alice "), Ok("alice".to_string()));
238
+
}
239
+
240
+
#[test]
241
+
fn test_handle_max_length() {
242
+
assert_eq!(
243
+
validate_short_handle("exactly18charslol"),
244
+
Ok("exactly18charslol".to_string())
245
+
);
246
+
assert_eq!(
247
+
validate_short_handle("exactly18charslol1"),
248
+
Ok("exactly18charslol1".to_string())
249
+
);
250
+
assert_eq!(
251
+
validate_short_handle("exactly19characters"),
252
+
Err(HandleValidationError::TooLong)
253
+
);
254
+
assert_eq!(
255
+
validate_short_handle("waytoolongusername123456789"),
256
+
Err(HandleValidationError::TooLong)
257
+
);
258
+
}
259
+
260
+
#[test]
261
+
fn test_reserved_subdomains() {
262
+
assert_eq!(
263
+
validate_short_handle("admin"),
264
+
Err(HandleValidationError::Reserved)
265
+
);
266
+
assert_eq!(
267
+
validate_short_handle("api"),
268
+
Err(HandleValidationError::Reserved)
269
+
);
270
+
assert_eq!(
271
+
validate_short_handle("bsky"),
272
+
Err(HandleValidationError::Reserved)
273
+
);
274
+
assert_eq!(
275
+
validate_short_handle("barackobama"),
276
+
Err(HandleValidationError::Reserved)
277
+
);
278
+
assert_eq!(
279
+
validate_short_handle("ADMIN"),
280
+
Err(HandleValidationError::Reserved)
281
+
);
282
+
assert_eq!(validate_short_handle("alice"), Ok("alice".to_string()));
283
+
assert_eq!(
284
+
validate_short_handle("notreserved"),
285
+
Ok("notreserved".to_string())
286
+
);
287
+
}
288
+
289
+
#[test]
290
+
fn test_allow_reserved() {
291
+
assert_eq!(
292
+
validate_service_handle("admin", true),
293
+
Ok("admin".to_string())
294
+
);
295
+
assert_eq!(validate_service_handle("api", true), Ok("api".to_string()));
296
+
assert_eq!(
297
+
validate_service_handle("admin", false),
298
+
Err(HandleValidationError::Reserved)
299
+
);
224
300
}
225
301
226
302
#[test]
+2
src/handle/mod.rs
+2
src/handle/mod.rs
+1097
src/handle/reserved.rs
+1097
src/handle/reserved.rs
···
1
+
use std::collections::HashSet;
2
+
use std::sync::LazyLock;
3
+
4
+
const ATP_SPECIFIC: &[&str] = &[
5
+
"at",
6
+
"atp",
7
+
"plc",
8
+
"pds",
9
+
"did",
10
+
"repo",
11
+
"tid",
12
+
"nsid",
13
+
"xrpc",
14
+
"lex",
15
+
"lexicon",
16
+
"bsky",
17
+
"bluesky",
18
+
"handle",
19
+
];
20
+
21
+
const COMMONLY_RESERVED: &[&str] = &[
22
+
"about",
23
+
"abuse",
24
+
"access",
25
+
"account",
26
+
"accounts",
27
+
"acme",
28
+
"activate",
29
+
"activities",
30
+
"activity",
31
+
"ad",
32
+
"add",
33
+
"address",
34
+
"adm",
35
+
"admanager",
36
+
"admin",
37
+
"administration",
38
+
"administrator",
39
+
"administrators",
40
+
"admins",
41
+
"ads",
42
+
"adsense",
43
+
"adult",
44
+
"advertising",
45
+
"adwords",
46
+
"affiliate",
47
+
"affiliatepage",
48
+
"affiliates",
49
+
"afp",
50
+
"ajax",
51
+
"all",
52
+
"alpha",
53
+
"analysis",
54
+
"analytics",
55
+
"android",
56
+
"anon",
57
+
"anonymous",
58
+
"answer",
59
+
"answers",
60
+
"ap",
61
+
"api",
62
+
"apis",
63
+
"app",
64
+
"appengine",
65
+
"appnews",
66
+
"apps",
67
+
"archive",
68
+
"archives",
69
+
"article",
70
+
"asdf",
71
+
"asset",
72
+
"assets",
73
+
"auth",
74
+
"authentication",
75
+
"avatar",
76
+
"backup",
77
+
"bank",
78
+
"banner",
79
+
"banners",
80
+
"base",
81
+
"beginners",
82
+
"beta",
83
+
"billing",
84
+
"bin",
85
+
"binaries",
86
+
"binary",
87
+
"blackberry",
88
+
"blog",
89
+
"blogs",
90
+
"blogsearch",
91
+
"board",
92
+
"book",
93
+
"bookmark",
94
+
"bookmarks",
95
+
"books",
96
+
"bot",
97
+
"bots",
98
+
"bug",
99
+
"bugs",
100
+
"business",
101
+
"buy",
102
+
"buzz",
103
+
"cache",
104
+
"calendar",
105
+
"call",
106
+
"campaign",
107
+
"cancel",
108
+
"captcha",
109
+
"career",
110
+
"careers",
111
+
"cart",
112
+
"catalog",
113
+
"catalogs",
114
+
"categories",
115
+
"category",
116
+
"cdn",
117
+
"cgi",
118
+
"cgi-bin",
119
+
"changelog",
120
+
"chart",
121
+
"charts",
122
+
"chat",
123
+
"check",
124
+
"checked",
125
+
"checking",
126
+
"checkout",
127
+
"client",
128
+
"cliente",
129
+
"clients",
130
+
"clients1",
131
+
"cnarne",
132
+
"code",
133
+
"comercial",
134
+
"comment",
135
+
"comments",
136
+
"communities",
137
+
"community",
138
+
"company",
139
+
"compare",
140
+
"compras",
141
+
"config",
142
+
"configuration",
143
+
"confirm",
144
+
"confirmation",
145
+
"connect",
146
+
"contact",
147
+
"contacts",
148
+
"contactus",
149
+
"contact-us",
150
+
"contact_us",
151
+
"content",
152
+
"contest",
153
+
"contribute",
154
+
"contributor",
155
+
"contributors",
156
+
"coppa",
157
+
"copyright",
158
+
"copyrights",
159
+
"core",
160
+
"corp",
161
+
"countries",
162
+
"country",
163
+
"cpanel",
164
+
"create",
165
+
"css",
166
+
"cssproxy",
167
+
"customise",
168
+
"customize",
169
+
"dashboard",
170
+
"data",
171
+
"db",
172
+
"default",
173
+
"delete",
174
+
"demo",
175
+
"design",
176
+
"designer",
177
+
"desktop",
178
+
"destroy",
179
+
"dev",
180
+
"devel",
181
+
"developer",
182
+
"developers",
183
+
"devs",
184
+
"diagram",
185
+
"diary",
186
+
"dict",
187
+
"dictionary",
188
+
"die",
189
+
"dir",
190
+
"directory",
191
+
"direct_messages",
192
+
"direct-messages",
193
+
"dist",
194
+
"diversity",
195
+
"dl",
196
+
"dmca",
197
+
"doc",
198
+
"docs",
199
+
"documentation",
200
+
"documentations",
201
+
"documents",
202
+
"domain",
203
+
"domains",
204
+
"donate",
205
+
"download",
206
+
"downloads",
207
+
"e",
208
+
"e-mail",
209
+
"earth",
210
+
"ecommerce",
211
+
"edit",
212
+
"edits",
213
+
"editor",
214
+
"edu",
215
+
"education",
216
+
"email",
217
+
"embed",
218
+
"embedded",
219
+
"employment",
220
+
"employments",
221
+
"empty",
222
+
"enable",
223
+
"encrypted",
224
+
"end",
225
+
"engine",
226
+
"enterprise",
227
+
"enterprises",
228
+
"entries",
229
+
"entry",
230
+
"error",
231
+
"errorlog",
232
+
"errors",
233
+
"eval",
234
+
"event",
235
+
"example",
236
+
"examplecommunity",
237
+
"exampleopenid",
238
+
"examplesyn",
239
+
"examplesyndicated",
240
+
"exampleusername",
241
+
"exchange",
242
+
"exit",
243
+
"explore",
244
+
"faq",
245
+
"faqs",
246
+
"favorite",
247
+
"favorites",
248
+
"favourite",
249
+
"favourites",
250
+
"feature",
251
+
"features",
252
+
"feed",
253
+
"feedback",
254
+
"feedburner",
255
+
"feedproxy",
256
+
"feeds",
257
+
"file",
258
+
"files",
259
+
"finance",
260
+
"folder",
261
+
"folders",
262
+
"first",
263
+
"following",
264
+
"forgot",
265
+
"form",
266
+
"forms",
267
+
"forum",
268
+
"forums",
269
+
"founder",
270
+
"free",
271
+
"friend",
272
+
"friends",
273
+
"ftp",
274
+
"fuck",
275
+
"fun",
276
+
"fusion",
277
+
"gadget",
278
+
"gadgets",
279
+
"game",
280
+
"games",
281
+
"gears",
282
+
"general",
283
+
"geographic",
284
+
"get",
285
+
"gettingstarted",
286
+
"gift",
287
+
"gifts",
288
+
"gist",
289
+
"git",
290
+
"github",
291
+
"gmail",
292
+
"go",
293
+
"golang",
294
+
"goto",
295
+
"gov",
296
+
"graph",
297
+
"graphs",
298
+
"group",
299
+
"groups",
300
+
"guest",
301
+
"guests",
302
+
"guide",
303
+
"guides",
304
+
"hack",
305
+
"hacks",
306
+
"head",
307
+
"help",
308
+
"home",
309
+
"homepage",
310
+
"host",
311
+
"hosting",
312
+
"hostmaster",
313
+
"hostname",
314
+
"howto",
315
+
"how-to",
316
+
"how_to",
317
+
"html",
318
+
"htrnl",
319
+
"http",
320
+
"httpd",
321
+
"https",
322
+
"i",
323
+
"iamges",
324
+
"icon",
325
+
"icons",
326
+
"id",
327
+
"idea",
328
+
"ideas",
329
+
"im",
330
+
"image",
331
+
"images",
332
+
"img",
333
+
"imap",
334
+
"inbox",
335
+
"inboxes",
336
+
"index",
337
+
"indexes",
338
+
"info",
339
+
"information",
340
+
"inquiry",
341
+
"intranet",
342
+
"investor",
343
+
"investors",
344
+
"invitation",
345
+
"invitations",
346
+
"invite",
347
+
"invoice",
348
+
"invoices",
349
+
"imac",
350
+
"ios",
351
+
"ipad",
352
+
"iphone",
353
+
"irc",
354
+
"irnages",
355
+
"irng",
356
+
"is",
357
+
"issue",
358
+
"issues",
359
+
"it",
360
+
"item",
361
+
"items",
362
+
"java",
363
+
"javascript",
364
+
"job",
365
+
"jobs",
366
+
"join",
367
+
"js",
368
+
"json",
369
+
"jump",
370
+
"kb",
371
+
"knowledge-base",
372
+
"knowledgebase",
373
+
"lab",
374
+
"labs",
375
+
"language",
376
+
"languages",
377
+
"last",
378
+
"ldap_status",
379
+
"ldap-status",
380
+
"ldapstatus",
381
+
"legal",
382
+
"license",
383
+
"licenses",
384
+
"link",
385
+
"links",
386
+
"linux",
387
+
"list",
388
+
"lists",
389
+
"livejournal",
390
+
"lj",
391
+
"local",
392
+
"locale",
393
+
"location",
394
+
"log",
395
+
"log-in",
396
+
"log-out",
397
+
"login",
398
+
"logout",
399
+
"logs",
400
+
"log_in",
401
+
"log_out",
402
+
"m",
403
+
"mac",
404
+
"macos",
405
+
"macosx",
406
+
"mac-os",
407
+
"mac-os-x",
408
+
"mac_os_x",
409
+
"mail",
410
+
"mailer",
411
+
"mailing",
412
+
"main",
413
+
"maintenance",
414
+
"manage",
415
+
"manager",
416
+
"manual",
417
+
"map",
418
+
"maps",
419
+
"marketing",
420
+
"master",
421
+
"me",
422
+
"media",
423
+
"member",
424
+
"members",
425
+
"memories",
426
+
"memory",
427
+
"merchandise",
428
+
"message",
429
+
"messages",
430
+
"messenger",
431
+
"mg",
432
+
"microblog",
433
+
"microblogs",
434
+
"mine",
435
+
"mis",
436
+
"misc",
437
+
"mms",
438
+
"mob",
439
+
"mobile",
440
+
"model",
441
+
"models",
442
+
"money",
443
+
"movie",
444
+
"movies",
445
+
"mp3",
446
+
"mp4",
447
+
"msg",
448
+
"msn",
449
+
"music",
450
+
"mx",
451
+
"my",
452
+
"mymme",
453
+
"mysql",
454
+
"name",
455
+
"named",
456
+
"nan",
457
+
"navi",
458
+
"navigation",
459
+
"net",
460
+
"network",
461
+
"networks",
462
+
"new",
463
+
"news",
464
+
"newsletter",
465
+
"nick",
466
+
"nickname",
467
+
"nil",
468
+
"none",
469
+
"notes",
470
+
"noticias",
471
+
"notification",
472
+
"notifications",
473
+
"notify",
474
+
"ns",
475
+
"ns1",
476
+
"ns2",
477
+
"ns3",
478
+
"ns4",
479
+
"ns5",
480
+
"null",
481
+
"oauth",
482
+
"oauth-clients",
483
+
"oauth_clients",
484
+
"ocsp",
485
+
"offer",
486
+
"offers",
487
+
"official",
488
+
"old",
489
+
"online",
490
+
"openid",
491
+
"operator",
492
+
"option",
493
+
"options",
494
+
"order",
495
+
"orders",
496
+
"org",
497
+
"organization",
498
+
"organizations",
499
+
"other",
500
+
"overview",
501
+
"owner",
502
+
"owners",
503
+
"p0rn",
504
+
"pack",
505
+
"page",
506
+
"pager",
507
+
"pages",
508
+
"paid",
509
+
"panel",
510
+
"partner",
511
+
"partnerpage",
512
+
"partners",
513
+
"password",
514
+
"patch",
515
+
"pay",
516
+
"payment",
517
+
"people",
518
+
"perl",
519
+
"person",
520
+
"phone",
521
+
"photo",
522
+
"photoalbum",
523
+
"photos",
524
+
"php",
525
+
"phpmyadmin",
526
+
"phppgadmin",
527
+
"phpredisadmin",
528
+
"pic",
529
+
"pics",
530
+
"picture",
531
+
"pictures",
532
+
"ping",
533
+
"pixel",
534
+
"places",
535
+
"plan",
536
+
"plans",
537
+
"plugin",
538
+
"plugins",
539
+
"podcasts",
540
+
"policies",
541
+
"policy",
542
+
"pop",
543
+
"pop3",
544
+
"popular",
545
+
"porn",
546
+
"portal",
547
+
"portals",
548
+
"post",
549
+
"postfix",
550
+
"postmaster",
551
+
"posts",
552
+
"pr",
553
+
"pr0n",
554
+
"premium",
555
+
"press",
556
+
"price",
557
+
"pricing",
558
+
"principles",
559
+
"print",
560
+
"privacy",
561
+
"privacy-policy",
562
+
"privacypolicy",
563
+
"privacy_policy",
564
+
"private",
565
+
"prod",
566
+
"product",
567
+
"production",
568
+
"products",
569
+
"profile",
570
+
"profiles",
571
+
"project",
572
+
"projects",
573
+
"promo",
574
+
"promotions",
575
+
"proxies",
576
+
"proxy",
577
+
"pub",
578
+
"public",
579
+
"purchase",
580
+
"purpose",
581
+
"put",
582
+
"python",
583
+
"queries",
584
+
"query",
585
+
"radio",
586
+
"random",
587
+
"ranking",
588
+
"read",
589
+
"reader",
590
+
"readme",
591
+
"recent",
592
+
"recruit",
593
+
"recruitment",
594
+
"redirect",
595
+
"register",
596
+
"registration",
597
+
"release",
598
+
"remove",
599
+
"replies",
600
+
"report",
601
+
"reports",
602
+
"repositories",
603
+
"repository",
604
+
"req",
605
+
"request",
606
+
"requests",
607
+
"research",
608
+
"reset",
609
+
"resolve",
610
+
"resolver",
611
+
"review",
612
+
"rnail",
613
+
"rnicrosoft",
614
+
"roc",
615
+
"root",
616
+
"rss",
617
+
"ruby",
618
+
"rule",
619
+
"sag",
620
+
"sale",
621
+
"sales",
622
+
"sample",
623
+
"samples",
624
+
"sandbox",
625
+
"save",
626
+
"scholar",
627
+
"school",
628
+
"schools",
629
+
"script",
630
+
"scripts",
631
+
"search",
632
+
"secure",
633
+
"security",
634
+
"self",
635
+
"seminars",
636
+
"send",
637
+
"server",
638
+
"server-info",
639
+
"server_info",
640
+
"server-status",
641
+
"server_status",
642
+
"servers",
643
+
"service",
644
+
"services",
645
+
"session",
646
+
"sessions",
647
+
"setting",
648
+
"settings",
649
+
"setup",
650
+
"share",
651
+
"shop",
652
+
"shopping",
653
+
"shortcut",
654
+
"shortcuts",
655
+
"show",
656
+
"sign-in",
657
+
"sign-up",
658
+
"signin",
659
+
"signout",
660
+
"signup",
661
+
"sign_in",
662
+
"sign_up",
663
+
"site",
664
+
"sitemap",
665
+
"sitemaps",
666
+
"sitenews",
667
+
"sites",
668
+
"sketchup",
669
+
"sky",
670
+
"slash",
671
+
"slashinvoice",
672
+
"slut",
673
+
"smartphone",
674
+
"sms",
675
+
"smtp",
676
+
"soap",
677
+
"software",
678
+
"sorry",
679
+
"source",
680
+
"spec",
681
+
"special",
682
+
"spreadsheet",
683
+
"spreadsheets",
684
+
"sql",
685
+
"src",
686
+
"srntp",
687
+
"ssh",
688
+
"ssl",
689
+
"ssladmin",
690
+
"ssladministrator",
691
+
"sslwebmaster",
692
+
"ssytem",
693
+
"staff",
694
+
"stage",
695
+
"staging",
696
+
"start",
697
+
"stat",
698
+
"state",
699
+
"static",
700
+
"statistics",
701
+
"stats",
702
+
"status",
703
+
"store",
704
+
"stores",
705
+
"stories",
706
+
"style",
707
+
"styleguide",
708
+
"styles",
709
+
"stylesheet",
710
+
"stylesheets",
711
+
"subdomain",
712
+
"subscribe",
713
+
"subscription",
714
+
"subscriptions",
715
+
"suggest",
716
+
"suggestqueries",
717
+
"support",
718
+
"survey",
719
+
"surveys",
720
+
"surveytool",
721
+
"svn",
722
+
"swf",
723
+
"syn",
724
+
"sync",
725
+
"syndicated",
726
+
"sys",
727
+
"sysadmin",
728
+
"sysadministrator",
729
+
"sysadmins",
730
+
"system",
731
+
"tablet",
732
+
"tablets",
733
+
"tag",
734
+
"tags",
735
+
"talk",
736
+
"talkgadget",
737
+
"task",
738
+
"tasks",
739
+
"team",
740
+
"teams",
741
+
"tech",
742
+
"telnet",
743
+
"term",
744
+
"terms",
745
+
"terms-of-service",
746
+
"termsofservice",
747
+
"terms_of_service",
748
+
"test",
749
+
"testing",
750
+
"tests",
751
+
"text",
752
+
"theme",
753
+
"themes",
754
+
"thread",
755
+
"threads",
756
+
"ticket",
757
+
"tickets",
758
+
"tmp",
759
+
"todo",
760
+
"to-do",
761
+
"to_do",
762
+
"toml",
763
+
"tool",
764
+
"toolbar",
765
+
"toolbars",
766
+
"tools",
767
+
"top",
768
+
"topic",
769
+
"topics",
770
+
"tos",
771
+
"tour",
772
+
"trac",
773
+
"translate",
774
+
"trace",
775
+
"translation",
776
+
"translations",
777
+
"translator",
778
+
"trends",
779
+
"tutorial",
780
+
"tux",
781
+
"tv",
782
+
"twitter",
783
+
"txt",
784
+
"ul",
785
+
"undef",
786
+
"unfollow",
787
+
"unsubscribe",
788
+
"update",
789
+
"updates",
790
+
"upgrade",
791
+
"upgrades",
792
+
"upi",
793
+
"upload",
794
+
"uploads",
795
+
"url",
796
+
"usage",
797
+
"user",
798
+
"username",
799
+
"usernames",
800
+
"users",
801
+
"uuid",
802
+
"validation",
803
+
"validations",
804
+
"ver",
805
+
"version",
806
+
"video",
807
+
"videos",
808
+
"video-stats",
809
+
"visitor",
810
+
"visitors",
811
+
"voice",
812
+
"volunteer",
813
+
"volunteers",
814
+
"w",
815
+
"watch",
816
+
"wave",
817
+
"weather",
818
+
"web",
819
+
"webdisk",
820
+
"webhook",
821
+
"webhooks",
822
+
"webmail",
823
+
"webmaster",
824
+
"webmasters",
825
+
"webrnail",
826
+
"website",
827
+
"websites",
828
+
"welcome",
829
+
"whm",
830
+
"whois",
831
+
"widget",
832
+
"widgets",
833
+
"wifi",
834
+
"wiki",
835
+
"wikis",
836
+
"win",
837
+
"windows",
838
+
"word",
839
+
"work",
840
+
"works",
841
+
"workshop",
842
+
"wpad",
843
+
"ww",
844
+
"wws",
845
+
"www",
846
+
"wwws",
847
+
"wwww",
848
+
"xfn",
849
+
"xhtml",
850
+
"xhtrnl",
851
+
"xml",
852
+
"xmpp",
853
+
"xpg",
854
+
"xxx",
855
+
"yaml",
856
+
"year",
857
+
"yml",
858
+
"you",
859
+
"yourdomain",
860
+
"yourname",
861
+
"yoursite",
862
+
"yourusername",
863
+
];
864
+
865
+
const FAMOUS_ACCOUNTS: &[&str] = &[
866
+
"10ronaldinho",
867
+
"3gerardpique",
868
+
"aclu",
869
+
"adele",
870
+
"akshaykumar",
871
+
"aliaa08",
872
+
"aliciakeys",
873
+
"amitshah",
874
+
"andresiniesta8",
875
+
"anushkasharma",
876
+
"arianagrande",
877
+
"arrahman",
878
+
"arvindkejriwal",
879
+
"avrillavigne",
880
+
"barackobama",
881
+
"bbcbreaking",
882
+
"bbcworld",
883
+
"beingsalmankhan",
884
+
"billgates",
885
+
"britneyspears",
886
+
"brunomars",
887
+
"bts_bighit",
888
+
"bts_twt",
889
+
"championsleague",
890
+
"chrisbrown",
891
+
"cnnbrk",
892
+
"coldplay",
893
+
"conanobrien",
894
+
"cristiano",
895
+
"danieltosh",
896
+
"davidguetta",
897
+
"ddlovato",
898
+
"deepikapadukone",
899
+
"drake",
900
+
"elisapie",
901
+
"ellendegeneres",
902
+
"elonmusk",
903
+
"eminem",
904
+
"emmawatson",
905
+
"fcbarcelona",
906
+
"foxnews",
907
+
"harry_styles",
908
+
"hillaryclinton",
909
+
"iamsrk",
910
+
"ihrithik",
911
+
"imvkohli",
912
+
"instagram",
913
+
"jimmyfallon",
914
+
"jlo",
915
+
"joebiden",
916
+
"jtimberlake",
917
+
"justinbieber",
918
+
"kaka",
919
+
"kanyewest",
920
+
"katyperry",
921
+
"kendalljenner",
922
+
"kevinhart4real",
923
+
"khloekardashian",
924
+
"kimkardashian",
925
+
"kingjames",
926
+
"kourtneykardash",
927
+
"kyliejenner",
928
+
"ladygaga",
929
+
"liampayne",
930
+
"liltunechi",
931
+
"manutd",
932
+
"mariahcarey",
933
+
"mileycyrus",
934
+
"mohamadalarefe",
935
+
"narendramodi",
936
+
"nasa",
937
+
"nba",
938
+
"neymarjr",
939
+
"nfl",
940
+
"niallofficial",
941
+
"nickiminaj",
942
+
"npr",
943
+
"nytimes",
944
+
"onedirection",
945
+
"oprah",
946
+
"pink",
947
+
"pitbull",
948
+
"playstation",
949
+
"pmoindia",
950
+
"premierleague",
951
+
"priyankachopra",
952
+
"realdonaldtrump",
953
+
"ricky_martin",
954
+
"rihanna",
955
+
"sachin_rt",
956
+
"selenagomez",
957
+
"shakira",
958
+
"shawnmendes",
959
+
"sportscenter",
960
+
"srbachchan",
961
+
"subhisharma100",
962
+
"taylorswift13",
963
+
"theeconomist",
964
+
"twitter",
965
+
"virendersehwag",
966
+
"whitehouse45",
967
+
"wizkhalifa",
968
+
"youtube",
969
+
"zaynmalik",
970
+
"beyonce",
971
+
"billieeilish",
972
+
"leomessi",
973
+
"natgeo",
974
+
"nike",
975
+
"snoopdogg",
976
+
"taylorswift",
977
+
"therock",
978
+
"10downingstreet",
979
+
"aoc",
980
+
"carterjwm",
981
+
"dril",
982
+
"gretathunberg",
983
+
"kamalaharris",
984
+
"kremlinrussia_e",
985
+
"potus",
986
+
"rondesantisfl",
987
+
"ukraine",
988
+
"washingtonpost",
989
+
"yousuck2020",
990
+
"zelenskyyua",
991
+
"akiko_lawson",
992
+
"ariyoshihiroiki",
993
+
"asahi",
994
+
"dozle_official",
995
+
"famima_now",
996
+
"ff_xiv_jp",
997
+
"fujitv",
998
+
"gigazine",
999
+
"hajimesyacho",
1000
+
"hikakin",
1001
+
"jocx",
1002
+
"jotx",
1003
+
"kiyo_saiore",
1004
+
"mainichi",
1005
+
"matsu_bouzu",
1006
+
"naomiosaka",
1007
+
"nhk",
1008
+
"nikkei",
1009
+
"nintendo",
1010
+
"ntv",
1011
+
"oowareware1945",
1012
+
"pamyurin",
1013
+
"poke_times",
1014
+
"rolaworld",
1015
+
"seikintv",
1016
+
"starbucksjapan",
1017
+
"tbs",
1018
+
"tbs_pr",
1019
+
"tvasahi",
1020
+
"tvtokyo",
1021
+
"yokoono",
1022
+
"yomiuri_online",
1023
+
"brasildefato",
1024
+
"claudialeitte",
1025
+
"correio",
1026
+
"em_com",
1027
+
"estadao",
1028
+
"folha",
1029
+
"gazetadopovo",
1030
+
"ivetesangalo",
1031
+
"jairbolsonaro",
1032
+
"jornaldobrasil",
1033
+
"jornaloglobo",
1034
+
"lucianohuck",
1035
+
"lulaoficial",
1036
+
"marcosmion",
1037
+
"paulocoelho",
1038
+
"portalr7",
1039
+
"rede_globo",
1040
+
"zerohora",
1041
+
];
1042
+
1043
+
pub static RESERVED_SUBDOMAINS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
1044
+
let mut set = HashSet::with_capacity(
1045
+
ATP_SPECIFIC.len() + COMMONLY_RESERVED.len() + FAMOUS_ACCOUNTS.len(),
1046
+
);
1047
+
for s in ATP_SPECIFIC {
1048
+
set.insert(*s);
1049
+
}
1050
+
for s in COMMONLY_RESERVED {
1051
+
set.insert(*s);
1052
+
}
1053
+
for s in FAMOUS_ACCOUNTS {
1054
+
set.insert(*s);
1055
+
}
1056
+
set
1057
+
});
1058
+
1059
+
pub fn is_reserved_subdomain(subdomain: &str) -> bool {
1060
+
RESERVED_SUBDOMAINS.contains(subdomain.to_lowercase().as_str())
1061
+
}
1062
+
1063
+
#[cfg(test)]
1064
+
mod tests {
1065
+
use super::*;
1066
+
1067
+
#[test]
1068
+
fn test_atp_specific_reserved() {
1069
+
assert!(is_reserved_subdomain("admin"));
1070
+
assert!(is_reserved_subdomain("api"));
1071
+
assert!(is_reserved_subdomain("bsky"));
1072
+
assert!(is_reserved_subdomain("plc"));
1073
+
assert!(is_reserved_subdomain("xrpc"));
1074
+
}
1075
+
1076
+
#[test]
1077
+
fn test_famous_accounts_reserved() {
1078
+
assert!(is_reserved_subdomain("barackobama"));
1079
+
assert!(is_reserved_subdomain("elonmusk"));
1080
+
assert!(is_reserved_subdomain("taylorswift"));
1081
+
assert!(is_reserved_subdomain("nintendo"));
1082
+
}
1083
+
1084
+
#[test]
1085
+
fn test_case_insensitive() {
1086
+
assert!(is_reserved_subdomain("ADMIN"));
1087
+
assert!(is_reserved_subdomain("Admin"));
1088
+
assert!(is_reserved_subdomain("BARACKOBAMA"));
1089
+
}
1090
+
1091
+
#[test]
1092
+
fn test_not_reserved() {
1093
+
assert!(!is_reserved_subdomain("alice"));
1094
+
assert!(!is_reserved_subdomain("bob123"));
1095
+
assert!(!is_reserved_subdomain("randomuser"));
1096
+
}
1097
+
}
+1
src/oauth/endpoints/metadata.rs
+1
src/oauth/endpoints/metadata.rs
+39
-62
src/sync/deprecated.rs
+39
-62
src/sync/deprecated.rs
···
1
+
use crate::auth::{extract_bearer_token_from_header, validate_bearer_token_allow_takendown};
1
2
use crate::state::AppState;
2
3
use crate::sync::car::encode_car_header;
4
+
use crate::sync::util::assert_repo_availability;
3
5
use axum::{
4
6
Json,
5
7
extract::{Query, State},
6
-
http::StatusCode,
8
+
http::{HeaderMap, StatusCode},
7
9
response::{IntoResponse, Response},
8
10
};
9
11
use cid::Cid;
···
13
15
use serde_json::json;
14
16
use std::io::Write;
15
17
use std::str::FromStr;
16
-
use tracing::error;
17
18
18
19
const MAX_REPO_BLOCKS_TRAVERSAL: usize = 20_000;
19
20
21
+
async fn check_admin_or_self(state: &AppState, headers: &HeaderMap, did: &str) -> bool {
22
+
let token = match extract_bearer_token_from_header(
23
+
headers.get("Authorization").and_then(|h| h.to_str().ok()),
24
+
) {
25
+
Some(t) => t,
26
+
None => return false,
27
+
};
28
+
match validate_bearer_token_allow_takendown(&state.db, &token).await {
29
+
Ok(auth_user) => auth_user.is_admin || auth_user.did == did,
30
+
Err(_) => false,
31
+
}
32
+
}
33
+
20
34
#[derive(Deserialize)]
21
35
pub struct GetHeadParams {
22
36
pub did: String,
···
29
43
30
44
pub async fn get_head(
31
45
State(state): State<AppState>,
46
+
headers: HeaderMap,
32
47
Query(params): Query<GetHeadParams>,
33
48
) -> Response {
34
49
let did = params.did.trim();
···
39
54
)
40
55
.into_response();
41
56
}
42
-
let result = sqlx::query!(
43
-
r#"
44
-
SELECT r.repo_root_cid
45
-
FROM repos r
46
-
JOIN users u ON r.user_id = u.id
47
-
WHERE u.did = $1
48
-
"#,
49
-
did
50
-
)
51
-
.fetch_optional(&state.db)
52
-
.await;
53
-
match result {
54
-
Ok(Some(row)) => (
55
-
StatusCode::OK,
56
-
Json(GetHeadOutput {
57
-
root: row.repo_root_cid,
58
-
}),
59
-
)
60
-
.into_response(),
61
-
Ok(None) => (
57
+
let is_admin_or_self = check_admin_or_self(&state, &headers, did).await;
58
+
let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await {
59
+
Ok(a) => a,
60
+
Err(e) => return e.into_response(),
61
+
};
62
+
match account.repo_root_cid {
63
+
Some(root) => (StatusCode::OK, Json(GetHeadOutput { root })).into_response(),
64
+
None => (
62
65
StatusCode::BAD_REQUEST,
63
-
Json(json!({"error": "HeadNotFound", "message": "Could not find root for DID"})),
66
+
Json(json!({"error": "HeadNotFound", "message": format!("Could not find root for DID: {}", did)})),
64
67
)
65
68
.into_response(),
66
-
Err(e) => {
67
-
error!("DB error in get_head: {:?}", e);
68
-
(
69
-
StatusCode::INTERNAL_SERVER_ERROR,
70
-
Json(json!({"error": "InternalError"})),
71
-
)
72
-
.into_response()
73
-
}
74
69
}
75
70
}
76
71
···
81
76
82
77
pub async fn get_checkout(
83
78
State(state): State<AppState>,
79
+
headers: HeaderMap,
84
80
Query(params): Query<GetCheckoutParams>,
85
81
) -> Response {
86
82
let did = params.did.trim();
···
91
87
)
92
88
.into_response();
93
89
}
94
-
let repo_row = sqlx::query!(
95
-
r#"
96
-
SELECT r.repo_root_cid
97
-
FROM repos r
98
-
JOIN users u ON u.id = r.user_id
99
-
WHERE u.did = $1
100
-
"#,
101
-
did
102
-
)
103
-
.fetch_optional(&state.db)
104
-
.await
105
-
.unwrap_or(None);
106
-
let head_str = match repo_row {
107
-
Some(r) => r.repo_root_cid,
90
+
let is_admin_or_self = check_admin_or_self(&state, &headers, did).await;
91
+
let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await {
92
+
Ok(a) => a,
93
+
Err(e) => return e.into_response(),
94
+
};
95
+
let head_str = match account.repo_root_cid {
96
+
Some(r) => r,
108
97
None => {
109
-
let user_exists = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
110
-
.fetch_optional(&state.db)
111
-
.await
112
-
.unwrap_or(None);
113
-
if user_exists.is_none() {
114
-
return (
115
-
StatusCode::NOT_FOUND,
116
-
Json(json!({"error": "RepoNotFound", "message": "Repo not found"})),
117
-
)
118
-
.into_response();
119
-
} else {
120
-
return (
121
-
StatusCode::NOT_FOUND,
122
-
Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})),
123
-
)
124
-
.into_response();
125
-
}
98
+
return (
99
+
StatusCode::BAD_REQUEST,
100
+
Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})),
101
+
)
102
+
.into_response();
126
103
}
127
104
};
128
105
let head_cid = match Cid::from_str(&head_str) {
+3
-3
tests/account_lifecycle.rs
+3
-3
tests/account_lifecycle.rs
···
154
154
let client = client();
155
155
let base = base_url().await;
156
156
157
-
let handle = format!("diddoctest-{}", uuid::Uuid::new_v4());
157
+
let handle = format!("dd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
158
158
let payload = json!({
159
159
"handle": handle,
160
160
"email": format!("{}@example.com", handle),
···
185
185
let client = client();
186
186
let base = base_url().await;
187
187
188
-
let handle = format!("tokentest-{}", uuid::Uuid::new_v4());
188
+
let handle = format!("tt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
189
189
let payload = json!({
190
190
"handle": handle,
191
191
"email": format!("{}@example.com", handle),
···
243
243
let client = client();
244
244
let base = base_url().await;
245
245
246
-
let handle = format!("pwdlentest-{}", uuid::Uuid::new_v4());
246
+
let handle = format!("pl{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
247
247
let payload = json!({
248
248
"handle": handle,
249
249
"email": format!("{}@example.com", handle),
+105
-4
tests/actor.rs
+105
-4
tests/actor.rs
···
140
140
}
141
141
142
142
#[tokio::test]
143
-
async fn test_put_preferences_read_only_rejected() {
143
+
async fn test_put_preferences_read_only_silently_filtered() {
144
144
let client = client();
145
145
let base = base_url().await;
146
146
let (token, _did) = create_account_and_login(&client).await;
···
149
149
{
150
150
"$type": "app.bsky.actor.defs#declaredAgePref",
151
151
"isOverAge18": true
152
+
},
153
+
{
154
+
"$type": "app.bsky.actor.defs#adultContentPref",
155
+
"enabled": true
152
156
}
153
157
]
154
158
});
···
159
163
.send()
160
164
.await
161
165
.unwrap();
162
-
assert_eq!(resp.status(), 400);
163
-
let body: Value = resp.json().await.unwrap();
164
-
assert_eq!(body["error"], "InvalidRequest");
166
+
assert_eq!(resp.status(), 200);
167
+
let get_resp = client
168
+
.get(format!("{}/xrpc/app.bsky.actor.getPreferences", base))
169
+
.header("Authorization", format!("Bearer {}", token))
170
+
.send()
171
+
.await
172
+
.unwrap();
173
+
assert_eq!(get_resp.status(), 200);
174
+
let body: Value = get_resp.json().await.unwrap();
175
+
let prefs_arr = body["preferences"].as_array().unwrap();
176
+
assert_eq!(prefs_arr.len(), 1);
177
+
assert_eq!(prefs_arr[0]["$type"], "app.bsky.actor.defs#adultContentPref");
165
178
}
166
179
167
180
#[tokio::test]
···
328
341
let body: Value = resp.json().await.unwrap();
329
342
assert!(body["preferences"].as_array().unwrap().is_empty());
330
343
}
344
+
345
+
#[tokio::test]
346
+
async fn test_declared_age_pref_computed_from_birth_date() {
347
+
let client = client();
348
+
let base = base_url().await;
349
+
let (token, _did) = create_account_and_login(&client).await;
350
+
let prefs = json!({
351
+
"preferences": [
352
+
{
353
+
"$type": "app.bsky.actor.defs#personalDetailsPref",
354
+
"birthDate": "1990-01-15"
355
+
}
356
+
]
357
+
});
358
+
let resp = client
359
+
.post(format!("{}/xrpc/app.bsky.actor.putPreferences", base))
360
+
.header("Authorization", format!("Bearer {}", token))
361
+
.json(&prefs)
362
+
.send()
363
+
.await
364
+
.unwrap();
365
+
assert_eq!(resp.status(), 200);
366
+
let get_resp = client
367
+
.get(format!("{}/xrpc/app.bsky.actor.getPreferences", base))
368
+
.header("Authorization", format!("Bearer {}", token))
369
+
.send()
370
+
.await
371
+
.unwrap();
372
+
assert_eq!(get_resp.status(), 200);
373
+
let body: Value = get_resp.json().await.unwrap();
374
+
let prefs_arr = body["preferences"].as_array().unwrap();
375
+
assert_eq!(prefs_arr.len(), 2);
376
+
let personal_details = prefs_arr
377
+
.iter()
378
+
.find(|p| p["$type"] == "app.bsky.actor.defs#personalDetailsPref");
379
+
assert!(personal_details.is_some());
380
+
assert_eq!(personal_details.unwrap()["birthDate"], "1990-01-15");
381
+
let declared_age = prefs_arr
382
+
.iter()
383
+
.find(|p| p["$type"] == "app.bsky.actor.defs#declaredAgePref");
384
+
assert!(declared_age.is_some());
385
+
let declared_age = declared_age.unwrap();
386
+
assert_eq!(declared_age["isOverAge13"], true);
387
+
assert_eq!(declared_age["isOverAge16"], true);
388
+
assert_eq!(declared_age["isOverAge18"], true);
389
+
}
390
+
391
+
#[tokio::test]
392
+
async fn test_declared_age_pref_computed_under_18() {
393
+
let client = client();
394
+
let base = base_url().await;
395
+
let (token, _did) = create_account_and_login(&client).await;
396
+
let current_year = chrono::Utc::now().format("%Y").to_string().parse::<i32>().unwrap();
397
+
let birth_year = current_year - 15;
398
+
let prefs = json!({
399
+
"preferences": [
400
+
{
401
+
"$type": "app.bsky.actor.defs#personalDetailsPref",
402
+
"birthDate": format!("{}-06-15", birth_year)
403
+
}
404
+
]
405
+
});
406
+
let resp = client
407
+
.post(format!("{}/xrpc/app.bsky.actor.putPreferences", base))
408
+
.header("Authorization", format!("Bearer {}", token))
409
+
.json(&prefs)
410
+
.send()
411
+
.await
412
+
.unwrap();
413
+
assert_eq!(resp.status(), 200);
414
+
let get_resp = client
415
+
.get(format!("{}/xrpc/app.bsky.actor.getPreferences", base))
416
+
.header("Authorization", format!("Bearer {}", token))
417
+
.send()
418
+
.await
419
+
.unwrap();
420
+
assert_eq!(get_resp.status(), 200);
421
+
let body: Value = get_resp.json().await.unwrap();
422
+
let prefs_arr = body["preferences"].as_array().unwrap();
423
+
let declared_age = prefs_arr
424
+
.iter()
425
+
.find(|p| p["$type"] == "app.bsky.actor.defs#declaredAgePref");
426
+
assert!(declared_age.is_some());
427
+
let declared_age = declared_age.unwrap();
428
+
assert_eq!(declared_age["isOverAge13"], true);
429
+
assert_eq!(declared_age["isOverAge16"], false);
430
+
assert_eq!(declared_age["isOverAge18"], false);
431
+
}
+6
-6
tests/banned_words.rs
+6
-6
tests/banned_words.rs
···
31
31
#[tokio::test]
32
32
async fn test_handle_with_slur_rejected() {
33
33
let client = client();
34
-
let timestamp = chrono::Utc::now().timestamp_millis();
34
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
35
35
let slur = decode("bmlnZ2Vy");
36
-
let offensive_handle = format!("{}{}", slur, timestamp);
36
+
let offensive_handle = format!("{}{}", slur, suffix);
37
37
38
38
let create_payload = json!({
39
39
"handle": offensive_handle,
40
-
"email": format!("test{}@example.com", timestamp),
40
+
"email": format!("test{}@example.com", suffix),
41
41
"password": "TestPassword123!"
42
42
});
43
43
···
65
65
#[tokio::test]
66
66
async fn test_handle_with_normalized_slur_rejected() {
67
67
let client = client();
68
-
let timestamp = chrono::Utc::now().timestamp_millis();
68
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..6];
69
69
let slur = decode("bi1pLWctZy1lLXI=");
70
-
let offensive_handle = format!("{}{}", slur, timestamp);
70
+
let offensive_handle = format!("{}{}", slur, suffix);
71
71
72
72
let create_payload = json!({
73
73
"handle": offensive_handle,
74
-
"email": format!("test{}@example.com", timestamp),
74
+
"email": format!("test{}@example.com", suffix),
75
75
"password": "TestPassword123!"
76
76
});
77
77
+1
-1
tests/common/mod.rs
+1
-1
tests/common/mod.rs
···
440
440
if attempt > 0 {
441
441
tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await;
442
442
}
443
-
let handle = format!("user-{}", uuid::Uuid::new_v4());
443
+
let handle = format!("u{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
444
444
let payload = json!({
445
445
"handle": handle,
446
446
"email": format!("{}@example.com", handle),
+7
-7
tests/did_web.rs
+7
-7
tests/did_web.rs
···
11
11
#[tokio::test]
12
12
async fn test_create_self_hosted_did_web() {
13
13
let client = client();
14
-
let handle = format!("selfweb-{}", uuid::Uuid::new_v4());
14
+
let handle = format!("sw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
15
15
let payload = json!({
16
16
"handle": handle,
17
17
"email": format!("{}@example.com", handle),
···
98
98
let mock_uri = mock_server.uri();
99
99
let mock_addr = mock_uri.trim_start_matches("http://");
100
100
let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
101
-
let handle = format!("extweb-{}", uuid::Uuid::new_v4());
101
+
let handle = format!("xw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
102
102
let pds_endpoint = base_url().await.replace("http://", "https://");
103
103
104
104
let reserve_res = client
···
180
180
#[tokio::test]
181
181
async fn test_plc_operations_blocked_for_did_web() {
182
182
let client = client();
183
-
let handle = format!("plcblock-{}", uuid::Uuid::new_v4());
183
+
let handle = format!("pb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
184
184
let payload = json!({
185
185
"handle": handle,
186
186
"email": format!("{}@example.com", handle),
···
245
245
#[tokio::test]
246
246
async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() {
247
247
let client = client();
248
-
let handle = format!("creds-{}", uuid::Uuid::new_v4());
248
+
let handle = format!("cr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
249
249
let payload = json!({
250
250
"handle": handle,
251
251
"email": format!("{}@example.com", handle),
···
294
294
#[tokio::test]
295
295
async fn test_did_plc_still_works_with_did_type_param() {
296
296
let client = client();
297
-
let handle = format!("plctype-{}", uuid::Uuid::new_v4());
297
+
let handle = format!("pt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
298
298
let payload = json!({
299
299
"handle": handle,
300
300
"email": format!("{}@example.com", handle),
···
323
323
#[tokio::test]
324
324
async fn test_external_did_web_requires_did_field() {
325
325
let client = client();
326
-
let handle = format!("nodid-{}", uuid::Uuid::new_v4());
326
+
let handle = format!("nd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
327
327
let payload = json!({
328
328
"handle": handle,
329
329
"email": format!("{}@example.com", handle),
···
392
392
mock_addr.replace(":", "%3A"),
393
393
unique_id
394
394
);
395
-
let handle = format!("byod-{}", uuid::Uuid::new_v4());
395
+
let handle = format!("by{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
396
396
let pds_endpoint = base_url().await.replace("http://", "https://");
397
397
let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://"));
398
398
+12
-12
tests/email_update.rs
+12
-12
tests/email_update.rs
···
57
57
async fn test_request_email_update_returns_token_required() {
58
58
let client = common::client();
59
59
let base_url = common::base_url().await;
60
-
let handle = format!("emailreq-{}", uuid::Uuid::new_v4());
60
+
let handle = format!("er{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
61
61
let email = format!("{}@example.com", handle);
62
62
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
63
63
···
80
80
let client = common::client();
81
81
let base_url = common::base_url().await;
82
82
let pool = common::get_test_db_pool().await;
83
-
let handle = format!("emailup-{}", uuid::Uuid::new_v4());
83
+
let handle = format!("eu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
84
84
let email = format!("{}@example.com", handle);
85
85
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
86
86
let new_email = format!("new_{}@example.com", handle);
···
123
123
async fn test_update_email_requires_token_when_verified() {
124
124
let client = common::client();
125
125
let base_url = common::base_url().await;
126
-
let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4());
126
+
let handle = format!("ed{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
127
127
let email = format!("{}@example.com", handle);
128
128
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
129
129
let new_email = format!("direct_{}@example.com", handle);
···
144
144
async fn test_update_email_same_email_noop() {
145
145
let client = common::client();
146
146
let base_url = common::base_url().await;
147
-
let handle = format!("emailup-same-{}", uuid::Uuid::new_v4());
147
+
let handle = format!("es{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
148
148
let email = format!("{}@example.com", handle);
149
149
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
150
150
···
166
166
async fn test_update_email_invalid_token() {
167
167
let client = common::client();
168
168
let base_url = common::base_url().await;
169
-
let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4());
169
+
let handle = format!("eb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
170
170
let email = format!("{}@example.com", handle);
171
171
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
172
172
let new_email = format!("badtok_{}@example.com", handle);
···
217
217
async fn test_update_email_invalid_format() {
218
218
let client = common::client();
219
219
let base_url = common::base_url().await;
220
-
let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4());
220
+
let handle = format!("ef{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
221
221
let email = format!("{}@example.com", handle);
222
222
let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
223
223
···
236
236
let client = common::client();
237
237
let base_url = common::base_url().await;
238
238
let pool = common::get_test_db_pool().await;
239
-
let handle = format!("emailconfirm-{}", uuid::Uuid::new_v4());
239
+
let handle = format!("ec{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
240
240
let email = format!("{}@example.com", handle);
241
241
242
242
let res = client
···
298
298
let client = common::client();
299
299
let base_url = common::base_url().await;
300
300
let pool = common::get_test_db_pool().await;
301
-
let handle = format!("emailconf-wrong-{}", uuid::Uuid::new_v4());
301
+
let handle = format!("ew{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
302
302
let email = format!("{}@example.com", handle);
303
303
304
304
let res = client
···
352
352
async fn test_confirm_email_invalid_token() {
353
353
let client = common::client();
354
354
let base_url = common::base_url().await;
355
-
let handle = format!("emailconf-inv-{}", uuid::Uuid::new_v4());
355
+
let handle = format!("ei{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
356
356
let email = format!("{}@example.com", handle);
357
357
358
358
let res = client
···
392
392
let client = common::client();
393
393
let base_url = common::base_url().await;
394
394
let pool = common::get_test_db_pool().await;
395
-
let handle = format!("emailup-unverified-{}", uuid::Uuid::new_v4());
395
+
let handle = format!("ev{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
396
396
let email = format!("{}@example.com", handle);
397
397
398
398
let res = client
···
457
457
let base_url = common::base_url().await;
458
458
let pool = common::get_test_db_pool().await;
459
459
460
-
let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4());
460
+
let handle1 = format!("d1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
461
461
let email1 = format!("{}@example.com", handle1);
462
462
let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
463
463
464
-
let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4());
464
+
let handle2 = format!("d2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
465
465
let email2 = format!("{}@example.com", handle2);
466
466
let (access_jwt2, did2) = create_verified_account(&client, &base_url, &handle2, &email2).await;
467
467
+4
-4
tests/identity.rs
+4
-4
tests/identity.rs
···
8
8
#[tokio::test]
9
9
async fn test_resolve_handle_success() {
10
10
let client = client();
11
-
let short_handle = format!("resolvetest-{}", uuid::Uuid::new_v4());
11
+
let short_handle = format!("rt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
12
12
let payload = json!({
13
13
"handle": short_handle,
14
14
"email": format!("{}@example.com", short_handle),
···
98
98
let mock_uri = mock_server.uri();
99
99
let mock_addr = mock_uri.trim_start_matches("http://");
100
100
let did = format!("did:web:{}", mock_addr.replace(":", "%3A"));
101
-
let handle = format!("webuser-{}", uuid::Uuid::new_v4());
101
+
let handle = format!("wu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
102
102
let pds_endpoint = base_url().await.replace("http://", "https://");
103
103
104
104
let reserve_res = client
···
183
183
#[tokio::test]
184
184
async fn test_create_account_duplicate_handle() {
185
185
let client = client();
186
-
let handle = format!("dupe-{}", uuid::Uuid::new_v4());
186
+
let handle = format!("dp{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
187
187
let email = format!("{}@example.com", handle);
188
188
let payload = json!({
189
189
"handle": handle,
···
220
220
let mock_server = MockServer::start().await;
221
221
let mock_uri = mock_server.uri();
222
222
let mock_addr = mock_uri.trim_start_matches("http://");
223
-
let handle = format!("lifecycle-{}", uuid::Uuid::new_v4());
223
+
let handle = format!("lc{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
224
224
let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle);
225
225
let email = format!("{}@test.com", handle);
226
226
let pds_endpoint = base_url().await.replace("http://", "https://");
+3
-3
tests/jwt_security.rs
+3
-3
tests/jwt_security.rs
···
669
669
async fn test_refresh_token_replay_protection() {
670
670
let url = base_url().await;
671
671
let http_client = client();
672
-
let ts = Utc::now().timestamp_millis();
673
-
let handle = format!("rt-replay-jwt-{}", ts);
674
-
let email = format!("rt-replay-jwt-{}@example.com", ts);
672
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
673
+
let handle = format!("rr{}", suffix);
674
+
let email = format!("rr{}@example.com", suffix);
675
675
676
676
let create_res = http_client
677
677
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
+22
-22
tests/oauth.rs
+22
-22
tests/oauth.rs
···
191
191
async fn test_full_oauth_flow() {
192
192
let url = base_url().await;
193
193
let http_client = client();
194
-
let ts = Utc::now().timestamp_millis();
195
-
let handle = format!("oauth-test-{}", ts);
196
-
let email = format!("oauth-test-{}@example.com", ts);
194
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
195
+
let handle = format!("ot{}", suffix);
196
+
let email = format!("ot{}@example.com", suffix);
197
197
let password = "Oauthtest123!";
198
198
let create_res = http_client
199
199
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
···
209
209
let mock_client = setup_mock_client_metadata(redirect_uri).await;
210
210
let client_id = mock_client.uri();
211
211
let (code_verifier, code_challenge) = generate_pkce();
212
-
let state = format!("state-{}", ts);
212
+
let state = format!("state-{}", suffix);
213
213
let par_res = http_client
214
214
.post(format!("{}/oauth/par", url))
215
215
.form(&[
···
349
349
async fn test_oauth_error_cases() {
350
350
let url = base_url().await;
351
351
let http_client = client();
352
-
let ts = Utc::now().timestamp_millis();
353
-
let handle = format!("wrong-creds-{}", ts);
354
-
let email = format!("wrong-creds-{}@example.com", ts);
352
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
353
+
let handle = format!("wc{}", suffix);
354
+
let email = format!("wc{}@example.com", suffix);
355
355
http_client
356
356
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
357
357
.json(&json!({ "handle": handle, "email": email, "password": "Correct123!" }))
···
435
435
async fn test_oauth_2fa_flow() {
436
436
let url = base_url().await;
437
437
let http_client = client();
438
-
let ts = Utc::now().timestamp_millis();
439
-
let handle = format!("2fa-test-{}", ts);
440
-
let email = format!("2fa-test-{}@example.com", ts);
438
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
439
+
let handle = format!("ft{}", suffix);
440
+
let email = format!("ft{}@example.com", suffix);
441
441
let password = "Twofa123test!";
442
442
let create_res = http_client
443
443
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
···
557
557
async fn test_oauth_2fa_lockout() {
558
558
let url = base_url().await;
559
559
let http_client = client();
560
-
let ts = Utc::now().timestamp_millis();
561
-
let handle = format!("2fa-lockout-{}", ts);
562
-
let email = format!("2fa-lockout-{}@example.com", ts);
560
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
561
+
let handle = format!("fl{}", suffix);
562
+
let email = format!("fl{}@example.com", suffix);
563
563
let password = "Twofa123test!";
564
564
let create_res = http_client
565
565
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
···
649
649
async fn test_account_selector_with_2fa() {
650
650
let url = base_url().await;
651
651
let http_client = client();
652
-
let ts = Utc::now().timestamp_millis();
653
-
let handle = format!("selector-2fa-{}", ts);
654
-
let email = format!("selector-2fa-{}@example.com", ts);
652
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
653
+
let handle = format!("sf{}", suffix);
654
+
let email = format!("sf{}@example.com", suffix);
655
655
let password = "Selector2fa123!";
656
656
let create_res = http_client
657
657
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
···
835
835
async fn test_oauth_state_encoding() {
836
836
let url = base_url().await;
837
837
let http_client = client();
838
-
let ts = Utc::now().timestamp_millis();
839
-
let handle = format!("state-special-{}", ts);
840
-
let email = format!("state-special-{}@example.com", ts);
838
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
839
+
let handle = format!("ss{}", suffix);
840
+
let email = format!("ss{}@example.com", suffix);
841
841
let password = "State123special!";
842
842
let create_res = http_client
843
843
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
···
914
914
async fn get_oauth_token_with_scope(scope: &str) -> (String, String, String) {
915
915
let url = base_url().await;
916
916
let http_client = client();
917
-
let ts = Utc::now().timestamp_millis();
918
-
let handle = format!("scope-test-{}", ts);
919
-
let email = format!("scope-test-{}@example.com", ts);
917
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
918
+
let handle = format!("st{}", suffix);
919
+
let email = format!("st{}@example.com", suffix);
920
920
let password = "Scopetest123!";
921
921
let create_res = http_client
922
922
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
+10
-10
tests/oauth_lifecycle.rs
+10
-10
tests/oauth_lifecycle.rs
···
54
54
) -> (OAuthSession, MockServer) {
55
55
let url = base_url().await;
56
56
let http_client = client();
57
-
let ts = Utc::now().timestamp_millis();
58
-
let handle = format!("{}-{}", handle_prefix, ts);
59
-
let email = format!("{}-{}@example.com", handle_prefix, ts);
57
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..4];
58
+
let handle = format!("{}{}", handle_prefix, suffix);
59
+
let email = format!("{}{}@example.com", handle_prefix, suffix);
60
60
let password = format!("{}Pass123!", handle_prefix);
61
61
let create_res = http_client
62
62
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
···
269
269
let url = base_url().await;
270
270
let http_client = client();
271
271
let (session, _mock) =
272
-
create_user_and_oauth_session("oauth-lifecycle", "https://example.com/callback").await;
272
+
create_user_and_oauth_session("oauthlife", "https://example.com/callback").await;
273
273
let collection = "app.bsky.feed.post";
274
274
let original_text = "Original post content";
275
275
let create_res = http_client
···
439
439
let url = base_url().await;
440
440
let http_client = client();
441
441
let (session, _mock) =
442
-
create_user_and_oauth_session("oauth-refresh-access", "https://example.com/callback").await;
442
+
create_user_and_oauth_session("oauth-refr", "https://example.com/callback").await;
443
443
let collection = "app.bsky.feed.post";
444
444
let create_res = http_client
445
445
.post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
···
520
520
let url = base_url().await;
521
521
let http_client = client();
522
522
let (session, _mock) =
523
-
create_user_and_oauth_session("oauth-revoke-access", "https://example.com/callback").await;
523
+
create_user_and_oauth_session("oauth-revo", "https://example.com/callback").await;
524
524
let collection = "app.bsky.feed.post";
525
525
let create_res = http_client
526
526
.post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
···
574
574
async fn test_oauth_multiple_clients_same_user() {
575
575
let url = base_url().await;
576
576
let http_client = client();
577
-
let ts = Utc::now().timestamp_millis();
578
-
let handle = format!("multi-client-{}", ts);
579
-
let email = format!("multi-client-{}@example.com", ts);
577
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
578
+
let handle = format!("mc{}", suffix);
579
+
let email = format!("mc{}@example.com", suffix);
580
580
let password = "MultiClient123!";
581
581
let create_res = http_client
582
582
.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
···
949
949
let url = base_url().await;
950
950
let http_client = client();
951
951
let (alice, _mock_alice) =
952
-
create_user_and_oauth_session("alice-isolation", "https://alice.example.com/callback")
952
+
create_user_and_oauth_session("alice-isol", "https://alice.example.com/callback")
953
953
.await;
954
954
let (bob, _mock_bob) =
955
955
create_user_and_oauth_session("bob-isolation", "https://bob.example.com/callback").await;
+13
-13
tests/oauth_scopes.rs
+13
-13
tests/oauth_scopes.rs
···
58
58
) -> (OAuthSession, MockServer) {
59
59
let url = base_url().await;
60
60
let http_client = client();
61
-
let ts = Utc::now().timestamp_millis();
62
-
let handle = format!("{}-{}", handle_prefix, ts);
63
-
let email = format!("{}-{}@example.com", handle_prefix, ts);
61
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..4];
62
+
let handle = format!("{}{}", handle_prefix, suffix);
63
+
let email = format!("{}{}@example.com", handle_prefix, suffix);
64
64
let password = format!("{}Pass123!", handle_prefix);
65
65
66
66
let create_res = http_client
···
345
345
let url = base_url().await;
346
346
let http_client = client();
347
347
let (session, _mock) = create_user_and_oauth_session_with_scope(
348
-
"scope-transition",
348
+
"scope-trans",
349
349
"https://example.com/callback",
350
350
"atproto transition:generic",
351
351
)
···
380
380
let url = base_url().await;
381
381
let http_client = client();
382
382
383
-
let ts = Utc::now().timestamp_millis();
384
-
let handle = format!("consent-test-{}", ts);
385
-
let email = format!("consent-{}@example.com", ts);
383
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
384
+
let handle = format!("ct{}", suffix);
385
+
let email = format!("ct{}@example.com", suffix);
386
386
let password = "Consent123!";
387
387
let redirect_uri = "https://consent-test.example.com/callback";
388
388
···
476
476
let url = base_url().await;
477
477
let http_client = client();
478
478
479
-
let ts = Utc::now().timestamp_millis();
480
-
let handle = format!("consent-post-{}", ts);
481
-
let email = format!("consent-post-{}@example.com", ts);
479
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
480
+
let handle = format!("cp{}", suffix);
481
+
let email = format!("cp{}@example.com", suffix);
482
482
let password = "ConsentPost123!";
483
483
let redirect_uri = "https://consent-post.example.com/callback";
484
484
···
590
590
let url = base_url().await;
591
591
let http_client = client();
592
592
593
-
let ts = Utc::now().timestamp_millis();
594
-
let handle = format!("consent-req-{}", ts);
595
-
let email = format!("consent-req-{}@example.com", ts);
593
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
594
+
let handle = format!("cq{}", suffix);
595
+
let email = format!("cq{}@example.com", suffix);
596
596
let password = "ConsentReq123!";
597
597
let redirect_uri = "https://consent-req.example.com/callback";
598
598
+12
-12
tests/oauth_security.rs
+12
-12
tests/oauth_security.rs
···
41
41
}
42
42
43
43
async fn get_oauth_tokens(http_client: &reqwest::Client, url: &str) -> (String, String, String) {
44
-
let ts = Utc::now().timestamp_millis();
45
-
let handle = format!("sec-test-{}", ts);
44
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
45
+
let handle = format!("se{}", suffix);
46
46
let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
47
47
.json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Security123!" }))
48
48
.send().await.unwrap();
···
255
255
StatusCode::BAD_REQUEST,
256
256
"Missing PKCE challenge should be rejected"
257
257
);
258
-
let ts = Utc::now().timestamp_millis();
259
-
let handle = format!("pkce-attack-{}", ts);
258
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
259
+
let handle = format!("pa{}", suffix);
260
260
let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
261
261
.json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Pkce123pass!" }))
262
262
.send().await.unwrap();
···
326
326
async fn test_replay_attacks() {
327
327
let url = base_url().await;
328
328
let http_client = client();
329
-
let ts = Utc::now().timestamp_millis();
330
-
let handle = format!("replay-{}", ts);
329
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
330
+
let handle = format!("rp{}", suffix);
331
331
let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
332
332
.json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Replay123pass!" }))
333
333
.send().await.unwrap();
···
532
532
StatusCode::BAD_REQUEST,
533
533
"Unregistered redirect_uri should be rejected"
534
534
);
535
-
let ts = Utc::now().timestamp_millis();
536
-
let handle = format!("deact-{}", ts);
535
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
536
+
let handle = format!("da{}", suffix);
537
537
let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
538
538
.json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Deact123pass!" }))
539
539
.send().await.unwrap();
···
576
576
let client_id_a = mock_a.uri();
577
577
let mock_b = setup_mock_client_metadata("https://app-b.com/callback").await;
578
578
let client_id_b = mock_b.uri();
579
-
let ts2 = Utc::now().timestamp_millis();
580
-
let handle2 = format!("cross-{}", ts2);
579
+
let suffix2 = &uuid::Uuid::new_v4().simple().to_string()[..8];
580
+
let handle2 = format!("cr{}", suffix2);
581
581
let create_res2 = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
582
582
.json(&json!({ "handle": handle2, "email": format!("{}@example.com", handle2), "password": "Cross123pass!" }))
583
583
.send().await.unwrap();
···
1110
1110
async fn test_delegation_viewer_scope_cannot_write() {
1111
1111
let url = base_url().await;
1112
1112
let http_client = client();
1113
-
let ts = Utc::now().timestamp_millis();
1113
+
let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8];
1114
1114
1115
1115
let (controller_jwt, controller_did) = create_account_and_login(&http_client).await;
1116
1116
1117
-
let delegated_handle = format!("deleg-{}", ts);
1117
+
let delegated_handle = format!("dg{}", suffix);
1118
1118
let delegated_res = http_client
1119
1119
.post(format!(
1120
1120
"{}/xrpc/com.tranquil.delegation.createDelegatedAccount",
+5
-5
tests/password_reset.rs
+5
-5
tests/password_reset.rs
···
9
9
let client = common::client();
10
10
let base_url = common::base_url().await;
11
11
let pool = common::get_test_db_pool().await;
12
-
let handle = format!("pwreset-{}", uuid::Uuid::new_v4());
12
+
let handle = format!("pr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
13
13
let email = format!("{}@example.com", handle);
14
14
let payload = json!({
15
15
"handle": handle,
···
71
71
let client = common::client();
72
72
let base_url = common::base_url().await;
73
73
let pool = common::get_test_db_pool().await;
74
-
let handle = format!("pwreset2-{}", uuid::Uuid::new_v4());
74
+
let handle = format!("pr2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
75
75
let email = format!("{}@example.com", handle);
76
76
let old_password = "Oldpass123!";
77
77
let new_password = "Newpass456!";
···
187
187
let client = common::client();
188
188
let base_url = common::base_url().await;
189
189
let pool = common::get_test_db_pool().await;
190
-
let handle = format!("pwreset3-{}", uuid::Uuid::new_v4());
190
+
let handle = format!("pr3{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
191
191
let email = format!("{}@example.com", handle);
192
192
let payload = json!({
193
193
"handle": handle,
···
251
251
let client = common::client();
252
252
let base_url = common::base_url().await;
253
253
let pool = common::get_test_db_pool().await;
254
-
let handle = format!("pwreset4-{}", uuid::Uuid::new_v4());
254
+
let handle = format!("pr4{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
255
255
let email = format!("{}@example.com", handle);
256
256
let payload = json!({
257
257
"handle": handle,
···
341
341
let pool = common::get_test_db_pool().await;
342
342
let client = common::client();
343
343
let base_url = common::base_url().await;
344
-
let handle = format!("pwreset5-{}", uuid::Uuid::new_v4());
344
+
let handle = format!("pr5{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
345
345
let email = format!("{}@example.com", handle);
346
346
let payload = json!({
347
347
"handle": handle,
+1
-1
tests/server.rs
+1
-1
tests/server.rs
···
26
26
async fn test_account_and_session_lifecycle() {
27
27
let client = client();
28
28
let base = base_url().await;
29
-
let handle = format!("user-{}", uuid::Uuid::new_v4());
29
+
let handle = format!("u{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
30
30
let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" });
31
31
let create_res = client
32
32
.post(format!("{}/xrpc/com.atproto.server.createAccount", base))
+5
-5
tests/signing_key.rs
+5
-5
tests/signing_key.rs
···
164
164
assert_eq!(res.status(), StatusCode::OK);
165
165
let body: Value = res.json().await.unwrap();
166
166
let signing_key = body["signingKey"].as_str().unwrap();
167
-
let handle = format!("reserved-key-user-{}", uuid::Uuid::new_v4());
167
+
let handle = format!("rk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
168
168
let res = client
169
169
.post(format!(
170
170
"{}/xrpc/com.atproto.server.createAccount",
···
202
202
async fn test_create_account_with_invalid_signing_key() {
203
203
let client = common::client();
204
204
let base_url = common::base_url().await;
205
-
let handle = format!("bad-key-user-{}", uuid::Uuid::new_v4());
205
+
let handle = format!("bk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
206
206
let res = client
207
207
.post(format!(
208
208
"{}/xrpc/com.atproto.server.createAccount",
···
238
238
assert_eq!(res.status(), StatusCode::OK);
239
239
let body: Value = res.json().await.unwrap();
240
240
let signing_key = body["signingKey"].as_str().unwrap();
241
-
let handle1 = format!("reuse-key-user1-{}", uuid::Uuid::new_v4());
241
+
let handle1 = format!("r1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
242
242
let res = client
243
243
.post(format!(
244
244
"{}/xrpc/com.atproto.server.createAccount",
···
254
254
.await
255
255
.expect("Failed to create first account");
256
256
assert_eq!(res.status(), StatusCode::OK);
257
-
let handle2 = format!("reuse-key-user2-{}", uuid::Uuid::new_v4());
257
+
let handle2 = format!("r2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
258
258
let res = client
259
259
.post(format!(
260
260
"{}/xrpc/com.atproto.server.createAccount",
···
291
291
assert_eq!(res.status(), StatusCode::OK);
292
292
let body: Value = res.json().await.unwrap();
293
293
let signing_key = body["signingKey"].as_str().unwrap();
294
-
let handle = format!("token-test-user-{}", uuid::Uuid::new_v4());
294
+
let handle = format!("tu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
295
295
let res = client
296
296
.post(format!(
297
297
"{}/xrpc/com.atproto.server.createAccount",
+165
-2
tests/sync_deprecated.rs
+165
-2
tests/sync_deprecated.rs
···
62
62
.expect("Failed to send request");
63
63
assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST);
64
64
let error_body: Value = not_found_res.json().await.unwrap();
65
-
assert_eq!(error_body["error"], "HeadNotFound");
65
+
assert_eq!(error_body["error"], "RepoNotFound");
66
66
let missing_res = client
67
67
.get(format!(
68
68
"{}/xrpc/com.atproto.sync.getHead",
···
165
165
.send()
166
166
.await
167
167
.expect("Failed to send request");
168
-
assert_eq!(not_found_res.status(), StatusCode::NOT_FOUND);
168
+
assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST);
169
169
let error_body: Value = not_found_res.json().await.unwrap();
170
170
assert_eq!(error_body["error"], "RepoNotFound");
171
171
let missing_res = client
···
188
188
.expect("Failed to send request");
189
189
assert_eq!(empty_did_res.status(), StatusCode::BAD_REQUEST);
190
190
}
191
+
192
+
#[tokio::test]
193
+
async fn test_get_head_deactivated_account_returns_error() {
194
+
let client = client();
195
+
let base = base_url().await;
196
+
let (did, jwt) = setup_new_user("deactheadtest").await;
197
+
let res = client
198
+
.get(format!("{}/xrpc/com.atproto.sync.getHead", base))
199
+
.query(&[("did", did.as_str())])
200
+
.send()
201
+
.await
202
+
.unwrap();
203
+
assert_eq!(res.status(), StatusCode::OK);
204
+
client
205
+
.post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base))
206
+
.bearer_auth(&jwt)
207
+
.json(&serde_json::json!({}))
208
+
.send()
209
+
.await
210
+
.unwrap();
211
+
let deact_res = client
212
+
.get(format!("{}/xrpc/com.atproto.sync.getHead", base))
213
+
.query(&[("did", did.as_str())])
214
+
.send()
215
+
.await
216
+
.unwrap();
217
+
assert_eq!(deact_res.status(), StatusCode::BAD_REQUEST);
218
+
let body: Value = deact_res.json().await.unwrap();
219
+
assert_eq!(body["error"], "RepoDeactivated");
220
+
}
221
+
222
+
#[tokio::test]
223
+
async fn test_get_head_takendown_account_returns_error() {
224
+
let client = client();
225
+
let base = base_url().await;
226
+
let (admin_jwt, _) = create_admin_account_and_login(&client).await;
227
+
let (_, target_did) = create_account_and_login(&client).await;
228
+
let res = client
229
+
.get(format!("{}/xrpc/com.atproto.sync.getHead", base))
230
+
.query(&[("did", target_did.as_str())])
231
+
.send()
232
+
.await
233
+
.unwrap();
234
+
assert_eq!(res.status(), StatusCode::OK);
235
+
client
236
+
.post(format!("{}/xrpc/com.atproto.admin.updateSubjectStatus", base))
237
+
.bearer_auth(&admin_jwt)
238
+
.json(&serde_json::json!({
239
+
"subject": {
240
+
"$type": "com.atproto.admin.defs#repoRef",
241
+
"did": target_did
242
+
},
243
+
"takedown": {
244
+
"applied": true,
245
+
"ref": "test-takedown"
246
+
}
247
+
}))
248
+
.send()
249
+
.await
250
+
.unwrap();
251
+
let takedown_res = client
252
+
.get(format!("{}/xrpc/com.atproto.sync.getHead", base))
253
+
.query(&[("did", target_did.as_str())])
254
+
.send()
255
+
.await
256
+
.unwrap();
257
+
assert_eq!(takedown_res.status(), StatusCode::BAD_REQUEST);
258
+
let body: Value = takedown_res.json().await.unwrap();
259
+
assert_eq!(body["error"], "RepoTakendown");
260
+
}
261
+
262
+
#[tokio::test]
263
+
async fn test_get_head_admin_can_access_deactivated() {
264
+
let client = client();
265
+
let base = base_url().await;
266
+
let (admin_jwt, _) = create_admin_account_and_login(&client).await;
267
+
let (user_jwt, did) = create_account_and_login(&client).await;
268
+
client
269
+
.post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base))
270
+
.bearer_auth(&user_jwt)
271
+
.json(&serde_json::json!({}))
272
+
.send()
273
+
.await
274
+
.unwrap();
275
+
let res = client
276
+
.get(format!("{}/xrpc/com.atproto.sync.getHead", base))
277
+
.bearer_auth(&admin_jwt)
278
+
.query(&[("did", did.as_str())])
279
+
.send()
280
+
.await
281
+
.unwrap();
282
+
assert_eq!(res.status(), StatusCode::OK);
283
+
}
284
+
285
+
#[tokio::test]
286
+
async fn test_get_checkout_deactivated_account_returns_error() {
287
+
let client = client();
288
+
let base = base_url().await;
289
+
let (did, jwt) = setup_new_user("deactcheckouttest").await;
290
+
let res = client
291
+
.get(format!("{}/xrpc/com.atproto.sync.getCheckout", base))
292
+
.query(&[("did", did.as_str())])
293
+
.send()
294
+
.await
295
+
.unwrap();
296
+
assert_eq!(res.status(), StatusCode::OK);
297
+
client
298
+
.post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base))
299
+
.bearer_auth(&jwt)
300
+
.json(&serde_json::json!({}))
301
+
.send()
302
+
.await
303
+
.unwrap();
304
+
let deact_res = client
305
+
.get(format!("{}/xrpc/com.atproto.sync.getCheckout", base))
306
+
.query(&[("did", did.as_str())])
307
+
.send()
308
+
.await
309
+
.unwrap();
310
+
assert_eq!(deact_res.status(), StatusCode::BAD_REQUEST);
311
+
let body: Value = deact_res.json().await.unwrap();
312
+
assert_eq!(body["error"], "RepoDeactivated");
313
+
}
314
+
315
+
#[tokio::test]
316
+
async fn test_get_checkout_takendown_account_returns_error() {
317
+
let client = client();
318
+
let base = base_url().await;
319
+
let (admin_jwt, _) = create_admin_account_and_login(&client).await;
320
+
let (_, target_did) = create_account_and_login(&client).await;
321
+
let res = client
322
+
.get(format!("{}/xrpc/com.atproto.sync.getCheckout", base))
323
+
.query(&[("did", target_did.as_str())])
324
+
.send()
325
+
.await
326
+
.unwrap();
327
+
assert_eq!(res.status(), StatusCode::OK);
328
+
client
329
+
.post(format!("{}/xrpc/com.atproto.admin.updateSubjectStatus", base))
330
+
.bearer_auth(&admin_jwt)
331
+
.json(&serde_json::json!({
332
+
"subject": {
333
+
"$type": "com.atproto.admin.defs#repoRef",
334
+
"did": target_did
335
+
},
336
+
"takedown": {
337
+
"applied": true,
338
+
"ref": "test-takedown"
339
+
}
340
+
}))
341
+
.send()
342
+
.await
343
+
.unwrap();
344
+
let takedown_res = client
345
+
.get(format!("{}/xrpc/com.atproto.sync.getCheckout", base))
346
+
.query(&[("did", target_did.as_str())])
347
+
.send()
348
+
.await
349
+
.unwrap();
350
+
assert_eq!(takedown_res.status(), StatusCode::BAD_REQUEST);
351
+
let body: Value = takedown_res.json().await.unwrap();
352
+
assert_eq!(body["error"], "RepoTakendown");
353
+
}