this repo has no description

Admin endoints vs ref

lewis 9e57563a daa358e7

+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!(