+64
.sqlx/query-13bea39e403ee15f13f877654c6677f7f2ad541edf72324231801ffead506031.json
+64
.sqlx/query-13bea39e403ee15f13f877654c6677f7f2ad541edf72324231801ffead506031.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at\n FROM users\n WHERE did = $1\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "id",
9
+
"type_info": "Uuid"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "did",
14
+
"type_info": "Text"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "handle",
19
+
"type_info": "Text"
20
+
},
21
+
{
22
+
"ordinal": 3,
23
+
"name": "email",
24
+
"type_info": "Text"
25
+
},
26
+
{
27
+
"ordinal": 4,
28
+
"name": "created_at",
29
+
"type_info": "Timestamptz"
30
+
},
31
+
{
32
+
"ordinal": 5,
33
+
"name": "invites_disabled",
34
+
"type_info": "Bool"
35
+
},
36
+
{
37
+
"ordinal": 6,
38
+
"name": "email_verified",
39
+
"type_info": "Bool"
40
+
},
41
+
{
42
+
"ordinal": 7,
43
+
"name": "deactivated_at",
44
+
"type_info": "Timestamptz"
45
+
}
46
+
],
47
+
"parameters": {
48
+
"Left": [
49
+
"Text"
50
+
]
51
+
},
52
+
"nullable": [
53
+
false,
54
+
false,
55
+
false,
56
+
true,
57
+
false,
58
+
true,
59
+
false,
60
+
true
61
+
]
62
+
},
63
+
"hash": "13bea39e403ee15f13f877654c6677f7f2ad541edf72324231801ffead506031"
64
+
}
-40
.sqlx/query-176d30f31356a4d128764c9c2eece81f8079a29e40b07ba58adc4380d58068c8.json
-40
.sqlx/query-176d30f31356a4d128764c9c2eece81f8079a29e40b07ba58adc4380d58068c8.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "\n SELECT did, handle, email, created_at\n FROM users\n WHERE did = $1\n ",
4
-
"describe": {
5
-
"columns": [
6
-
{
7
-
"ordinal": 0,
8
-
"name": "did",
9
-
"type_info": "Text"
10
-
},
11
-
{
12
-
"ordinal": 1,
13
-
"name": "handle",
14
-
"type_info": "Text"
15
-
},
16
-
{
17
-
"ordinal": 2,
18
-
"name": "email",
19
-
"type_info": "Text"
20
-
},
21
-
{
22
-
"ordinal": 3,
23
-
"name": "created_at",
24
-
"type_info": "Timestamptz"
25
-
}
26
-
],
27
-
"parameters": {
28
-
"Left": [
29
-
"Text"
30
-
]
31
-
},
32
-
"nullable": [
33
-
false,
34
-
false,
35
-
true,
36
-
false
37
-
]
38
-
},
39
-
"hash": "176d30f31356a4d128764c9c2eece81f8079a29e40b07ba58adc4380d58068c8"
40
-
}
+22
.sqlx/query-1e034c36940110579d5ba3e6f64b4455a4945b4116dbd561e12269cf1df495b3.json
+22
.sqlx/query-1e034c36940110579d5ba3e6f64b4455a4945b4116dbd561e12269cf1df495b3.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT icu.code\n FROM invite_code_uses icu\n WHERE icu.used_by_user = $1\n LIMIT 1\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "code",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Uuid"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "1e034c36940110579d5ba3e6f64b4455a4945b4116dbd561e12269cf1df495b3"
22
+
}
+22
.sqlx/query-5a98e015997942835800fcd326e69b4f54b9830d0490c4f8841f8435478c57d3.json
+22
.sqlx/query-5a98e015997942835800fcd326e69b4f54b9830d0490c4f8841f8435478c57d3.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT code FROM invite_codes WHERE created_by_user = $1\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "code",
9
+
"type_info": "Text"
10
+
}
11
+
],
12
+
"parameters": {
13
+
"Left": [
14
+
"Uuid"
15
+
]
16
+
},
17
+
"nullable": [
18
+
false
19
+
]
20
+
},
21
+
"hash": "5a98e015997942835800fcd326e69b4f54b9830d0490c4f8841f8435478c57d3"
22
+
}
+64
.sqlx/query-6df413951ea7648c77d8db2fe6e704370869816a3f47c86671dfe000b5961eee.json
+64
.sqlx/query-6df413951ea7648c77d8db2fe6e704370869816a3f47c86671dfe000b5961eee.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at\n FROM users\n WHERE did = $1\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "id",
9
+
"type_info": "Uuid"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "did",
14
+
"type_info": "Text"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "handle",
19
+
"type_info": "Text"
20
+
},
21
+
{
22
+
"ordinal": 3,
23
+
"name": "email",
24
+
"type_info": "Text"
25
+
},
26
+
{
27
+
"ordinal": 4,
28
+
"name": "created_at",
29
+
"type_info": "Timestamptz"
30
+
},
31
+
{
32
+
"ordinal": 5,
33
+
"name": "invites_disabled",
34
+
"type_info": "Bool"
35
+
},
36
+
{
37
+
"ordinal": 6,
38
+
"name": "email_verified",
39
+
"type_info": "Bool"
40
+
},
41
+
{
42
+
"ordinal": 7,
43
+
"name": "deactivated_at",
44
+
"type_info": "Timestamptz"
45
+
}
46
+
],
47
+
"parameters": {
48
+
"Left": [
49
+
"Text"
50
+
]
51
+
},
52
+
"nullable": [
53
+
false,
54
+
false,
55
+
false,
56
+
true,
57
+
false,
58
+
true,
59
+
false,
60
+
true
61
+
]
62
+
},
63
+
"hash": "6df413951ea7648c77d8db2fe6e704370869816a3f47c86671dfe000b5961eee"
64
+
}
-40
.sqlx/query-c2a90157c47bf1c36f08f4608932d214cc26b4794e0b922b1dae3dad18a7ddc0.json
-40
.sqlx/query-c2a90157c47bf1c36f08f4608932d214cc26b4794e0b922b1dae3dad18a7ddc0.json
···
1
-
{
2
-
"db_name": "PostgreSQL",
3
-
"query": "\n SELECT did, handle, email, created_at\n FROM users\n WHERE did = $1\n ",
4
-
"describe": {
5
-
"columns": [
6
-
{
7
-
"ordinal": 0,
8
-
"name": "did",
9
-
"type_info": "Text"
10
-
},
11
-
{
12
-
"ordinal": 1,
13
-
"name": "handle",
14
-
"type_info": "Text"
15
-
},
16
-
{
17
-
"ordinal": 2,
18
-
"name": "email",
19
-
"type_info": "Text"
20
-
},
21
-
{
22
-
"ordinal": 3,
23
-
"name": "created_at",
24
-
"type_info": "Timestamptz"
25
-
}
26
-
],
27
-
"parameters": {
28
-
"Left": [
29
-
"Text"
30
-
]
31
-
},
32
-
"nullable": [
33
-
false,
34
-
false,
35
-
true,
36
-
false
37
-
]
38
-
},
39
-
"hash": "c2a90157c47bf1c36f08f4608932d214cc26b4794e0b922b1dae3dad18a7ddc0"
40
-
}
+52
.sqlx/query-c3139484bba403cd256801e278fe95ae77634e79d14764dd8c3764886cf08eac.json
+52
.sqlx/query-c3139484bba403cd256801e278fe95ae77634e79d14764dd8c3764886cf08eac.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, u.did as created_by\n FROM invite_codes ic\n JOIN users u ON ic.created_by_user = u.id\n WHERE ic.code = $1\n ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "code",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "available_uses",
14
+
"type_info": "Int4"
15
+
},
16
+
{
17
+
"ordinal": 2,
18
+
"name": "disabled",
19
+
"type_info": "Bool"
20
+
},
21
+
{
22
+
"ordinal": 3,
23
+
"name": "for_account",
24
+
"type_info": "Text"
25
+
},
26
+
{
27
+
"ordinal": 4,
28
+
"name": "created_at",
29
+
"type_info": "Timestamptz"
30
+
},
31
+
{
32
+
"ordinal": 5,
33
+
"name": "created_by",
34
+
"type_info": "Text"
35
+
}
36
+
],
37
+
"parameters": {
38
+
"Left": [
39
+
"Text"
40
+
]
41
+
},
42
+
"nullable": [
43
+
false,
44
+
false,
45
+
true,
46
+
false,
47
+
false,
48
+
false
49
+
]
50
+
},
51
+
"hash": "c3139484bba403cd256801e278fe95ae77634e79d14764dd8c3764886cf08eac"
52
+
}
+28
.sqlx/query-c9f3d584c161b6492abc082bdbb563d40173a9a4983d6454dba4e02f7e0f8458.json
+28
.sqlx/query-c9f3d584c161b6492abc082bdbb563d40173a9a4983d6454dba4e02f7e0f8458.json
···
1
+
{
2
+
"db_name": "PostgreSQL",
3
+
"query": "\n SELECT u.did as used_by, 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 ",
4
+
"describe": {
5
+
"columns": [
6
+
{
7
+
"ordinal": 0,
8
+
"name": "used_by",
9
+
"type_info": "Text"
10
+
},
11
+
{
12
+
"ordinal": 1,
13
+
"name": "used_at",
14
+
"type_info": "Timestamptz"
15
+
}
16
+
],
17
+
"parameters": {
18
+
"Left": [
19
+
"Text"
20
+
]
21
+
},
22
+
"nullable": [
23
+
false,
24
+
false
25
+
]
26
+
},
27
+
"hash": "c9f3d584c161b6492abc082bdbb563d40173a9a4983d6454dba4e02f7e0f8458"
28
+
}
+2
-1
scripts/test-infra.sh
+2
-1
scripts/test-infra.sh
+155
-20
src/api/admin/account/info.rs
+155
-20
src/api/admin/account/info.rs
···
20
20
pub struct AccountInfo {
21
21
pub did: String,
22
22
pub handle: String,
23
+
#[serde(skip_serializing_if = "Option::is_none")]
23
24
pub email: Option<String>,
24
25
pub indexed_at: String,
26
+
#[serde(skip_serializing_if = "Option::is_none")]
25
27
pub invite_note: Option<String>,
26
28
pub invites_disabled: bool,
27
-
pub email_verified_at: Option<String>,
29
+
#[serde(skip_serializing_if = "Option::is_none")]
30
+
pub email_confirmed_at: Option<String>,
31
+
#[serde(skip_serializing_if = "Option::is_none")]
28
32
pub deactivated_at: Option<String>,
33
+
#[serde(skip_serializing_if = "Option::is_none")]
34
+
pub invited_by: Option<InviteCodeInfo>,
35
+
#[serde(skip_serializing_if = "Option::is_none")]
36
+
pub invites: Option<Vec<InviteCodeInfo>>,
37
+
}
38
+
39
+
#[derive(Serialize, Clone)]
40
+
#[serde(rename_all = "camelCase")]
41
+
pub struct InviteCodeInfo {
42
+
pub code: String,
43
+
pub available: i32,
44
+
pub disabled: bool,
45
+
pub for_account: String,
46
+
pub created_by: String,
47
+
pub created_at: String,
48
+
pub uses: Vec<InviteCodeUseInfo>,
49
+
}
50
+
51
+
#[derive(Serialize, Clone)]
52
+
#[serde(rename_all = "camelCase")]
53
+
pub struct InviteCodeUseInfo {
54
+
pub used_by: String,
55
+
pub used_at: String,
29
56
}
30
57
31
58
#[derive(Serialize)]
···
49
76
}
50
77
let result = sqlx::query!(
51
78
r#"
52
-
SELECT did, handle, email, created_at
79
+
SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
53
80
FROM users
54
81
WHERE did = $1
55
82
"#,
···
58
85
.fetch_optional(&state.db)
59
86
.await;
60
87
match result {
61
-
Ok(Some(row)) => (
62
-
StatusCode::OK,
63
-
Json(AccountInfo {
64
-
did: row.did,
65
-
handle: row.handle,
66
-
email: row.email,
67
-
indexed_at: row.created_at.to_rfc3339(),
68
-
invite_note: None,
69
-
invites_disabled: false,
70
-
email_verified_at: None,
71
-
deactivated_at: None,
72
-
}),
73
-
)
74
-
.into_response(),
88
+
Ok(Some(row)) => {
89
+
let invited_by = get_invited_by(&state.db, row.id).await;
90
+
let invites = get_invites_for_user(&state.db, row.id).await;
91
+
(
92
+
StatusCode::OK,
93
+
Json(AccountInfo {
94
+
did: row.did,
95
+
handle: row.handle,
96
+
email: row.email,
97
+
indexed_at: row.created_at.to_rfc3339(),
98
+
invite_note: None,
99
+
invites_disabled: row.invites_disabled.unwrap_or(false),
100
+
email_confirmed_at: if row.email_verified {
101
+
Some(row.created_at.to_rfc3339())
102
+
} else {
103
+
None
104
+
},
105
+
deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
106
+
invited_by,
107
+
invites,
108
+
}),
109
+
)
110
+
.into_response()
111
+
}
75
112
Ok(None) => (
76
113
StatusCode::NOT_FOUND,
77
114
Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
···
88
125
}
89
126
}
90
127
128
+
async fn get_invited_by(
129
+
db: &sqlx::PgPool,
130
+
user_id: uuid::Uuid,
131
+
) -> Option<InviteCodeInfo> {
132
+
let use_row = sqlx::query!(
133
+
r#"
134
+
SELECT icu.code
135
+
FROM invite_code_uses icu
136
+
WHERE icu.used_by_user = $1
137
+
LIMIT 1
138
+
"#,
139
+
user_id
140
+
)
141
+
.fetch_optional(db)
142
+
.await
143
+
.ok()??;
144
+
get_invite_code_info(db, &use_row.code).await
145
+
}
146
+
147
+
async fn get_invites_for_user(
148
+
db: &sqlx::PgPool,
149
+
user_id: uuid::Uuid,
150
+
) -> Option<Vec<InviteCodeInfo>> {
151
+
let codes = sqlx::query_scalar!(
152
+
r#"
153
+
SELECT code FROM invite_codes WHERE created_by_user = $1
154
+
"#,
155
+
user_id
156
+
)
157
+
.fetch_all(db)
158
+
.await
159
+
.ok()?;
160
+
if codes.is_empty() {
161
+
return None;
162
+
}
163
+
let mut invites = Vec::new();
164
+
for code in codes {
165
+
if let Some(info) = get_invite_code_info(db, &code).await {
166
+
invites.push(info);
167
+
}
168
+
}
169
+
if invites.is_empty() {
170
+
None
171
+
} else {
172
+
Some(invites)
173
+
}
174
+
}
175
+
176
+
async fn get_invite_code_info(db: &sqlx::PgPool, code: &str) -> Option<InviteCodeInfo> {
177
+
let row = sqlx::query!(
178
+
r#"
179
+
SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at, u.did as created_by
180
+
FROM invite_codes ic
181
+
JOIN users u ON ic.created_by_user = u.id
182
+
WHERE ic.code = $1
183
+
"#,
184
+
code
185
+
)
186
+
.fetch_optional(db)
187
+
.await
188
+
.ok()??;
189
+
let uses = sqlx::query!(
190
+
r#"
191
+
SELECT u.did as used_by, icu.used_at
192
+
FROM invite_code_uses icu
193
+
JOIN users u ON icu.used_by_user = u.id
194
+
WHERE icu.code = $1
195
+
"#,
196
+
code
197
+
)
198
+
.fetch_all(db)
199
+
.await
200
+
.ok()?;
201
+
Some(InviteCodeInfo {
202
+
code: row.code,
203
+
available: row.available_uses,
204
+
disabled: row.disabled.unwrap_or(false),
205
+
for_account: row.for_account,
206
+
created_by: row.created_by,
207
+
created_at: row.created_at.to_rfc3339(),
208
+
uses: uses
209
+
.into_iter()
210
+
.map(|u| InviteCodeUseInfo {
211
+
used_by: u.used_by,
212
+
used_at: u.used_at.to_rfc3339(),
213
+
})
214
+
.collect(),
215
+
})
216
+
}
217
+
91
218
pub async fn get_account_infos(
92
219
State(state): State<AppState>,
93
220
_auth: BearerAuthAdmin,
···
108
235
}
109
236
let result = sqlx::query!(
110
237
r#"
111
-
SELECT did, handle, email, created_at
238
+
SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
112
239
FROM users
113
240
WHERE did = $1
114
241
"#,
···
117
244
.fetch_optional(&state.db)
118
245
.await;
119
246
if let Ok(Some(row)) = result {
247
+
let invited_by = get_invited_by(&state.db, row.id).await;
248
+
let invites = get_invites_for_user(&state.db, row.id).await;
120
249
infos.push(AccountInfo {
121
250
did: row.did,
122
251
handle: row.handle,
123
252
email: row.email,
124
253
indexed_at: row.created_at.to_rfc3339(),
125
254
invite_note: None,
126
-
invites_disabled: false,
127
-
email_verified_at: None,
128
-
deactivated_at: None,
255
+
invites_disabled: row.invites_disabled.unwrap_or(false),
256
+
email_confirmed_at: if row.email_verified {
257
+
Some(row.created_at.to_rfc3339())
258
+
} else {
259
+
None
260
+
},
261
+
deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
262
+
invited_by,
263
+
invites,
129
264
});
130
265
}
131
266
}
+13
-7
src/api/admin/account/search.rs
+13
-7
src/api/admin/account/search.rs
···
12
12
13
13
#[derive(Deserialize)]
14
14
pub struct SearchAccountsParams {
15
+
pub email: Option<String>,
15
16
pub handle: Option<String>,
16
17
pub cursor: Option<String>,
17
18
#[serde(default = "default_limit")]
···
31
32
pub email: Option<String>,
32
33
pub indexed_at: String,
33
34
#[serde(skip_serializing_if = "Option::is_none")]
34
-
pub email_verified_at: Option<String>,
35
+
pub email_confirmed_at: Option<String>,
35
36
#[serde(skip_serializing_if = "Option::is_none")]
36
37
pub deactivated_at: Option<String>,
37
38
#[serde(skip_serializing_if = "Option::is_none")]
···
53
54
) -> Response {
54
55
let limit = params.limit.clamp(1, 100);
55
56
let cursor_did = params.cursor.as_deref().unwrap_or("");
57
+
let email_filter = params.email.as_deref().map(|e| format!("%{}%", e));
56
58
let handle_filter = params.handle.as_deref().map(|h| format!("%{}%", h));
57
59
let result = sqlx::query_as::<
58
60
_,
···
63
65
chrono::DateTime<chrono::Utc>,
64
66
bool,
65
67
Option<chrono::DateTime<chrono::Utc>>,
68
+
Option<bool>,
66
69
),
67
70
>(
68
71
r#"
69
-
SELECT did, handle, email, created_at, email_verified, deactivated_at
72
+
SELECT did, handle, email, created_at, email_verified, deactivated_at, invites_disabled
70
73
FROM users
71
-
WHERE did > $1 AND ($2::text IS NULL OR handle ILIKE $2)
74
+
WHERE did > $1
75
+
AND ($2::text IS NULL OR email ILIKE $2)
76
+
AND ($3::text IS NULL OR handle ILIKE $3)
72
77
ORDER BY did ASC
73
-
LIMIT $3
78
+
LIMIT $4
74
79
"#,
75
80
)
76
81
.bind(cursor_did)
82
+
.bind(&email_filter)
77
83
.bind(&handle_filter)
78
84
.bind(limit + 1)
79
85
.fetch_all(&state.db)
···
85
91
.into_iter()
86
92
.take(limit as usize)
87
93
.map(
88
-
|(did, handle, email, created_at, email_verified, deactivated_at)| {
94
+
|(did, handle, email, created_at, email_verified, deactivated_at, invites_disabled)| {
89
95
AccountView {
90
96
did: did.clone(),
91
97
handle,
92
98
email,
93
99
indexed_at: created_at.to_rfc3339(),
94
-
email_verified_at: if email_verified {
100
+
email_confirmed_at: if email_verified {
95
101
Some(created_at.to_rfc3339())
96
102
} else {
97
103
None
98
104
},
99
105
deactivated_at: deactivated_at.map(|dt| dt.to_rfc3339()),
100
-
invites_disabled: None,
106
+
invites_disabled,
101
107
}
102
108
},
103
109
)
+10
-1
src/api/admin/account/update.rs
+10
-1
src/api/admin/account/update.rs
···
8
8
};
9
9
use serde::Deserialize;
10
10
use serde_json::json;
11
-
use tracing::error;
11
+
use tracing::{error, warn};
12
12
13
13
#[derive(Deserialize)]
14
14
pub struct UpdateAccountEmailInput {
···
128
128
let _ = state.cache.delete(&format!("handle:{}", old)).await;
129
129
}
130
130
let _ = state.cache.delete(&format!("handle:{}", handle)).await;
131
+
if let Err(e) =
132
+
crate::api::repo::record::sequence_identity_event(&state, did, Some(&handle)).await
133
+
{
134
+
warn!("Failed to sequence identity event for admin handle update: {}", e);
135
+
}
136
+
if let Err(e) = crate::api::identity::did::update_plc_handle(&state, did, &handle).await
137
+
{
138
+
warn!("Failed to update PLC handle for admin handle update: {}", e);
139
+
}
131
140
(StatusCode::OK, Json(json!({}))).into_response()
132
141
}
133
142
Err(e) => {
+24
-14
src/api/admin/status.rs
+24
-14
src/api/admin/status.rs
···
135
135
}
136
136
}
137
137
if let Some(blob_cid) = ¶ms.blob {
138
+
let did = match ¶ms.did {
139
+
Some(d) => d,
140
+
None => {
141
+
return (
142
+
StatusCode::BAD_REQUEST,
143
+
Json(json!({"error": "InvalidRequest", "message": "Must provide a did to request blob state"})),
144
+
)
145
+
.into_response();
146
+
}
147
+
};
138
148
let blob = sqlx::query!(
139
149
"SELECT cid, takedown_ref FROM blobs WHERE cid = $1",
140
150
blob_cid
···
152
162
Json(SubjectStatus {
153
163
subject: json!({
154
164
"$type": "com.atproto.admin.defs#repoBlobRef",
155
-
"did": "",
165
+
"did": did,
156
166
"cid": row.cid
157
167
}),
158
168
takedown,
···
195
205
196
206
#[derive(Deserialize)]
197
207
pub struct StatusAttrInput {
198
-
pub apply: bool,
208
+
pub applied: bool,
199
209
pub r#ref: Option<String>,
200
210
}
201
211
···
221
231
}
222
232
};
223
233
if let Some(takedown) = &input.takedown {
224
-
let takedown_ref = if takedown.apply {
234
+
let takedown_ref = if takedown.applied {
225
235
takedown.r#ref.clone()
226
236
} else {
227
237
None
···
243
253
}
244
254
}
245
255
if let Some(deactivated) = &input.deactivated {
246
-
let result = if deactivated.apply {
256
+
let result = if deactivated.applied {
247
257
sqlx::query!(
248
258
"UPDATE users SET deactivated_at = NOW() WHERE did = $1",
249
259
did
···
276
286
.into_response();
277
287
}
278
288
if let Some(takedown) = &input.takedown {
279
-
let status = if takedown.apply {
289
+
let status = if takedown.applied {
280
290
Some("takendown")
281
291
} else {
282
292
None
···
284
294
if let Err(e) = crate::api::repo::record::sequence_account_event(
285
295
&state,
286
296
did,
287
-
!takedown.apply,
297
+
!takedown.applied,
288
298
status,
289
299
)
290
300
.await
···
293
303
}
294
304
}
295
305
if let Some(deactivated) = &input.deactivated {
296
-
let status = if deactivated.apply {
306
+
let status = if deactivated.applied {
297
307
Some("deactivated")
298
308
} else {
299
309
None
···
301
311
if let Err(e) = crate::api::repo::record::sequence_account_event(
302
312
&state,
303
313
did,
304
-
!deactivated.apply,
314
+
!deactivated.applied,
305
315
status,
306
316
)
307
317
.await
···
321
331
Json(json!({
322
332
"subject": input.subject,
323
333
"takedown": input.takedown.as_ref().map(|t| json!({
324
-
"applied": t.apply,
334
+
"applied": t.applied,
325
335
"ref": t.r#ref
326
336
})),
327
337
"deactivated": input.deactivated.as_ref().map(|d| json!({
328
-
"applied": d.apply
338
+
"applied": d.applied
329
339
}))
330
340
})),
331
341
)
···
336
346
let uri = input.subject.get("uri").and_then(|u| u.as_str());
337
347
if let Some(uri) = uri {
338
348
if let Some(takedown) = &input.takedown {
339
-
let takedown_ref = if takedown.apply {
349
+
let takedown_ref = if takedown.applied {
340
350
takedown.r#ref.clone()
341
351
} else {
342
352
None
···
365
375
Json(json!({
366
376
"subject": input.subject,
367
377
"takedown": input.takedown.as_ref().map(|t| json!({
368
-
"applied": t.apply,
378
+
"applied": t.applied,
369
379
"ref": t.r#ref
370
380
}))
371
381
})),
···
377
387
let cid = input.subject.get("cid").and_then(|c| c.as_str());
378
388
if let Some(cid) = cid {
379
389
if let Some(takedown) = &input.takedown {
380
-
let takedown_ref = if takedown.apply {
390
+
let takedown_ref = if takedown.applied {
381
391
takedown.r#ref.clone()
382
392
} else {
383
393
None
···
403
413
Json(json!({
404
414
"subject": input.subject,
405
415
"takedown": input.takedown.as_ref().map(|t| json!({
406
-
"applied": t.apply,
416
+
"applied": t.applied,
407
417
"ref": t.r#ref
408
418
}))
409
419
})),
+1
-1
src/api/identity/did.rs
+1
-1
src/api/identity/did.rs
+10
-20
tests/account_notifications.rs
+10
-20
tests/account_notifications.rs
···
1
1
mod common;
2
-
use common::{base_url, client, create_account_and_login, get_db_connection_string};
2
+
use common::{base_url, client, create_account_and_login, get_test_db_pool};
3
3
use serde_json::{Value, json};
4
-
use sqlx::PgPool;
5
4
use tranquil_pds::comms::{CommsType, NewComms, enqueue_comms};
6
5
7
-
async fn get_pool() -> PgPool {
8
-
let conn_str = get_db_connection_string().await;
9
-
sqlx::postgres::PgPoolOptions::new()
10
-
.max_connections(5)
11
-
.connect(&conn_str)
12
-
.await
13
-
.expect("Failed to connect to test database")
14
-
}
15
-
16
6
#[tokio::test]
17
7
async fn test_get_notification_history() {
18
8
let client = client();
19
9
let base = base_url().await;
20
-
let pool = get_pool().await;
10
+
let pool = get_test_db_pool().await;
21
11
let (token, did) = create_account_and_login(&client).await;
22
12
23
13
let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
24
-
.fetch_one(&pool)
14
+
.fetch_one(pool)
25
15
.await
26
16
.expect("User not found");
27
17
···
33
23
format!("Subject {}", i),
34
24
format!("Body {}", i),
35
25
);
36
-
enqueue_comms(&pool, comms)
26
+
enqueue_comms(pool, comms)
37
27
.await
38
28
.expect("Failed to enqueue");
39
29
}
···
86
76
.contains(&json!("discord"))
87
77
);
88
78
89
-
let pool = get_pool().await;
79
+
let pool = get_test_db_pool().await;
90
80
let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
91
-
.fetch_one(&pool)
81
+
.fetch_one(pool)
92
82
.await
93
83
.expect("User not found");
94
84
···
96
86
"SELECT body, metadata FROM comms_queue WHERE user_id = $1 AND comms_type = 'channel_verification' ORDER BY created_at DESC LIMIT 1",
97
87
user_id
98
88
)
99
-
.fetch_one(&pool)
89
+
.fetch_one(pool)
100
90
.await
101
91
.expect("Verification code not found");
102
92
···
213
203
async fn test_update_email_via_notification_prefs() {
214
204
let client = client();
215
205
let base = base_url().await;
216
-
let pool = get_pool().await;
206
+
let pool = get_test_db_pool().await;
217
207
let (token, did) = create_account_and_login(&client).await;
218
208
219
209
let unique_email = format!("newemail_{}@example.com", uuid::Uuid::new_v4());
···
240
230
);
241
231
242
232
let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
243
-
.fetch_one(&pool)
233
+
.fetch_one(pool)
244
234
.await
245
235
.expect("User not found");
246
236
···
248
238
"SELECT body FROM comms_queue WHERE user_id = $1 AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1",
249
239
user_id
250
240
)
251
-
.fetch_one(&pool)
241
+
.fetch_one(pool)
252
242
.await
253
243
.expect("Verification code not found");
254
244
+6
-16
tests/admin_email.rs
+6
-16
tests/admin_email.rs
···
2
2
3
3
use reqwest::StatusCode;
4
4
use serde_json::{Value, json};
5
-
use sqlx::PgPool;
6
-
7
-
async fn get_pool() -> PgPool {
8
-
let conn_str = common::get_db_connection_string().await;
9
-
sqlx::postgres::PgPoolOptions::new()
10
-
.max_connections(5)
11
-
.connect(&conn_str)
12
-
.await
13
-
.expect("Failed to connect to test database")
14
-
}
15
5
16
6
#[tokio::test]
17
7
async fn test_send_email_success() {
18
8
let client = common::client();
19
9
let base_url = common::base_url().await;
20
-
let pool = get_pool().await;
10
+
let pool = common::get_test_db_pool().await;
21
11
let (access_jwt, did) = common::create_admin_account_and_login(&client).await;
22
12
let res = client
23
13
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
···
35
25
let body: Value = res.json().await.expect("Invalid JSON");
36
26
assert_eq!(body["sent"], true);
37
27
let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
38
-
.fetch_one(&pool)
28
+
.fetch_one(pool)
39
29
.await
40
30
.expect("User not found");
41
31
let notification = sqlx::query!(
42
32
"SELECT subject, body, comms_type as \"comms_type: String\" FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' ORDER BY created_at DESC LIMIT 1",
43
33
user.id
44
34
)
45
-
.fetch_one(&pool)
35
+
.fetch_one(pool)
46
36
.await
47
37
.expect("Notification not found");
48
38
assert_eq!(notification.subject.as_deref(), Some("Test Admin Email"));
···
57
47
async fn test_send_email_default_subject() {
58
48
let client = common::client();
59
49
let base_url = common::base_url().await;
60
-
let pool = get_pool().await;
50
+
let pool = common::get_test_db_pool().await;
61
51
let (access_jwt, did) = common::create_admin_account_and_login(&client).await;
62
52
let res = client
63
53
.post(format!("{}/xrpc/com.atproto.admin.sendEmail", base_url))
···
74
64
let body: Value = res.json().await.expect("Invalid JSON");
75
65
assert_eq!(body["sent"], true);
76
66
let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
77
-
.fetch_one(&pool)
67
+
.fetch_one(pool)
78
68
.await
79
69
.expect("User not found");
80
70
let notification = sqlx::query!(
81
71
"SELECT subject FROM comms_queue WHERE user_id = $1 AND comms_type = 'admin_email' AND body = 'Email without subject' LIMIT 1",
82
72
user.id
83
73
)
84
-
.fetch_one(&pool)
74
+
.fetch_one(pool)
85
75
.await
86
76
.expect("Notification not found");
87
77
assert!(notification.subject.is_some());
+6
-6
tests/admin_moderation.rs
+6
-6
tests/admin_moderation.rs
···
88
88
"did": target_did
89
89
},
90
90
"takedown": {
91
-
"apply": true,
91
+
"applied": true,
92
92
"ref": "mod-action-123"
93
93
}
94
94
});
···
134
134
"did": target_did
135
135
},
136
136
"takedown": {
137
-
"apply": true,
137
+
"applied": true,
138
138
"ref": "mod-action-456"
139
139
}
140
140
});
···
153
153
"did": target_did
154
154
},
155
155
"takedown": {
156
-
"apply": false
156
+
"applied": false
157
157
}
158
158
});
159
159
let res = client
···
197
197
"did": target_did
198
198
},
199
199
"deactivated": {
200
-
"apply": true
200
+
"applied": true
201
201
}
202
202
});
203
203
let res = client
···
236
236
"did": "did:plc:test"
237
237
},
238
238
"takedown": {
239
-
"apply": true
239
+
"applied": true
240
240
}
241
241
});
242
242
let res = client
···
263
263
"did": "did:plc:test"
264
264
},
265
265
"takedown": {
266
-
"apply": true
266
+
"applied": true
267
267
}
268
268
});
269
269
let res = client
+21
-16
tests/common/mod.rs
+21
-16
tests/common/mod.rs
···
18
18
static SERVER_URL: OnceLock<String> = OnceLock::new();
19
19
static APP_PORT: OnceLock<u16> = OnceLock::new();
20
20
static MOCK_APPVIEW: OnceLock<MockServer> = OnceLock::new();
21
+
static TEST_DB_POOL: OnceLock<sqlx::PgPool> = OnceLock::new();
21
22
22
23
#[cfg(not(feature = "external-infra"))]
23
24
use testcontainers::core::ContainerPort;
···
237
238
async fn spawn_app(database_url: String) -> String {
238
239
use tranquil_pds::rate_limit::RateLimiters;
239
240
let pool = PgPoolOptions::new()
240
-
.max_connections(50)
241
+
.max_connections(3)
242
+
.acquire_timeout(std::time::Duration::from_secs(30))
241
243
.connect(&database_url)
242
244
.await
243
245
.expect("Failed to connect to Postgres. Make sure the database is running.");
···
245
247
.run(&pool)
246
248
.await
247
249
.expect("Failed to run migrations");
250
+
let test_pool = PgPoolOptions::new()
251
+
.max_connections(5)
252
+
.acquire_timeout(std::time::Duration::from_secs(30))
253
+
.connect(&database_url)
254
+
.await
255
+
.expect("Failed to create test pool");
256
+
TEST_DB_POOL.set(test_pool).ok();
248
257
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
249
258
let addr = listener.local_addr().unwrap();
250
259
APP_PORT.set(addr.port()).ok();
···
292
301
}
293
302
294
303
#[allow(dead_code)]
304
+
pub async fn get_test_db_pool() -> &'static sqlx::PgPool {
305
+
base_url().await;
306
+
TEST_DB_POOL.get().expect("TEST_DB_POOL not initialized")
307
+
}
308
+
309
+
#[allow(dead_code)]
295
310
pub async fn verify_new_account(client: &Client, did: &str) -> String {
296
-
let conn_str = get_db_connection_string().await;
297
-
let pool = sqlx::postgres::PgPoolOptions::new()
298
-
.max_connections(2)
299
-
.connect(&conn_str)
300
-
.await
301
-
.expect("Failed to connect to test database");
311
+
let pool = get_test_db_pool().await;
302
312
let body_text: String = sqlx::query_scalar!(
303
313
"SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
304
314
did
305
315
)
306
-
.fetch_one(&pool)
316
+
.fetch_one(pool)
307
317
.await
308
318
.expect("Failed to get verification code");
309
319
···
454
464
if res.status() == StatusCode::OK {
455
465
let body: Value = res.json().await.expect("Invalid JSON");
456
466
let did = body["did"].as_str().expect("No did").to_string();
457
-
let conn_str = get_db_connection_string().await;
458
-
let pool = sqlx::postgres::PgPoolOptions::new()
459
-
.max_connections(2)
460
-
.connect(&conn_str)
461
-
.await
462
-
.expect("Failed to connect to test database");
467
+
let pool = get_test_db_pool().await;
463
468
if make_admin {
464
469
sqlx::query!("UPDATE users SET is_admin = TRUE WHERE did = $1", &did)
465
-
.execute(&pool)
470
+
.execute(pool)
466
471
.await
467
472
.expect("Failed to mark user as admin");
468
473
}
···
476
481
"SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
477
482
&did
478
483
)
479
-
.fetch_one(&pool)
484
+
.fetch_one(pool)
480
485
.await
481
486
.expect("Failed to get verification from comms_queue");
482
487
let lines: Vec<&str> = body_text.lines().collect();
+13
-23
tests/delete_account.rs
+13
-23
tests/delete_account.rs
···
4
4
use common::*;
5
5
use reqwest::StatusCode;
6
6
use serde_json::{Value, json};
7
-
use sqlx::PgPool;
8
-
9
-
async fn get_pool() -> PgPool {
10
-
let conn_str = get_db_connection_string().await;
11
-
sqlx::postgres::PgPoolOptions::new()
12
-
.max_connections(5)
13
-
.connect(&conn_str)
14
-
.await
15
-
.expect("Failed to connect to test database")
16
-
}
17
7
18
8
async fn create_verified_account(
19
9
client: &reqwest::Client,
···
61
51
.await
62
52
.expect("Failed to request account deletion");
63
53
assert_eq!(request_delete_res.status(), StatusCode::OK);
64
-
let pool = get_pool().await;
54
+
let pool = get_test_db_pool().await;
65
55
let row = sqlx::query!(
66
56
"SELECT token FROM account_deletion_requests WHERE did = $1",
67
57
did
68
58
)
69
-
.fetch_one(&pool)
59
+
.fetch_one(pool)
70
60
.await
71
61
.expect("Failed to query deletion token");
72
62
let token = row.token;
···
86
76
.expect("Failed to delete account");
87
77
assert_eq!(delete_res.status(), StatusCode::OK);
88
78
let user_row = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
89
-
.fetch_optional(&pool)
79
+
.fetch_optional(pool)
90
80
.await
91
81
.expect("Failed to query user");
92
82
assert!(user_row.is_none(), "User should be deleted from database");
···
118
108
.await
119
109
.expect("Failed to request account deletion");
120
110
assert_eq!(request_delete_res.status(), StatusCode::OK);
121
-
let pool = get_pool().await;
111
+
let pool = get_test_db_pool().await;
122
112
let row = sqlx::query!(
123
113
"SELECT token FROM account_deletion_requests WHERE did = $1",
124
114
did
125
115
)
126
-
.fetch_one(&pool)
116
+
.fetch_one(pool)
127
117
.await
128
118
.expect("Failed to query deletion token");
129
119
let token = row.token;
···
208
198
.await
209
199
.expect("Failed to request account deletion");
210
200
assert_eq!(request_delete_res.status(), StatusCode::OK);
211
-
let pool = get_pool().await;
201
+
let pool = get_test_db_pool().await;
212
202
let row = sqlx::query!(
213
203
"SELECT token FROM account_deletion_requests WHERE did = $1",
214
204
did
215
205
)
216
-
.fetch_one(&pool)
206
+
.fetch_one(pool)
217
207
.await
218
208
.expect("Failed to query deletion token");
219
209
let token = row.token;
···
221
211
"UPDATE account_deletion_requests SET expires_at = NOW() - INTERVAL '1 hour' WHERE token = $1",
222
212
token
223
213
)
224
-
.execute(&pool)
214
+
.execute(pool)
225
215
.await
226
216
.expect("Failed to expire token");
227
217
let delete_payload = json!({
···
267
257
.await
268
258
.expect("Failed to request account deletion");
269
259
assert_eq!(request_delete_res.status(), StatusCode::OK);
270
-
let pool = get_pool().await;
260
+
let pool = get_test_db_pool().await;
271
261
let row = sqlx::query!(
272
262
"SELECT token FROM account_deletion_requests WHERE did = $1",
273
263
did1
274
264
)
275
-
.fetch_one(&pool)
265
+
.fetch_one(pool)
276
266
.await
277
267
.expect("Failed to query deletion token");
278
268
let token = row.token;
···
328
318
.await
329
319
.expect("Failed to request account deletion");
330
320
assert_eq!(request_delete_res.status(), StatusCode::OK);
331
-
let pool = get_pool().await;
321
+
let pool = get_test_db_pool().await;
332
322
let row = sqlx::query!(
333
323
"SELECT token FROM account_deletion_requests WHERE did = $1",
334
324
did
335
325
)
336
-
.fetch_one(&pool)
326
+
.fetch_one(pool)
337
327
.await
338
328
.expect("Failed to query deletion token");
339
329
let token = row.token;
···
353
343
.expect("Failed to delete account");
354
344
assert_eq!(delete_res.status(), StatusCode::OK);
355
345
let user_row = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
356
-
.fetch_optional(&pool)
346
+
.fetch_optional(pool)
357
347
.await
358
348
.expect("Failed to query user");
359
349
assert!(user_row.is_none(), "User should be deleted from database");
+12
-21
tests/email_update.rs
+12
-21
tests/email_update.rs
···
3
3
use serde_json::{Value, json};
4
4
use sqlx::PgPool;
5
5
6
-
async fn get_pool() -> PgPool {
7
-
let conn_str = common::get_db_connection_string().await;
8
-
sqlx::postgres::PgPoolOptions::new()
9
-
.max_connections(5)
10
-
.connect(&conn_str)
11
-
.await
12
-
.expect("Failed to connect to test database")
13
-
}
14
-
15
6
async fn get_email_update_token(pool: &PgPool, did: &str) -> String {
16
7
let body_text: String = sqlx::query_scalar!(
17
8
"SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_update' ORDER BY created_at DESC LIMIT 1",
···
88
79
async fn test_update_email_flow_success() {
89
80
let client = common::client();
90
81
let base_url = common::base_url().await;
91
-
let pool = get_pool().await;
82
+
let pool = common::get_test_db_pool().await;
92
83
let handle = format!("emailup-{}", uuid::Uuid::new_v4());
93
84
let email = format!("{}@example.com", handle);
94
85
let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
···
107
98
let body: Value = res.json().await.expect("Invalid JSON");
108
99
assert_eq!(body["tokenRequired"], true);
109
100
110
-
let code = get_email_update_token(&pool, &did).await;
101
+
let code = get_email_update_token(pool, &did).await;
111
102
112
103
let res = client
113
104
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
···
122
113
assert_eq!(res.status(), StatusCode::OK);
123
114
124
115
let user_email: Option<String> = sqlx::query_scalar!("SELECT email FROM users WHERE did = $1", did)
125
-
.fetch_one(&pool)
116
+
.fetch_one(pool)
126
117
.await
127
118
.expect("User not found");
128
119
assert_eq!(user_email, Some(new_email));
···
244
235
async fn test_confirm_email_confirms_existing_email() {
245
236
let client = common::client();
246
237
let base_url = common::base_url().await;
247
-
let pool = get_pool().await;
238
+
let pool = common::get_test_db_pool().await;
248
239
let handle = format!("emailconfirm-{}", uuid::Uuid::new_v4());
249
240
let email = format!("{}@example.com", handle);
250
241
···
270
261
"SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
271
262
did
272
263
)
273
-
.fetch_one(&pool)
264
+
.fetch_one(pool)
274
265
.await
275
266
.expect("Verification email not found");
276
267
···
296
287
"SELECT email_verified FROM users WHERE did = $1",
297
288
did
298
289
)
299
-
.fetch_one(&pool)
290
+
.fetch_one(pool)
300
291
.await
301
292
.expect("User not found");
302
293
assert!(verified);
···
306
297
async fn test_confirm_email_rejects_wrong_email() {
307
298
let client = common::client();
308
299
let base_url = common::base_url().await;
309
-
let pool = get_pool().await;
300
+
let pool = common::get_test_db_pool().await;
310
301
let handle = format!("emailconf-wrong-{}", uuid::Uuid::new_v4());
311
302
let email = format!("{}@example.com", handle);
312
303
···
332
323
"SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
333
324
did
334
325
)
335
-
.fetch_one(&pool)
326
+
.fetch_one(pool)
336
327
.await
337
328
.expect("Verification email not found");
338
329
···
400
391
async fn test_unverified_account_can_update_email_without_token() {
401
392
let client = common::client();
402
393
let base_url = common::base_url().await;
403
-
let pool = get_pool().await;
394
+
let pool = common::get_test_db_pool().await;
404
395
let handle = format!("emailup-unverified-{}", uuid::Uuid::new_v4());
405
396
let email = format!("{}@example.com", handle);
406
397
···
454
445
455
446
let user_email: Option<String> =
456
447
sqlx::query_scalar!("SELECT email FROM users WHERE did = $1", did)
457
-
.fetch_one(&pool)
448
+
.fetch_one(pool)
458
449
.await
459
450
.expect("User not found");
460
451
assert_eq!(user_email, Some(new_email));
···
464
455
async fn test_update_email_taken_by_another_user() {
465
456
let client = common::client();
466
457
let base_url = common::base_url().await;
467
-
let pool = get_pool().await;
458
+
let pool = common::get_test_db_pool().await;
468
459
469
460
let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4());
470
461
let email1 = format!("{}@example.com", handle1);
···
485
476
.expect("Failed to request email update");
486
477
assert_eq!(res.status(), StatusCode::OK);
487
478
488
-
let code = get_email_update_token(&pool, &did2).await;
479
+
let code = get_email_update_token(pool, &did2).await;
489
480
490
481
let res = client
491
482
.post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
+4
-14
tests/helpers/mod.rs
+4
-14
tests/helpers/mod.rs
···
217
217
218
218
#[allow(dead_code)]
219
219
pub async fn set_account_takedown(did: &str, takedown_ref: Option<&str>) {
220
-
let conn_str = get_db_connection_string().await;
221
-
let pool = sqlx::postgres::PgPoolOptions::new()
222
-
.max_connections(2)
223
-
.connect(&conn_str)
224
-
.await
225
-
.expect("Failed to connect to test database");
220
+
let pool = get_test_db_pool().await;
226
221
sqlx::query!(
227
222
"UPDATE users SET takedown_ref = $1 WHERE did = $2",
228
223
takedown_ref,
229
224
did
230
225
)
231
-
.execute(&pool)
226
+
.execute(pool)
232
227
.await
233
228
.expect("Failed to update takedown_ref");
234
229
}
235
230
236
231
#[allow(dead_code)]
237
232
pub async fn set_account_deactivated(did: &str, deactivated: bool) {
238
-
let conn_str = get_db_connection_string().await;
239
-
let pool = sqlx::postgres::PgPoolOptions::new()
240
-
.max_connections(2)
241
-
.connect(&conn_str)
242
-
.await
243
-
.expect("Failed to connect to test database");
233
+
let pool = get_test_db_pool().await;
244
234
let deactivated_at: Option<chrono::DateTime<Utc>> =
245
235
if deactivated { Some(Utc::now()) } else { None };
246
236
sqlx::query!(
···
248
238
deactivated_at,
249
239
did
250
240
)
251
-
.execute(&pool)
241
+
.execute(pool)
252
242
.await
253
243
.expect("Failed to update deactivated_at");
254
244
}
+3
-7
tests/jwt_security.rs
+3
-7
tests/jwt_security.rs
···
2
2
mod common;
3
3
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
4
4
use chrono::{Duration, Utc};
5
-
use common::{base_url, client, create_account_and_login, get_db_connection_string};
5
+
use common::{base_url, client, create_account_and_login, get_test_db_pool};
6
6
use k256::SecretKey;
7
7
use k256::ecdsa::{Signature, SigningKey, signature::Signer};
8
8
use rand::rngs::OsRng;
···
683
683
let account: Value = create_res.json().await.unwrap();
684
684
let did = account["did"].as_str().unwrap();
685
685
686
-
let pool = sqlx::postgres::PgPoolOptions::new()
687
-
.max_connections(2)
688
-
.connect(&get_db_connection_string().await)
689
-
.await
690
-
.unwrap();
686
+
let pool = get_test_db_pool().await;
691
687
let body_text: String = sqlx::query_scalar!(
692
688
"SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
693
689
did
694
-
).fetch_one(&pool).await.unwrap();
690
+
).fetch_one(pool).await.unwrap();
695
691
let lines: Vec<&str> = body_text.lines().collect();
696
692
let code = lines
697
693
.iter()
+13
-23
tests/notifications.rs
+13
-23
tests/notifications.rs
···
1
1
mod common;
2
-
use sqlx::PgPool;
3
2
use tranquil_pds::comms::{
4
3
CommsChannel, CommsStatus, CommsType, NewComms, enqueue_comms, enqueue_welcome,
5
4
};
6
5
7
-
async fn get_pool() -> PgPool {
8
-
let conn_str = common::get_db_connection_string().await;
9
-
sqlx::postgres::PgPoolOptions::new()
10
-
.max_connections(5)
11
-
.connect(&conn_str)
12
-
.await
13
-
.expect("Failed to connect to test database")
14
-
}
15
-
16
6
#[tokio::test]
17
7
async fn test_enqueue_comms() {
18
-
let pool = get_pool().await;
8
+
let pool = common::get_test_db_pool().await;
19
9
let (_, did) = common::create_account_and_login(&common::client()).await;
20
10
let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
21
-
.fetch_one(&pool)
11
+
.fetch_one(pool)
22
12
.await
23
13
.expect("User not found");
24
14
let item = NewComms::email(
···
28
18
"Test Subject".to_string(),
29
19
"Test body".to_string(),
30
20
);
31
-
let comms_id = enqueue_comms(&pool, item)
21
+
let comms_id = enqueue_comms(pool, item)
32
22
.await
33
23
.expect("Failed to enqueue comms");
34
24
let row = sqlx::query!(
···
43
33
"#,
44
34
comms_id
45
35
)
46
-
.fetch_one(&pool)
36
+
.fetch_one(pool)
47
37
.await
48
38
.expect("Comms not found");
49
39
assert_eq!(row.user_id, user_id);
···
57
47
58
48
#[tokio::test]
59
49
async fn test_enqueue_welcome() {
60
-
let pool = get_pool().await;
50
+
let pool = common::get_test_db_pool().await;
61
51
let (_, did) = common::create_account_and_login(&common::client()).await;
62
52
let user_row = sqlx::query!("SELECT id, email, handle FROM users WHERE did = $1", did)
63
-
.fetch_one(&pool)
53
+
.fetch_one(pool)
64
54
.await
65
55
.expect("User not found");
66
-
let comms_id = enqueue_welcome(&pool, user_row.id, "example.com")
56
+
let comms_id = enqueue_welcome(pool, user_row.id, "example.com")
67
57
.await
68
58
.expect("Failed to enqueue welcome comms");
69
59
let row = sqlx::query!(
···
76
66
"#,
77
67
comms_id
78
68
)
79
-
.fetch_one(&pool)
69
+
.fetch_one(pool)
80
70
.await
81
71
.expect("Comms not found");
82
72
assert_eq!(Some(row.recipient), user_row.email);
···
87
77
88
78
#[tokio::test]
89
79
async fn test_comms_queue_status_index() {
90
-
let pool = get_pool().await;
80
+
let pool = common::get_test_db_pool().await;
91
81
let (_, did) = common::create_account_and_login(&common::client()).await;
92
82
let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
93
-
.fetch_one(&pool)
83
+
.fetch_one(pool)
94
84
.await
95
85
.expect("User not found");
96
86
let initial_count: i64 = sqlx::query_scalar!(
97
87
"SELECT COUNT(*) FROM comms_queue WHERE status = 'pending' AND user_id = $1",
98
88
user_id
99
89
)
100
-
.fetch_one(&pool)
90
+
.fetch_one(pool)
101
91
.await
102
92
.expect("Failed to count")
103
93
.unwrap_or(0);
···
109
99
"Test".to_string(),
110
100
"Body".to_string(),
111
101
);
112
-
enqueue_comms(&pool, item).await.expect("Failed to enqueue");
102
+
enqueue_comms(pool, item).await.expect("Failed to enqueue");
113
103
}
114
104
let final_count: i64 = sqlx::query_scalar!(
115
105
"SELECT COUNT(*) FROM comms_queue WHERE status = 'pending' AND user_id = $1",
116
106
user_id
117
107
)
118
-
.fetch_one(&pool)
108
+
.fetch_one(pool)
119
109
.await
120
110
.expect("Failed to count")
121
111
.unwrap_or(0);
+9
-24
tests/oauth.rs
+9
-24
tests/oauth.rs
···
2
2
mod helpers;
3
3
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
4
4
use chrono::Utc;
5
-
use common::{base_url, client, get_db_connection_string};
5
+
use common::{base_url, client, get_test_db_pool};
6
6
use helpers::verify_new_account;
7
7
use reqwest::{StatusCode, redirect};
8
8
use serde_json::{Value, json};
···
449
449
let account: Value = create_res.json().await.unwrap();
450
450
let user_did = account["did"].as_str().unwrap();
451
451
verify_new_account(&http_client, user_did).await;
452
-
let db_url = get_db_connection_string().await;
453
-
let pool = sqlx::postgres::PgPoolOptions::new()
454
-
.max_connections(1)
455
-
.connect(&db_url)
456
-
.await
457
-
.unwrap();
452
+
let pool = get_test_db_pool().await;
458
453
sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
459
454
.bind(user_did)
460
-
.execute(&pool)
455
+
.execute(pool)
461
456
.await
462
457
.unwrap();
463
458
let redirect_uri = "https://example.com/2fa-callback";
···
516
511
let twofa_code: String =
517
512
sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1")
518
513
.bind(request_uri)
519
-
.fetch_one(&pool)
514
+
.fetch_one(pool)
520
515
.await
521
516
.unwrap();
522
517
let twofa_res = http_client
···
575
570
let account: Value = create_res.json().await.unwrap();
576
571
let user_did = account["did"].as_str().unwrap();
577
572
verify_new_account(&http_client, user_did).await;
578
-
let db_url = get_db_connection_string().await;
579
-
let pool = sqlx::postgres::PgPoolOptions::new()
580
-
.max_connections(1)
581
-
.connect(&db_url)
582
-
.await
583
-
.unwrap();
573
+
let pool = get_test_db_pool().await;
584
574
sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
585
575
.bind(user_did)
586
-
.execute(&pool)
576
+
.execute(pool)
587
577
.await
588
578
.unwrap();
589
579
let redirect_uri = "https://example.com/2fa-lockout-callback";
···
754
744
.json::<Value>()
755
745
.await
756
746
.unwrap();
757
-
let db_url = get_db_connection_string().await;
758
-
let pool = sqlx::postgres::PgPoolOptions::new()
759
-
.max_connections(1)
760
-
.connect(&db_url)
761
-
.await
762
-
.unwrap();
747
+
let pool = get_test_db_pool().await;
763
748
sqlx::query("UPDATE users SET two_factor_enabled = true WHERE did = $1")
764
749
.bind(&user_did)
765
-
.execute(&pool)
750
+
.execute(pool)
766
751
.await
767
752
.unwrap();
768
753
let (code_verifier2, code_challenge2) = generate_pkce();
···
803
788
let twofa_code: String =
804
789
sqlx::query_scalar("SELECT code FROM oauth_2fa_challenge WHERE request_uri = $1")
805
790
.bind(request_uri2)
806
-
.fetch_one(&pool)
791
+
.fetch_one(pool)
807
792
.await
808
793
.unwrap();
809
794
let twofa_res = http_client
+14
-24
tests/password_reset.rs
+14
-24
tests/password_reset.rs
···
3
3
use helpers::verify_new_account;
4
4
use reqwest::StatusCode;
5
5
use serde_json::{Value, json};
6
-
use sqlx::PgPool;
7
-
8
-
async fn get_pool() -> PgPool {
9
-
let conn_str = common::get_db_connection_string().await;
10
-
sqlx::postgres::PgPoolOptions::new()
11
-
.max_connections(5)
12
-
.connect(&conn_str)
13
-
.await
14
-
.expect("Failed to connect to test database")
15
-
}
16
6
17
7
#[tokio::test]
18
8
async fn test_request_password_reset_creates_code() {
19
9
let client = common::client();
20
10
let base_url = common::base_url().await;
21
-
let pool = get_pool().await;
11
+
let pool = common::get_test_db_pool().await;
22
12
let handle = format!("pwreset-{}", uuid::Uuid::new_v4());
23
13
let email = format!("{}@example.com", handle);
24
14
let payload = json!({
···
50
40
"SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1",
51
41
email
52
42
)
53
-
.fetch_one(&pool)
43
+
.fetch_one(pool)
54
44
.await
55
45
.expect("User not found");
56
46
assert!(user.password_reset_code.is_some());
···
80
70
async fn test_reset_password_with_valid_token() {
81
71
let client = common::client();
82
72
let base_url = common::base_url().await;
83
-
let pool = get_pool().await;
73
+
let pool = common::get_test_db_pool().await;
84
74
let handle = format!("pwreset2-{}", uuid::Uuid::new_v4());
85
75
let email = format!("{}@example.com", handle);
86
76
let old_password = "Oldpass123!";
···
117
107
"SELECT password_reset_code FROM users WHERE email = $1",
118
108
email
119
109
)
120
-
.fetch_one(&pool)
110
+
.fetch_one(pool)
121
111
.await
122
112
.expect("User not found");
123
113
let token = user.password_reset_code.expect("No reset code");
···
138
128
"SELECT password_reset_code, password_reset_code_expires_at FROM users WHERE email = $1",
139
129
email
140
130
)
141
-
.fetch_one(&pool)
131
+
.fetch_one(pool)
142
132
.await
143
133
.expect("User not found");
144
134
assert!(user.password_reset_code.is_none());
···
196
186
async fn test_reset_password_with_expired_token() {
197
187
let client = common::client();
198
188
let base_url = common::base_url().await;
199
-
let pool = get_pool().await;
189
+
let pool = common::get_test_db_pool().await;
200
190
let handle = format!("pwreset3-{}", uuid::Uuid::new_v4());
201
191
let email = format!("{}@example.com", handle);
202
192
let payload = json!({
···
228
218
"SELECT password_reset_code FROM users WHERE email = $1",
229
219
email
230
220
)
231
-
.fetch_one(&pool)
221
+
.fetch_one(pool)
232
222
.await
233
223
.expect("User not found");
234
224
let token = user.password_reset_code.expect("No reset code");
···
236
226
"UPDATE users SET password_reset_code_expires_at = NOW() - INTERVAL '1 hour' WHERE email = $1",
237
227
email
238
228
)
239
-
.execute(&pool)
229
+
.execute(pool)
240
230
.await
241
231
.expect("Failed to expire token");
242
232
let res = client
···
260
250
async fn test_reset_password_invalidates_sessions() {
261
251
let client = common::client();
262
252
let base_url = common::base_url().await;
263
-
let pool = get_pool().await;
253
+
let pool = common::get_test_db_pool().await;
264
254
let handle = format!("pwreset4-{}", uuid::Uuid::new_v4());
265
255
let email = format!("{}@example.com", handle);
266
256
let payload = json!({
···
302
292
"SELECT password_reset_code FROM users WHERE email = $1",
303
293
email
304
294
)
305
-
.fetch_one(&pool)
295
+
.fetch_one(pool)
306
296
.await
307
297
.expect("User not found");
308
298
let token = user.password_reset_code.expect("No reset code");
···
348
338
349
339
#[tokio::test]
350
340
async fn test_reset_password_creates_notification() {
351
-
let pool = get_pool().await;
341
+
let pool = common::get_test_db_pool().await;
352
342
let client = common::client();
353
343
let base_url = common::base_url().await;
354
344
let handle = format!("pwreset5-{}", uuid::Uuid::new_v4());
···
369
359
.expect("Failed to create account");
370
360
assert_eq!(res.status(), StatusCode::OK);
371
361
let user = sqlx::query!("SELECT id FROM users WHERE email = $1", email)
372
-
.fetch_one(&pool)
362
+
.fetch_one(pool)
373
363
.await
374
364
.expect("User not found");
375
365
let initial_count: i64 = sqlx::query_scalar!(
376
366
"SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'",
377
367
user.id
378
368
)
379
-
.fetch_one(&pool)
369
+
.fetch_one(pool)
380
370
.await
381
371
.expect("Failed to count")
382
372
.unwrap_or(0);
···
394
384
"SELECT COUNT(*) FROM comms_queue WHERE user_id = $1 AND comms_type = 'password_reset'",
395
385
user.id
396
386
)
397
-
.fetch_one(&pool)
387
+
.fetch_one(pool)
398
388
.await
399
389
.expect("Failed to count")
400
390
.unwrap_or(0);
+6
-16
tests/signing_key.rs
+6
-16
tests/signing_key.rs
···
3
3
use helpers::verify_new_account;
4
4
use reqwest::StatusCode;
5
5
use serde_json::{Value, json};
6
-
use sqlx::PgPool;
7
-
8
-
async fn get_pool() -> PgPool {
9
-
let conn_str = common::get_db_connection_string().await;
10
-
sqlx::postgres::PgPoolOptions::new()
11
-
.max_connections(5)
12
-
.connect(&conn_str)
13
-
.await
14
-
.expect("Failed to connect to test database")
15
-
}
16
6
17
7
#[tokio::test]
18
8
async fn test_reserve_signing_key_without_did() {
···
41
31
async fn test_reserve_signing_key_with_did() {
42
32
let client = common::client();
43
33
let base_url = common::base_url().await;
44
-
let pool = get_pool().await;
34
+
let pool = common::get_test_db_pool().await;
45
35
let target_did = "did:plc:test123456";
46
36
let res = client
47
37
.post(format!(
···
60
50
"SELECT did, public_key_did_key FROM reserved_signing_keys WHERE public_key_did_key = $1",
61
51
signing_key
62
52
)
63
-
.fetch_one(&pool)
53
+
.fetch_one(pool)
64
54
.await
65
55
.expect("Reserved key not found in database");
66
56
assert_eq!(row.did.as_deref(), Some(target_did));
···
71
61
async fn test_reserve_signing_key_stores_private_key() {
72
62
let client = common::client();
73
63
let base_url = common::base_url().await;
74
-
let pool = get_pool().await;
64
+
let pool = common::get_test_db_pool().await;
75
65
let res = client
76
66
.post(format!(
77
67
"{}/xrpc/com.atproto.server.reserveSigningKey",
···
88
78
"SELECT private_key_bytes, expires_at, used_at FROM reserved_signing_keys WHERE public_key_did_key = $1",
89
79
signing_key
90
80
)
91
-
.fetch_one(&pool)
81
+
.fetch_one(pool)
92
82
.await
93
83
.expect("Reserved key not found in database");
94
84
assert_eq!(
···
161
151
async fn test_create_account_with_reserved_signing_key() {
162
152
let client = common::client();
163
153
let base_url = common::base_url().await;
164
-
let pool = get_pool().await;
154
+
let pool = common::get_test_db_pool().await;
165
155
let res = client
166
156
.post(format!(
167
157
"{}/xrpc/com.atproto.server.reserveSigningKey",
···
199
189
"SELECT used_at FROM reserved_signing_keys WHERE public_key_did_key = $1",
200
190
signing_key
201
191
)
202
-
.fetch_one(&pool)
192
+
.fetch_one(pool)
203
193
.await
204
194
.expect("Reserved key not found");
205
195
assert!(