···11+ALTER TABLE users ADD COLUMN IF NOT EXISTS delete_after TIMESTAMPTZ;
+7
migrations/20251240_add_block_count.sql
···11+CREATE TABLE IF NOT EXISTS user_blocks (
22+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
33+ block_cid BYTEA NOT NULL,
44+ PRIMARY KEY (user_id, block_cid)
55+);
66+77+CREATE INDEX IF NOT EXISTS idx_user_blocks_user_id ON user_blocks(user_id);
···11+mod common;
22+mod helpers;
33+use common::*;
44+use reqwest::StatusCode;
55+use serde_json::{Value, json};
66+77+#[tokio::test]
88+async fn test_check_account_status_returns_correct_block_count() {
99+ let client = client();
1010+ let base = base_url().await;
1111+ let (access_jwt, did) = create_account_and_login(&client).await;
1212+1313+ let status1 = client
1414+ .get(format!("{}/xrpc/com.atproto.server.checkAccountStatus", base))
1515+ .bearer_auth(&access_jwt)
1616+ .send()
1717+ .await
1818+ .unwrap();
1919+ assert_eq!(status1.status(), StatusCode::OK);
2020+ let body1: Value = status1.json().await.unwrap();
2121+ let initial_blocks = body1["repoBlocks"].as_i64().unwrap();
2222+ assert!(initial_blocks >= 2, "New account should have at least 2 blocks (commit + empty MST)");
2323+2424+ let create_res = client
2525+ .post(format!("{}/xrpc/com.atproto.repo.createRecord", base))
2626+ .bearer_auth(&access_jwt)
2727+ .json(&json!({
2828+ "repo": did,
2929+ "collection": "app.bsky.feed.post",
3030+ "record": {
3131+ "$type": "app.bsky.feed.post",
3232+ "text": "Test post for block counting",
3333+ "createdAt": chrono::Utc::now().to_rfc3339()
3434+ }
3535+ }))
3636+ .send()
3737+ .await
3838+ .unwrap();
3939+ assert_eq!(create_res.status(), StatusCode::OK);
4040+ let create_body: Value = create_res.json().await.unwrap();
4141+ let rkey = create_body["uri"].as_str().unwrap().split('/').last().unwrap().to_string();
4242+4343+ let status2 = client
4444+ .get(format!("{}/xrpc/com.atproto.server.checkAccountStatus", base))
4545+ .bearer_auth(&access_jwt)
4646+ .send()
4747+ .await
4848+ .unwrap();
4949+ let body2: Value = status2.json().await.unwrap();
5050+ let after_create_blocks = body2["repoBlocks"].as_i64().unwrap();
5151+ assert!(after_create_blocks > initial_blocks, "Block count should increase after creating a record");
5252+5353+ let delete_res = client
5454+ .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base))
5555+ .bearer_auth(&access_jwt)
5656+ .json(&json!({
5757+ "repo": did,
5858+ "collection": "app.bsky.feed.post",
5959+ "rkey": rkey
6060+ }))
6161+ .send()
6262+ .await
6363+ .unwrap();
6464+ assert_eq!(delete_res.status(), StatusCode::OK);
6565+6666+ let status3 = client
6767+ .get(format!("{}/xrpc/com.atproto.server.checkAccountStatus", base))
6868+ .bearer_auth(&access_jwt)
6969+ .send()
7070+ .await
7171+ .unwrap();
7272+ let body3: Value = status3.json().await.unwrap();
7373+ let after_delete_blocks = body3["repoBlocks"].as_i64().unwrap();
7474+ assert!(
7575+ after_delete_blocks >= after_create_blocks,
7676+ "Block count should not decrease after deleting a record (was {}, now {})",
7777+ after_create_blocks,
7878+ after_delete_blocks
7979+ );
8080+}
8181+8282+#[tokio::test]
8383+async fn test_check_account_status_returns_valid_repo_rev() {
8484+ let client = client();
8585+ let base = base_url().await;
8686+ let (access_jwt, _) = create_account_and_login(&client).await;
8787+8888+ let status = client
8989+ .get(format!("{}/xrpc/com.atproto.server.checkAccountStatus", base))
9090+ .bearer_auth(&access_jwt)
9191+ .send()
9292+ .await
9393+ .unwrap();
9494+ assert_eq!(status.status(), StatusCode::OK);
9595+ let body: Value = status.json().await.unwrap();
9696+9797+ let repo_rev = body["repoRev"].as_str().unwrap();
9898+ assert!(!repo_rev.is_empty(), "repoRev should not be empty");
9999+ assert!(repo_rev.chars().all(|c| c.is_alphanumeric()), "repoRev should be alphanumeric TID");
100100+}
101101+102102+#[tokio::test]
103103+async fn test_check_account_status_valid_did_is_true_for_active_account() {
104104+ let client = client();
105105+ let base = base_url().await;
106106+ let (access_jwt, _) = create_account_and_login(&client).await;
107107+108108+ let status = client
109109+ .get(format!("{}/xrpc/com.atproto.server.checkAccountStatus", base))
110110+ .bearer_auth(&access_jwt)
111111+ .send()
112112+ .await
113113+ .unwrap();
114114+ assert_eq!(status.status(), StatusCode::OK);
115115+ let body: Value = status.json().await.unwrap();
116116+117117+ assert_eq!(body["validDid"], true, "validDid should be true for active account with correct DID document");
118118+ assert_eq!(body["activated"], true, "activated should be true for active account");
119119+}
120120+121121+#[tokio::test]
122122+async fn test_deactivate_account_with_delete_after() {
123123+ let client = client();
124124+ let base = base_url().await;
125125+ let (access_jwt, _) = create_account_and_login(&client).await;
126126+127127+ let future_time = chrono::Utc::now() + chrono::Duration::hours(24);
128128+ let delete_after = future_time.to_rfc3339();
129129+130130+ let deactivate = client
131131+ .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base))
132132+ .bearer_auth(&access_jwt)
133133+ .json(&json!({
134134+ "deleteAfter": delete_after
135135+ }))
136136+ .send()
137137+ .await
138138+ .unwrap();
139139+ assert_eq!(deactivate.status(), StatusCode::OK);
140140+141141+ let status = client
142142+ .get(format!("{}/xrpc/com.atproto.server.checkAccountStatus", base))
143143+ .bearer_auth(&access_jwt)
144144+ .send()
145145+ .await
146146+ .unwrap();
147147+ assert_eq!(status.status(), StatusCode::OK);
148148+ let body: Value = status.json().await.unwrap();
149149+ assert_eq!(body["activated"], false, "Account should be deactivated");
150150+}
151151+152152+#[tokio::test]
153153+async fn test_create_account_returns_did_doc() {
154154+ let client = client();
155155+ let base = base_url().await;
156156+157157+ let handle = format!("diddoctest-{}", uuid::Uuid::new_v4());
158158+ let payload = json!({
159159+ "handle": handle,
160160+ "email": format!("{}@example.com", handle),
161161+ "password": "Testpass123!"
162162+ });
163163+164164+ let create_res = client
165165+ .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
166166+ .json(&payload)
167167+ .send()
168168+ .await
169169+ .unwrap();
170170+ assert_eq!(create_res.status(), StatusCode::OK);
171171+ let body: Value = create_res.json().await.unwrap();
172172+173173+ assert!(body["accessJwt"].is_string(), "accessJwt should always be returned");
174174+ assert!(body["refreshJwt"].is_string(), "refreshJwt should always be returned");
175175+ assert!(body["did"].is_string(), "did should be returned");
176176+177177+ if body["didDoc"].is_object() {
178178+ let did_doc = &body["didDoc"];
179179+ assert!(did_doc["id"].is_string(), "didDoc should have id field");
180180+ }
181181+}
182182+183183+#[tokio::test]
184184+async fn test_create_account_always_returns_tokens() {
185185+ let client = client();
186186+ let base = base_url().await;
187187+188188+ let handle = format!("tokentest-{}", uuid::Uuid::new_v4());
189189+ let payload = json!({
190190+ "handle": handle,
191191+ "email": format!("{}@example.com", handle),
192192+ "password": "Testpass123!"
193193+ });
194194+195195+ let create_res = client
196196+ .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
197197+ .json(&payload)
198198+ .send()
199199+ .await
200200+ .unwrap();
201201+ assert_eq!(create_res.status(), StatusCode::OK);
202202+ let body: Value = create_res.json().await.unwrap();
203203+204204+ let access_jwt = body["accessJwt"].as_str().expect("accessJwt should be present");
205205+ let refresh_jwt = body["refreshJwt"].as_str().expect("refreshJwt should be present");
206206+207207+ assert!(!access_jwt.is_empty(), "accessJwt should not be empty");
208208+ assert!(!refresh_jwt.is_empty(), "refreshJwt should not be empty");
209209+210210+ let parts: Vec<&str> = access_jwt.split('.').collect();
211211+ assert_eq!(parts.len(), 3, "accessJwt should be a valid JWT with 3 parts");
212212+}
213213+214214+#[tokio::test]
215215+async fn test_describe_server_has_links_and_contact() {
216216+ let client = client();
217217+ let base = base_url().await;
218218+219219+ let describe = client
220220+ .get(format!("{}/xrpc/com.atproto.server.describeServer", base))
221221+ .send()
222222+ .await
223223+ .unwrap();
224224+ assert_eq!(describe.status(), StatusCode::OK);
225225+ let body: Value = describe.json().await.unwrap();
226226+227227+ assert!(body.get("links").is_some(), "describeServer should include links object");
228228+ assert!(body.get("contact").is_some(), "describeServer should include contact object");
229229+230230+ let links = &body["links"];
231231+ assert!(links.get("privacyPolicy").is_some() || links["privacyPolicy"].is_null(),
232232+ "links should have privacyPolicy field (can be null)");
233233+ assert!(links.get("termsOfService").is_some() || links["termsOfService"].is_null(),
234234+ "links should have termsOfService field (can be null)");
235235+236236+ let contact = &body["contact"];
237237+ assert!(contact.get("email").is_some() || contact["email"].is_null(),
238238+ "contact should have email field (can be null)");
239239+}
240240+241241+#[tokio::test]
242242+async fn test_delete_account_password_max_length() {
243243+ let client = client();
244244+ let base = base_url().await;
245245+246246+ let handle = format!("pwdlentest-{}", uuid::Uuid::new_v4());
247247+ let payload = json!({
248248+ "handle": handle,
249249+ "email": format!("{}@example.com", handle),
250250+ "password": "Testpass123!"
251251+ });
252252+253253+ let create_res = client
254254+ .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
255255+ .json(&payload)
256256+ .send()
257257+ .await
258258+ .unwrap();
259259+ assert_eq!(create_res.status(), StatusCode::OK);
260260+ let body: Value = create_res.json().await.unwrap();
261261+ let did = body["did"].as_str().unwrap();
262262+263263+ let too_long_password = "a".repeat(600);
264264+ let delete_res = client
265265+ .post(format!("{}/xrpc/com.atproto.server.deleteAccount", base))
266266+ .json(&json!({
267267+ "did": did,
268268+ "password": too_long_password,
269269+ "token": "fake-token"
270270+ }))
271271+ .send()
272272+ .await
273273+ .unwrap();
274274+275275+ assert_eq!(delete_res.status(), StatusCode::BAD_REQUEST);
276276+ let error_body: Value = delete_res.json().await.unwrap();
277277+ assert!(error_body["message"].as_str().unwrap().contains("password length")
278278+ || error_body["error"].as_str().unwrap() == "InvalidRequest");
279279+}
+4-1
tests/common/mod.rs
···466466 .await
467467 .expect("Failed to mark user as admin");
468468 }
469469+ let verification_required = body["verificationRequired"].as_bool().unwrap_or(true);
469470 if let Some(access_jwt) = body["accessJwt"].as_str() {
470470- return (access_jwt.to_string(), did);
471471+ if !verification_required {
472472+ return (access_jwt.to_string(), did);
473473+ }
471474 }
472475 let body_text: String = sqlx::query_scalar!(
473476 "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",