An easy-to-host PDS on the ATProtocol, MacOS. Grandma-approved.

fix(relay): address PR review issues for MM-74 describeServer

- relay.dev.toml: change available_user_domains to ["localhost"] so dev
startup doesn't fail validation out of the box
- config.rs: return ConfigError::Invalid (not MissingField) when
available_user_domains is present but empty; update doc comment to
list it as a required field
- app.rs: remove stale #[allow(dead_code)] on config field now that
describe_server reads it
- describe_server.rs: remove stale TODO/Parameters/trade-offs comment
from resolve_did; add two handler-level tests asserting the did field
(derived from public_url and from explicit server_did config)

authored by malpercio.dev and committed by

Tangled 464a8398 cc7f0292

+69 -29
+14 -12
crates/common/src/config.rs
··· 133 133 134 134 /// Validate a [`RawConfig`] and build a [`Config`], applying defaults for optional fields. 135 135 /// 136 - /// Required fields: `data_dir`, `public_url`. 136 + /// Required fields: `data_dir`, `public_url`, `available_user_domains` (non-empty). 137 137 /// Defaults: `bind_address = "0.0.0.0"`, `port = 8080`, 138 138 /// `database_url = "{data_dir}/relay.db"` (derived; fails if `data_dir` is non-UTF-8). 139 139 pub(crate) fn validate_and_build(raw: RawConfig) -> Result<Config, ConfigError> { ··· 159 159 let public_url = raw.public_url.ok_or(ConfigError::MissingField { 160 160 field: "public_url", 161 161 })?; 162 - let available_user_domains = raw.available_user_domains.unwrap_or_default(); 163 - if available_user_domains.is_empty() { 164 - return Err(ConfigError::MissingField { 162 + let available_user_domains = raw 163 + .available_user_domains 164 + .ok_or(ConfigError::MissingField { 165 165 field: "available_user_domains", 166 - }); 166 + })?; 167 + if available_user_domains.is_empty() { 168 + return Err(ConfigError::Invalid( 169 + "available_user_domains must contain at least one domain".to_string(), 170 + )); 167 171 } 168 172 let invite_code_required = raw.invite_code_required.unwrap_or(true); 169 173 ··· 409 413 } 410 414 411 415 #[test] 412 - fn available_user_domains_empty_returns_error() { 416 + fn available_user_domains_empty_returns_invalid_error() { 413 417 let raw = RawConfig { 414 418 data_dir: Some("/var/pds".to_string()), 415 419 public_url: Some("https://pds.example.com".to_string()), ··· 418 422 }; 419 423 let err = validate_and_build(raw).unwrap_err(); 420 424 421 - assert!(matches!( 422 - err, 423 - ConfigError::MissingField { 424 - field: "available_user_domains" 425 - } 426 - )); 425 + assert!(matches!(err, ConfigError::Invalid(_))); 426 + assert!(err 427 + .to_string() 428 + .contains("available_user_domains must contain at least one domain")); 427 429 } 428 430 429 431 #[test]
-1
crates/relay/src/app.rs
··· 10 10 /// Shared application state cloned into every request handler via Axum's `State` extractor. 11 11 #[derive(Clone)] 12 12 pub struct AppState { 13 - #[allow(dead_code)] 14 13 pub config: Arc<Config>, 15 14 pub db: sqlx::SqlitePool, 16 15 }
+54 -15
crates/relay/src/routes/describe_server.rs
··· 42 42 43 43 /// Resolve the DID to return in the `did` field. 44 44 /// 45 - /// The ATProto Lexicon marks `did` as required, but in Wave 1 the server DID may not be 46 - /// configured — DID generation is deferred to Wave 3. This function decides what to surface 47 - /// in the meantime. 48 - /// 49 - /// TODO: implement this function (5-10 lines). 50 - /// 51 - /// Parameters: 52 - /// - `server_did`: the configured `server_did` value, if any 53 - /// - `public_url`: the server's configured public URL (e.g. "https://pds.example.com") 54 - /// 55 - /// Consider the trade-offs: 56 - /// - Returning `""` is the simplest option; the Bluesky app tolerates it during initial setup 57 - /// - Deriving `did:web:<hostname>` from `public_url` is a valid DID and more semantically correct 58 - /// - Whatever you choose becomes the pattern for how Wave 3 replaces the placeholder 45 + /// Returns the configured `server_did` verbatim when present. Otherwise derives a `did:web` 46 + /// DID from the hostname in `public_url` as a placeholder until Wave 3 generates a real DID. 59 47 fn resolve_did(server_did: &Option<String>, public_url: &str) -> String { 60 48 if let Some(did) = server_did { 61 49 return did.clone(); ··· 105 93 }; 106 94 use tower::ServiceExt; 107 95 108 - use crate::app::{app, test_state}; 96 + use std::sync::Arc; 97 + 98 + use crate::app::{app, test_state, AppState}; 99 + 100 + #[tokio::test] 101 + async fn describe_server_did_derived_from_public_url() { 102 + // test_state() sets public_url = "https://test.example.com", server_did = None 103 + let response = app(test_state().await) 104 + .oneshot( 105 + Request::builder() 106 + .uri("/xrpc/com.atproto.server.describeServer") 107 + .body(Body::empty()) 108 + .unwrap(), 109 + ) 110 + .await 111 + .unwrap(); 112 + 113 + let body = axum::body::to_bytes(response.into_body(), 4096) 114 + .await 115 + .unwrap(); 116 + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); 117 + 118 + assert_eq!(json["did"], "did:web:test.example.com"); 119 + } 120 + 121 + #[tokio::test] 122 + async fn describe_server_did_from_config() { 123 + let base = test_state().await; 124 + let mut config = (*base.config).clone(); 125 + config.server_did = Some("did:plc:configured123".to_string()); 126 + let state = AppState { 127 + config: Arc::new(config), 128 + db: base.db, 129 + }; 130 + 131 + let response = app(state) 132 + .oneshot( 133 + Request::builder() 134 + .uri("/xrpc/com.atproto.server.describeServer") 135 + .body(Body::empty()) 136 + .unwrap(), 137 + ) 138 + .await 139 + .unwrap(); 140 + 141 + let body = axum::body::to_bytes(response.into_body(), 4096) 142 + .await 143 + .unwrap(); 144 + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); 145 + 146 + assert_eq!(json["did"], "did:plc:configured123"); 147 + } 109 148 110 149 #[test] 111 150 fn resolve_did_returns_configured_did() {
+1 -1
relay.dev.toml
··· 13 13 database_url = ":memory:" 14 14 15 15 # Required: at least one handle domain your PDS serves (e.g. ["your.domain"]) 16 - available_user_domains = [] 16 + available_user_domains = ["localhost"] 17 17 invite_code_required = true 18 18 19 19 # Optional: set to your server's DID once generated in Wave 3