Our Personal Data Server from scratch! tranquil.farm
pds rust database fun oauth atproto
237
fork

Configure Feed

Select the types of activity you want to include in your feed.

Admin endoints vs ref

+574 -334
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 41 41 -e POSTGRES_DB=postgres \ 42 42 -P \ 43 43 --label tranquil_pds_test=true \ 44 - postgres:18-alpine >/dev/null 44 + postgres:18-alpine \ 45 + -c max_connections=500 >/dev/null 45 46 echo "Starting MinIO..." 46 47 $CONTAINER_CMD run -d \ 47 48 --name "${CONTAINER_PREFIX}-minio" \
+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
··· 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
··· 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
··· 135 135 } 136 136 } 137 137 if let Some(blob_cid) = &params.blob { 138 + let did = match &params.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
··· 780 780 } 781 781 } 782 782 783 - async fn update_plc_handle( 783 + pub async fn update_plc_handle( 784 784 state: &AppState, 785 785 did: &str, 786 786 new_handle: &str,
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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!(