···11+ALTER TYPE notification_type ADD VALUE IF NOT EXISTS 'channel_verification';
22+33+CREATE TABLE IF NOT EXISTS channel_verifications (
44+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
55+ channel notification_channel NOT NULL,
66+ code TEXT NOT NULL,
77+ expires_at TIMESTAMPTZ NOT NULL,
88+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
99+ PRIMARY KEY (user_id, channel)
1010+);
1111+1212+CREATE INDEX IF NOT EXISTS idx_channel_verifications_expires ON channel_verifications(expires_at);
···11+ALTER TABLE channel_verifications ADD COLUMN pending_identifier TEXT;
22+33+INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at)
44+SELECT id, 'email', email_confirmation_code, email_pending_verification, email_confirmation_code_expires_at
55+FROM users
66+WHERE email_confirmation_code IS NOT NULL AND email_confirmation_code_expires_at IS NOT NULL;
77+88+ALTER TABLE users
99+DROP COLUMN email_confirmation_code,
1010+DROP COLUMN email_confirmation_code_expires_at,
1111+DROP COLUMN email_pending_verification;
+2
src/api/admin/mod.rs
···11pub mod account;
22pub mod invite;
33+pub mod server_stats;
34pub mod status;
4556pub use account::{
···910pub use invite::{
1011 disable_account_invites, disable_invite_codes, enable_account_invites, get_invite_codes,
1112};
1313+pub use server_stats::get_server_stats;
1214pub use status::{get_subject_status, update_subject_status};
···1313pub mod server;
1414pub mod temp;
1515pub mod validation;
1616+pub mod verification;
16171718pub use error::ApiError;
1819pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_did, validate_limit};
···11+mod common;
22+use common::{base_url, client, create_account_and_login, get_db_connection_string};
33+use bspds::notifications::{NewNotification, NotificationType, enqueue_notification};
44+use serde_json::{Value, json};
55+use sqlx::PgPool;
66+77+async fn get_pool() -> PgPool {
88+ let conn_str = get_db_connection_string().await;
99+ sqlx::postgres::PgPoolOptions::new()
1010+ .max_connections(5)
1111+ .connect(&conn_str)
1212+ .await
1313+ .expect("Failed to connect to test database")
1414+}
1515+1616+#[tokio::test]
1717+async fn test_get_notification_history() {
1818+ let client = client();
1919+ let base = base_url().await;
2020+ let pool = get_pool().await;
2121+ let (token, did) = create_account_and_login(&client).await;
2222+2323+ let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
2424+ .fetch_one(&pool)
2525+ .await
2626+ .expect("User not found");
2727+2828+ for i in 0..3 {
2929+ let notification = NewNotification::email(
3030+ user_id,
3131+ NotificationType::Welcome,
3232+ "test@example.com".to_string(),
3333+ format!("Subject {}", i),
3434+ format!("Body {}", i),
3535+ );
3636+ enqueue_notification(&pool, notification).await.expect("Failed to enqueue");
3737+ }
3838+3939+ let resp = client
4040+ .get(format!("{}/xrpc/com.bspds.account.getNotificationHistory", base))
4141+ .header("Authorization", format!("Bearer {}", token))
4242+ .send()
4343+ .await
4444+ .unwrap();
4545+4646+ assert_eq!(resp.status(), 200);
4747+ let body: Value = resp.json().await.unwrap();
4848+ let notifications = body["notifications"].as_array().unwrap();
4949+ assert_eq!(notifications.len(), 5);
5050+5151+ assert_eq!(notifications[0]["subject"], "Subject 2");
5252+ assert_eq!(notifications[1]["subject"], "Subject 1");
5353+ assert_eq!(notifications[2]["subject"], "Subject 0");
5454+}
5555+5656+#[tokio::test]
5757+async fn test_verify_channel_discord() {
5858+ let client = client();
5959+ let base = base_url().await;
6060+ let (token, did) = create_account_and_login(&client).await;
6161+6262+ let prefs = json!({
6363+ "discordId": "123456789"
6464+ });
6565+ let resp = client
6666+ .post(format!("{}/xrpc/com.bspds.account.updateNotificationPrefs", base))
6767+ .header("Authorization", format!("Bearer {}", token))
6868+ .json(&prefs)
6969+ .send()
7070+ .await
7171+ .unwrap();
7272+ assert_eq!(resp.status(), 200);
7373+ let body: Value = resp.json().await.unwrap();
7474+ assert!(body["verificationRequired"].as_array().unwrap().contains(&json!("discord")));
7575+7676+ let pool = get_pool().await;
7777+ let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
7878+ .fetch_one(&pool)
7979+ .await
8080+ .expect("User not found");
8181+8282+ let code: String = sqlx::query_scalar!(
8383+ "SELECT code FROM channel_verifications WHERE user_id = $1 AND channel = 'discord'",
8484+ user_id
8585+ )
8686+ .fetch_one(&pool)
8787+ .await
8888+ .expect("Verification code not found");
8989+9090+ let input = json!({
9191+ "channel": "discord",
9292+ "code": code
9393+ });
9494+ let resp = client
9595+ .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base))
9696+ .header("Authorization", format!("Bearer {}", token))
9797+ .json(&input)
9898+ .send()
9999+ .await
100100+ .unwrap();
101101+ assert_eq!(resp.status(), 200);
102102+103103+ let resp = client
104104+ .get(format!("{}/xrpc/com.bspds.account.getNotificationPrefs", base))
105105+ .header("Authorization", format!("Bearer {}", token))
106106+ .send()
107107+ .await
108108+ .unwrap();
109109+ let body: Value = resp.json().await.unwrap();
110110+ assert_eq!(body["discordVerified"], true);
111111+ assert_eq!(body["discordId"], "123456789");
112112+}
113113+114114+#[tokio::test]
115115+async fn test_verify_channel_invalid_code() {
116116+ let client = client();
117117+ let base = base_url().await;
118118+ let (token, _did) = create_account_and_login(&client).await;
119119+120120+ let prefs = json!({
121121+ "telegramUsername": "testuser"
122122+ });
123123+ let resp = client
124124+ .post(format!("{}/xrpc/com.bspds.account.updateNotificationPrefs", base))
125125+ .header("Authorization", format!("Bearer {}", token))
126126+ .json(&prefs)
127127+ .send()
128128+ .await
129129+ .unwrap();
130130+ assert_eq!(resp.status(), 200);
131131+132132+ let input = json!({
133133+ "channel": "telegram",
134134+ "code": "000000"
135135+ });
136136+ let resp = client
137137+ .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base))
138138+ .header("Authorization", format!("Bearer {}", token))
139139+ .json(&input)
140140+ .send()
141141+ .await
142142+ .unwrap();
143143+ assert_eq!(resp.status(), 400);
144144+}
145145+146146+#[tokio::test]
147147+async fn test_verify_channel_not_set() {
148148+ let client = client();
149149+ let base = base_url().await;
150150+ let (token, _did) = create_account_and_login(&client).await;
151151+152152+ let input = json!({
153153+ "channel": "signal",
154154+ "code": "123456"
155155+ });
156156+ let resp = client
157157+ .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base))
158158+ .header("Authorization", format!("Bearer {}", token))
159159+ .json(&input)
160160+ .send()
161161+ .await
162162+ .unwrap();
163163+ assert_eq!(resp.status(), 400);
164164+}
165165+166166+#[tokio::test]
167167+async fn test_update_email_via_notification_prefs() {
168168+ let client = client();
169169+ let base = base_url().await;
170170+ let pool = get_pool().await;
171171+ let (token, did) = create_account_and_login(&client).await;
172172+173173+ let prefs = json!({
174174+ "email": "newemail@example.com"
175175+ });
176176+ let resp = client
177177+ .post(format!("{}/xrpc/com.bspds.account.updateNotificationPrefs", base))
178178+ .header("Authorization", format!("Bearer {}", token))
179179+ .json(&prefs)
180180+ .send()
181181+ .await
182182+ .unwrap();
183183+ assert_eq!(resp.status(), 200);
184184+ let body: Value = resp.json().await.unwrap();
185185+ assert!(body["verificationRequired"].as_array().unwrap().contains(&json!("email")));
186186+187187+ let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
188188+ .fetch_one(&pool)
189189+ .await
190190+ .expect("User not found");
191191+192192+ let code: String = sqlx::query_scalar!(
193193+ "SELECT code FROM channel_verifications WHERE user_id = $1 AND channel = 'email'",
194194+ user_id
195195+ )
196196+ .fetch_one(&pool)
197197+ .await
198198+ .expect("Verification code not found");
199199+200200+ let input = json!({
201201+ "channel": "email",
202202+ "code": code
203203+ });
204204+ let resp = client
205205+ .post(format!("{}/xrpc/com.bspds.account.confirmChannelVerification", base))
206206+ .header("Authorization", format!("Bearer {}", token))
207207+ .json(&input)
208208+ .send()
209209+ .await
210210+ .unwrap();
211211+ assert_eq!(resp.status(), 200);
212212+213213+ let resp = client
214214+ .get(format!("{}/xrpc/com.bspds.account.getNotificationPrefs", base))
215215+ .header("Authorization", format!("Bearer {}", token))
216216+ .send()
217217+ .await
218218+ .unwrap();
219219+ let body: Value = resp.json().await.unwrap();
220220+ assert_eq!(body["email"], "newemail@example.com");
221221+}
+41
tests/admin_stats.rs
···11+mod common;
22+use common::{base_url, client, create_account_and_login};
33+use serde_json::Value;
44+55+#[tokio::test]
66+async fn test_get_server_stats() {
77+ let client = client();
88+ let base = base_url().await;
99+ let (token1, _) = create_account_and_login(&client).await;
1010+1111+ let (_, _) = create_account_and_login(&client).await;
1212+1313+ let resp = client
1414+ .get(format!("{}/xrpc/com.bspds.admin.getServerStats", base))
1515+ .header("Authorization", format!("Bearer {}", token1))
1616+ .send()
1717+ .await
1818+ .unwrap();
1919+2020+ assert_eq!(resp.status(), 200);
2121+ let body: Value = resp.json().await.unwrap();
2222+2323+ let user_count = body["userCount"].as_i64().unwrap();
2424+ assert!(user_count >= 2);
2525+2626+ assert!(body["repoCount"].is_number());
2727+ assert!(body["recordCount"].is_number());
2828+ assert!(body["blobStorageBytes"].is_number());
2929+}
3030+3131+#[tokio::test]
3232+async fn test_get_server_stats_no_auth() {
3333+ let client = client();
3434+ let base = base_url().await;
3535+ let resp = client
3636+ .get(format!("{}/xrpc/com.bspds.admin.getServerStats", base))
3737+ .send()
3838+ .await
3939+ .unwrap();
4040+ assert_eq!(resp.status(), 401);
4141+}
+9-6
tests/common/mod.rs
···7979 SERVER_URL.get_or_init(|| {
8080 let (tx, rx) = std::sync::mpsc::channel();
8181 std::thread::spawn(move || {
8282+ unsafe {
8383+ std::env::set_var("BSPDS_ALLOW_INSECURE_SECRETS", "1");
8484+ }
8285 if std::env::var("DOCKER_HOST").is_err() {
8386 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
8487 let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
···406409 .await
407410 .expect("Failed to connect to test database");
408411 let verification_code: String = sqlx::query_scalar!(
409409- "SELECT email_confirmation_code FROM users WHERE did = $1",
412412+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
410413 did
411414 )
412415 .fetch_one(&pool)
413416 .await
414414- .expect("Failed to get verification code")
415415- .expect("No verification code found");
417417+ .expect("Failed to get verification code");
418418+416419 let confirm_payload = json!({
417420 "did": did,
418421 "verificationCode": verification_code
···548551 .await
549552 .expect("Failed to connect to test database");
550553 let verification_code: String = sqlx::query_scalar!(
551551- "SELECT email_confirmation_code FROM users WHERE did = $1",
554554+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
552555 &did
553556 )
554557 .fetch_one(&pool)
555558 .await
556556- .expect("Failed to get verification code")
557557- .expect("No verification code found");
559559+ .expect("Failed to get verification code");
560560+558561 let confirm_payload = json!({
559562 "did": did,
560563 "verificationCode": verification_code
+34-19
tests/email_update.rs
···5959 assert_eq!(res.status(), StatusCode::OK);
6060 let body: Value = res.json().await.expect("Invalid JSON");
6161 assert_eq!(body["tokenRequired"], true);
6262- let user = sqlx::query!(
6363- "SELECT email_pending_verification, email_confirmation_code, email FROM users WHERE handle = $1",
6262+6363+ let verification = sqlx::query!(
6464+ "SELECT pending_identifier, code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
6465 handle
6566 )
6667 .fetch_one(&pool)
6768 .await
6868- .expect("User not found");
6969+ .expect("Verification not found");
7070+6971 assert_eq!(
7070- user.email_pending_verification.as_deref(),
7272+ verification.pending_identifier.as_deref(),
7173 Some(new_email.as_str())
7274 );
7373- assert!(user.email_confirmation_code.is_some());
7474- let code = user.email_confirmation_code.unwrap();
7575+ let code = verification.code;
7576 let res = client
7677 .post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
7778 .bearer_auth(&access_jwt)
···8485 .expect("Failed to confirm email");
8586 assert_eq!(res.status(), StatusCode::OK);
8687 let user = sqlx::query!(
8787- "SELECT email, email_pending_verification, email_confirmation_code FROM users WHERE handle = $1",
8888+ "SELECT email FROM users WHERE handle = $1",
8889 handle
8990 )
9091 .fetch_one(&pool)
9192 .await
9293 .expect("User not found");
9394 assert_eq!(user.email, Some(new_email));
9494- assert!(user.email_pending_verification.is_none());
9595- assert!(user.email_confirmation_code.is_none());
9595+9696+ let verification = sqlx::query!(
9797+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
9898+ handle
9999+ )
100100+ .fetch_optional(&pool)
101101+ .await
102102+ .expect("DB error");
103103+ assert!(verification.is_none());
96104}
9710598106#[tokio::test]
···174182 .await
175183 .expect("Failed to request email update");
176184 assert_eq!(res.status(), StatusCode::OK);
177177- let user = sqlx::query!(
178178- "SELECT email_confirmation_code FROM users WHERE handle = $1",
185185+ let verification = sqlx::query!(
186186+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
179187 handle
180188 )
181189 .fetch_one(&pool)
182190 .await
183183- .expect("User not found");
184184- let code = user.email_confirmation_code.unwrap();
191191+ .expect("Verification not found");
192192+ let code = verification.code;
185193 let res = client
186194 .post(format!("{}/xrpc/com.atproto.server.confirmEmail", base_url))
187195 .bearer_auth(&access_jwt)
···293301 .await
294302 .expect("Failed to request email update");
295303 assert_eq!(res.status(), StatusCode::OK);
296296- let user = sqlx::query!(
297297- "SELECT email_confirmation_code FROM users WHERE handle = $1",
304304+ let verification = sqlx::query!(
305305+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
298306 handle
299307 )
300308 .fetch_one(&pool)
301309 .await
302302- .expect("User not found");
303303- let code = user.email_confirmation_code.unwrap();
310310+ .expect("Verification not found");
311311+ let code = verification.code;
304312 let res = client
305313 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
306314 .bearer_auth(&access_jwt)
···313321 .expect("Failed to update email");
314322 assert_eq!(res.status(), StatusCode::OK);
315323 let user = sqlx::query!(
316316- "SELECT email, email_pending_verification FROM users WHERE handle = $1",
324324+ "SELECT email FROM users WHERE handle = $1",
317325 handle
318326 )
319327 .fetch_one(&pool)
320328 .await
321329 .expect("User not found");
322330 assert_eq!(user.email, Some(new_email));
323323- assert!(user.email_pending_verification.is_none());
331331+ let verification = sqlx::query!(
332332+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
333333+ handle
334334+ )
335335+ .fetch_optional(&pool)
336336+ .await
337337+ .expect("DB error");
338338+ assert!(verification.is_none());
324339}
325340326341#[tokio::test]
+2-3
tests/jwt_security.rs
···872872 .await
873873 .expect("Failed to connect to test database");
874874 let verification_code: String = sqlx::query_scalar!(
875875- "SELECT email_confirmation_code FROM users WHERE did = $1",
875875+ "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
876876 did
877877 )
878878 .fetch_one(&pool)
879879 .await
880880- .expect("Failed to get verification code")
881881- .expect("No verification code found");
880880+ .expect("Failed to get verification code");
882881 let confirm_res = http_client
883882 .post(format!("{}/xrpc/com.atproto.server.confirmSignup", url))
884883 .json(&json!({