use super::error::AppError;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FeaturesConfig {
#[serde(default)]
pub didcomm: bool,
#[serde(default)]
pub tsp: bool,
#[serde(default)]
pub rest_api: bool,
#[serde(default = "default_true")]
pub agent_names: bool,
#[serde(default = "default_deployment_mode")]
pub deployment_mode: String,
}
fn default_deployment_mode() -> String {
"standalone".to_string()
}
fn default_true() -> bool {
true
}
impl Default for FeaturesConfig {
fn default() -> Self {
Self {
didcomm: false,
tsp: false,
rest_api: false,
agent_names: true,
deployment_mode: default_deployment_mode(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransportSelection {
Didcomm,
Tsp,
Both,
}
impl TransportSelection {
pub fn as_flags(self) -> (bool, bool) {
match self {
Self::Didcomm => (true, false),
Self::Tsp => (false, true),
Self::Both => (true, true),
}
}
pub fn from_flags(didcomm: bool, tsp: bool) -> Option<Self> {
match (didcomm, tsp) {
(true, true) => Some(Self::Both),
(true, false) => Some(Self::Didcomm),
(false, true) => Some(Self::Tsp),
(false, false) => None,
}
}
pub fn parse(s: &str) -> Result<Self, AppError> {
match s.trim().to_ascii_lowercase().as_str() {
"didcomm" => Ok(Self::Didcomm),
"tsp" => Ok(Self::Tsp),
"both" | "didcomm+tsp" | "tsp+didcomm" => Ok(Self::Both),
other => Err(AppError::Config(format!(
"invalid transport '{other}' (expected 'didcomm', 'tsp', or 'both')"
))),
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Didcomm => "didcomm",
Self::Tsp => "tsp",
Self::Both => "both",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerConfig {
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
#[serde(default)]
pub trusted_proxies: Vec<String>,
#[serde(default)]
pub trusted_proxy_cidrs: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LogConfig {
#[serde(default = "default_log_level")]
pub level: String,
#[serde(default)]
pub format: LogFormat,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct StoreConfig {
#[serde(default = "default_data_dir")]
pub data_dir: PathBuf,
pub redis_url: Option<String>,
pub dynamodb_table_prefix: Option<String>,
pub dynamodb_region: Option<String>,
pub firestore_project: Option<String>,
pub firestore_database: Option<String>,
pub cosmosdb_connection_string: Option<String>,
pub cosmosdb_database: Option<String>,
pub cosmosdb_region: Option<String>,
}
impl std::fmt::Debug for StoreConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StoreConfig")
.field("data_dir", &self.data_dir)
.field("redis_url", &self.redis_url.as_ref().map(|_| "<redacted>"))
.field("dynamodb_table_prefix", &self.dynamodb_table_prefix)
.field("dynamodb_region", &self.dynamodb_region)
.field("firestore_project", &self.firestore_project)
.field("firestore_database", &self.firestore_database)
.field(
"cosmosdb_connection_string",
&self
.cosmosdb_connection_string
.as_ref()
.map(|_| "<redacted>"),
)
.field("cosmosdb_database", &self.cosmosdb_database)
.field("cosmosdb_region", &self.cosmosdb_region)
.finish()
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AuthConfig {
#[serde(default = "default_access_token_expiry")]
pub access_token_expiry: u64,
#[serde(default = "default_refresh_token_expiry")]
pub refresh_token_expiry: u64,
#[serde(default = "default_challenge_ttl")]
pub challenge_ttl: u64,
#[serde(default = "default_session_cleanup_interval")]
pub session_cleanup_interval: u64,
#[serde(default = "default_passkey_enrollment_ttl")]
pub passkey_enrollment_ttl: u64,
#[serde(default = "default_cleanup_ttl_minutes")]
pub cleanup_ttl_minutes: u64,
}
fn default_access_token_expiry() -> u64 {
900
}
fn default_refresh_token_expiry() -> u64 {
86400
}
fn default_challenge_ttl() -> u64 {
30
}
fn default_session_cleanup_interval() -> u64 {
600
}
fn default_passkey_enrollment_ttl() -> u64 {
86400
}
fn default_cleanup_ttl_minutes() -> u64 {
60
}
impl AuthConfig {
pub fn validate(&self) -> Result<(), AppError> {
if self.challenge_ttl < 10 {
return Err(AppError::Config(
"challenge_ttl must be at least 10 seconds".into(),
));
}
if self.session_cleanup_interval < 10 {
return Err(AppError::Config(
"session_cleanup_interval must be at least 10 seconds".into(),
));
}
if self.access_token_expiry < 30 {
return Err(AppError::Config(
"access_token_expiry must be at least 30 seconds".into(),
));
}
Ok(())
}
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
access_token_expiry: default_access_token_expiry(),
refresh_token_expiry: default_refresh_token_expiry(),
challenge_ttl: default_challenge_ttl(),
session_cleanup_interval: default_session_cleanup_interval(),
passkey_enrollment_ttl: default_passkey_enrollment_ttl(),
cleanup_ttl_minutes: default_cleanup_ttl_minutes(),
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum LogFormat {
#[default]
Text,
Json,
}
fn default_host() -> String {
"0.0.0.0".to_string()
}
fn default_port() -> u16 {
8530
}
fn default_log_level() -> String {
"info".to_string()
}
fn default_data_dir() -> PathBuf {
PathBuf::from("data/did-hosting-server")
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: default_host(),
port: default_port(),
trusted_proxies: Vec::new(),
trusted_proxy_cidrs: Vec::new(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct HostingConfig {
#[serde(default)]
pub bootstrap_domains: Vec<String>,
#[serde(default = "default_unassigned_purge_grace")]
pub unassigned_purge_grace: String,
#[serde(default = "default_disable_purge_grace")]
pub disable_purge_grace: String,
}
fn default_unassigned_purge_grace() -> String {
"2h".to_string()
}
fn default_disable_purge_grace() -> String {
"30d".to_string()
}
impl Default for HostingConfig {
fn default() -> Self {
Self {
bootstrap_domains: Vec::new(),
unassigned_purge_grace: default_unassigned_purge_grace(),
disable_purge_grace: default_disable_purge_grace(),
}
}
}
impl Default for LogConfig {
fn default() -> Self {
Self {
level: default_log_level(),
format: LogFormat::default(),
}
}
}
impl Default for StoreConfig {
fn default() -> Self {
Self {
data_dir: default_data_dir(),
redis_url: None,
dynamodb_table_prefix: None,
dynamodb_region: None,
firestore_project: None,
firestore_database: None,
cosmosdb_connection_string: None,
cosmosdb_database: None,
cosmosdb_region: None,
}
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct SecretsConfig {
pub aws_secret_name: Option<String>,
pub aws_region: Option<String>,
pub gcp_project: Option<String>,
pub gcp_secret_name: Option<String>,
pub azure_vault_url: Option<String>,
pub azure_secret_name: Option<String>,
#[serde(default = "default_keyring_service")]
pub keyring_service: String,
pub vault_addr: Option<String>,
#[serde(default = "default_vault_kv_mount")]
pub vault_kv_mount: String,
pub vault_secret_path: Option<String>,
#[serde(default = "default_vault_secret_key")]
pub vault_secret_key: String,
pub vault_namespace: Option<String>,
#[serde(default = "default_vault_auth_method")]
pub vault_auth_method: String,
pub vault_k8s_role: Option<String>,
#[serde(default = "default_vault_k8s_mount")]
pub vault_k8s_mount: String,
#[serde(default = "default_vault_k8s_jwt_path")]
pub vault_k8s_jwt_path: String,
pub vault_token: Option<String>,
pub vault_approle_role_id: Option<String>,
pub vault_approle_secret_id: Option<String>,
#[serde(default = "default_vault_approle_mount")]
pub vault_approle_mount: String,
#[serde(default, skip_serializing_if = "is_false")]
pub vault_skip_verify: bool,
pub k8s_secret_name: Option<String>,
pub k8s_namespace: Option<String>,
#[serde(default = "default_k8s_secret_key")]
pub k8s_secret_key: String,
#[serde(default, skip_serializing_if = "is_false")]
pub plaintext_mode: bool,
pub plaintext: Option<PlaintextSecrets>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plaintext_bootstrap_seed: Option<String>,
}
impl std::fmt::Debug for SecretsConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretsConfig")
.field("aws_secret_name", &self.aws_secret_name)
.field("aws_region", &self.aws_region)
.field("gcp_project", &self.gcp_project)
.field("gcp_secret_name", &self.gcp_secret_name)
.field("azure_vault_url", &self.azure_vault_url)
.field("azure_secret_name", &self.azure_secret_name)
.field("keyring_service", &self.keyring_service)
.field("vault_addr", &self.vault_addr)
.field("vault_kv_mount", &self.vault_kv_mount)
.field("vault_secret_path", &self.vault_secret_path)
.field("vault_secret_key", &self.vault_secret_key)
.field("vault_namespace", &self.vault_namespace)
.field("vault_auth_method", &self.vault_auth_method)
.field("vault_k8s_role", &self.vault_k8s_role)
.field("vault_k8s_mount", &self.vault_k8s_mount)
.field("vault_k8s_jwt_path", &self.vault_k8s_jwt_path)
.field(
"vault_token",
&self.vault_token.as_ref().map(|_| "<redacted>"),
)
.field("vault_approle_role_id", &self.vault_approle_role_id)
.field(
"vault_approle_secret_id",
&self.vault_approle_secret_id.as_ref().map(|_| "<redacted>"),
)
.field("vault_approle_mount", &self.vault_approle_mount)
.field("vault_skip_verify", &self.vault_skip_verify)
.field("k8s_secret_name", &self.k8s_secret_name)
.field("k8s_namespace", &self.k8s_namespace)
.field("k8s_secret_key", &self.k8s_secret_key)
.field("plaintext", &self.plaintext)
.field(
"plaintext_bootstrap_seed",
&self.plaintext_bootstrap_seed.as_ref().map(|_| "<redacted>"),
)
.finish()
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct VtaConfig {
pub url: Option<String>,
pub did: Option<String>,
pub context_id: Option<String>,
}
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum IdentityMode {
#[default]
Vta,
SelfManaged,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct IdentityConfig {
#[serde(default)]
pub mode: IdentityMode,
#[serde(default = "default_rotation_grace_period")]
pub rotation_grace_period: String,
}
fn default_rotation_grace_period() -> String {
"1h".to_string()
}
impl Default for IdentityConfig {
fn default() -> Self {
Self {
mode: IdentityMode::default(),
rotation_grace_period: default_rotation_grace_period(),
}
}
}
impl IdentityConfig {
pub fn rotation_grace_secs(&self) -> u64 {
match crate::server::pending_purge::parse_grace_string(&self.rotation_grace_period) {
Ok(secs) => secs,
Err(e) => {
tracing::warn!(
value = %self.rotation_grace_period,
"invalid identity.rotation_grace_period ({e}) — falling back to 1h"
);
3600
}
}
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct PlaintextSecrets {
pub signing_key: String,
pub key_agreement_key: String,
pub jwt_signing_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vta_credential: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub retired: Vec<crate::server::secret_store::RetiredKeys>,
}
impl std::fmt::Debug for PlaintextSecrets {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PlaintextSecrets")
.field("signing_key", &"<redacted>")
.field("key_agreement_key", &"<redacted>")
.field("jwt_signing_key", &"<redacted>")
.field(
"vta_credential",
&self.vta_credential.as_ref().map(|_| "<redacted>"),
)
.finish()
}
}
fn default_keyring_service() -> String {
"webvh".to_string()
}
fn default_vault_kv_mount() -> String {
"secret".to_string()
}
fn default_vault_secret_key() -> String {
"seed".to_string()
}
fn default_vault_auth_method() -> String {
"kubernetes".to_string()
}
fn default_vault_k8s_mount() -> String {
"kubernetes".to_string()
}
fn default_vault_k8s_jwt_path() -> String {
"/var/run/secrets/kubernetes.io/serviceaccount/token".to_string()
}
fn default_vault_approle_mount() -> String {
"approle".to_string()
}
fn default_k8s_secret_key() -> String {
"seed".to_string()
}
impl Default for SecretsConfig {
fn default() -> Self {
Self {
aws_secret_name: None,
aws_region: None,
gcp_project: None,
gcp_secret_name: None,
azure_vault_url: None,
azure_secret_name: None,
keyring_service: default_keyring_service(),
vault_addr: None,
vault_kv_mount: default_vault_kv_mount(),
vault_secret_path: None,
vault_secret_key: default_vault_secret_key(),
vault_namespace: None,
vault_auth_method: default_vault_auth_method(),
vault_k8s_role: None,
vault_k8s_mount: default_vault_k8s_mount(),
vault_k8s_jwt_path: default_vault_k8s_jwt_path(),
vault_token: None,
vault_approle_role_id: None,
vault_approle_secret_id: None,
vault_approle_mount: default_vault_approle_mount(),
vault_skip_verify: false,
k8s_secret_name: None,
k8s_namespace: None,
k8s_secret_key: default_k8s_secret_key(),
plaintext_mode: false,
plaintext: None,
plaintext_bootstrap_seed: None,
}
}
}
fn is_false(b: &bool) -> bool {
!*b
}
pub fn apply_env_overrides(
prefix: &str,
features: &mut FeaturesConfig,
server: &mut ServerConfig,
log: &mut LogConfig,
store: &mut StoreConfig,
auth: &mut AuthConfig,
secrets: &mut SecretsConfig,
) -> Result<(), AppError> {
macro_rules! env_str {
($var:expr, $field:expr) => {
if let Ok(v) = std::env::var($var) {
$field = v;
}
};
}
macro_rules! env_opt {
($var:expr, $field:expr) => {
if let Ok(v) = std::env::var($var) {
$field = Some(v);
}
};
}
macro_rules! env_parse {
($var:expr, $field:expr) => {
if let Ok(v) = std::env::var($var) {
$field = v
.parse()
.map_err(|e| AppError::Config(format!("invalid {}: {e}", $var)))?;
}
};
}
macro_rules! env_bool {
($var:expr, $field:expr) => {
if let Ok(v) = std::env::var($var) {
$field = v == "1" || v.eq_ignore_ascii_case("true");
}
};
}
env_bool!(&format!("{prefix}_FEATURES_DIDCOMM"), features.didcomm);
env_bool!(&format!("{prefix}_FEATURES_TSP"), features.tsp);
env_bool!(&format!("{prefix}_FEATURES_REST_API"), features.rest_api);
env_bool!(
&format!("{prefix}_FEATURES_AGENT_NAMES"),
features.agent_names
);
env_str!(&format!("{prefix}_SERVER_HOST"), server.host);
env_parse!(&format!("{prefix}_SERVER_PORT"), server.port);
env_str!(&format!("{prefix}_LOG_LEVEL"), log.level);
let log_format_var = format!("{prefix}_LOG_FORMAT");
if let Ok(format) = std::env::var(&log_format_var) {
log.format = match format.to_lowercase().as_str() {
"json" => LogFormat::Json,
"text" => LogFormat::Text,
other => {
return Err(AppError::Config(format!(
"invalid {log_format_var} '{other}', expected 'text' or 'json'"
)));
}
};
}
let store_data_dir_var = format!("{prefix}_STORE_DATA_DIR");
if let Ok(data_dir) = std::env::var(&store_data_dir_var) {
store.data_dir = PathBuf::from(data_dir);
}
env_opt!(&format!("{prefix}_STORE_REDIS_URL"), store.redis_url);
env_opt!(
&format!("{prefix}_STORE_DYNAMODB_TABLE_PREFIX"),
store.dynamodb_table_prefix
);
env_opt!(
&format!("{prefix}_STORE_DYNAMODB_REGION"),
store.dynamodb_region
);
env_opt!(
&format!("{prefix}_STORE_FIRESTORE_PROJECT"),
store.firestore_project
);
env_opt!(
&format!("{prefix}_STORE_FIRESTORE_DATABASE"),
store.firestore_database
);
env_opt!(
&format!("{prefix}_STORE_COSMOSDB_CONNECTION_STRING"),
store.cosmosdb_connection_string
);
env_opt!(
&format!("{prefix}_STORE_COSMOSDB_DATABASE"),
store.cosmosdb_database
);
env_opt!(
&format!("{prefix}_STORE_COSMOSDB_REGION"),
store.cosmosdb_region
);
env_parse!(
&format!("{prefix}_AUTH_ACCESS_EXPIRY"),
auth.access_token_expiry
);
env_parse!(
&format!("{prefix}_AUTH_REFRESH_EXPIRY"),
auth.refresh_token_expiry
);
env_parse!(&format!("{prefix}_AUTH_CHALLENGE_TTL"), auth.challenge_ttl);
env_parse!(
&format!("{prefix}_AUTH_SESSION_CLEANUP_INTERVAL"),
auth.session_cleanup_interval
);
env_parse!(
&format!("{prefix}_AUTH_PASSKEY_ENROLLMENT_TTL"),
auth.passkey_enrollment_ttl
);
env_parse!(
&format!("{prefix}_CLEANUP_TTL_MINUTES"),
auth.cleanup_ttl_minutes
);
env_opt!(
&format!("{prefix}_SECRETS_AWS_SECRET_NAME"),
secrets.aws_secret_name
);
env_opt!(&format!("{prefix}_SECRETS_AWS_REGION"), secrets.aws_region);
env_opt!(
&format!("{prefix}_SECRETS_GCP_PROJECT"),
secrets.gcp_project
);
env_opt!(
&format!("{prefix}_SECRETS_GCP_SECRET_NAME"),
secrets.gcp_secret_name
);
env_opt!(
&format!("{prefix}_SECRETS_AZURE_VAULT_URL"),
secrets.azure_vault_url
);
env_opt!(
&format!("{prefix}_SECRETS_AZURE_SECRET_NAME"),
secrets.azure_secret_name
);
env_str!(
&format!("{prefix}_SECRETS_KEYRING_SERVICE"),
secrets.keyring_service
);
env_opt!(&format!("{prefix}_SECRETS_VAULT_ADDR"), secrets.vault_addr);
env_str!(
&format!("{prefix}_SECRETS_VAULT_KV_MOUNT"),
secrets.vault_kv_mount
);
env_opt!(
&format!("{prefix}_SECRETS_VAULT_SECRET_PATH"),
secrets.vault_secret_path
);
env_str!(
&format!("{prefix}_SECRETS_VAULT_SECRET_KEY"),
secrets.vault_secret_key
);
env_opt!(
&format!("{prefix}_SECRETS_VAULT_NAMESPACE"),
secrets.vault_namespace
);
env_str!(
&format!("{prefix}_SECRETS_VAULT_AUTH_METHOD"),
secrets.vault_auth_method
);
env_opt!(
&format!("{prefix}_SECRETS_VAULT_K8S_ROLE"),
secrets.vault_k8s_role
);
env_str!(
&format!("{prefix}_SECRETS_VAULT_K8S_MOUNT"),
secrets.vault_k8s_mount
);
env_str!(
&format!("{prefix}_SECRETS_VAULT_K8S_JWT_PATH"),
secrets.vault_k8s_jwt_path
);
env_opt!(
&format!("{prefix}_SECRETS_VAULT_TOKEN"),
secrets.vault_token
);
env_opt!(
&format!("{prefix}_SECRETS_VAULT_APPROLE_ROLE_ID"),
secrets.vault_approle_role_id
);
env_opt!(
&format!("{prefix}_SECRETS_VAULT_APPROLE_SECRET_ID"),
secrets.vault_approle_secret_id
);
env_str!(
&format!("{prefix}_SECRETS_VAULT_APPROLE_MOUNT"),
secrets.vault_approle_mount
);
env_bool!(
&format!("{prefix}_SECRETS_VAULT_SKIP_VERIFY"),
secrets.vault_skip_verify
);
env_opt!(
&format!("{prefix}_SECRETS_K8S_SECRET_NAME"),
secrets.k8s_secret_name
);
env_opt!(
&format!("{prefix}_SECRETS_K8S_NAMESPACE"),
secrets.k8s_namespace
);
env_str!(
&format!("{prefix}_SECRETS_K8S_SECRET_KEY"),
secrets.k8s_secret_key
);
Ok(())
}
pub fn init_tracing(log: &LogConfig) {
use tracing_subscriber::EnvFilter;
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&log.level));
let subscriber = tracing_subscriber::fmt().with_env_filter(filter);
let result = match log.format {
LogFormat::Json => subscriber.json().try_init(),
LogFormat::Text => subscriber.try_init(),
};
if let Err(e) = result {
eprintln!("tracing subscriber already initialised; continuing ({e})");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_names_default_on_for_absent_and_empty_config() {
assert!(FeaturesConfig::default().agent_names);
let cfg: FeaturesConfig = toml::from_str("").unwrap();
assert!(cfg.agent_names);
assert!(!cfg.didcomm);
assert!(!cfg.tsp);
assert!(!cfg.rest_api);
}
#[test]
fn agent_names_can_still_be_switched_off() {
let cfg: FeaturesConfig = toml::from_str("agent_names = false").unwrap();
assert!(!cfg.agent_names);
}
#[test]
fn features_default_matches_toml_round_trip() {
let from_toml: FeaturesConfig = toml::from_str("").unwrap();
let from_default = FeaturesConfig::default();
assert_eq!(from_toml.agent_names, from_default.agent_names);
assert_eq!(from_toml.deployment_mode, from_default.deployment_mode);
assert_eq!(from_default.deployment_mode, "standalone");
}
#[test]
fn identity_mode_default_is_vta() {
assert_eq!(IdentityMode::default(), IdentityMode::Vta);
assert_eq!(IdentityConfig::default().mode, IdentityMode::Vta);
}
#[test]
fn identity_mode_serializes_kebab_case() {
let toml_str = toml::to_string(&IdentityConfig {
mode: IdentityMode::SelfManaged,
..Default::default()
})
.unwrap();
assert!(
toml_str.contains(r#"mode = "self-managed""#),
"expected kebab-case `self-managed`, got: {toml_str}"
);
let toml_str = toml::to_string(&IdentityConfig {
mode: IdentityMode::Vta,
..Default::default()
})
.unwrap();
assert!(
toml_str.contains(r#"mode = "vta""#),
"expected `vta`, got: {toml_str}"
);
}
#[test]
fn identity_config_deserializes_self_managed() {
let cfg: IdentityConfig = toml::from_str(r#"mode = "self-managed""#).unwrap();
assert_eq!(cfg.mode, IdentityMode::SelfManaged);
}
#[test]
fn identity_config_deserializes_vta() {
let cfg: IdentityConfig = toml::from_str(r#"mode = "vta""#).unwrap();
assert_eq!(cfg.mode, IdentityMode::Vta);
}
#[test]
fn identity_config_round_trips() {
let original = IdentityConfig {
mode: IdentityMode::SelfManaged,
..Default::default()
};
let serialized = toml::to_string(&original).unwrap();
let deserialized: IdentityConfig = toml::from_str(&serialized).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn identity_config_missing_mode_defaults_to_vta() {
let cfg: IdentityConfig = toml::from_str("").unwrap();
assert_eq!(cfg.mode, IdentityMode::Vta);
}
#[test]
fn identity_config_unknown_mode_rejected() {
let result: Result<IdentityConfig, _> = toml::from_str(r#"mode = "bogus""#);
assert!(
result.is_err(),
"expected unknown mode to be rejected, got: {result:?}"
);
}
#[test]
fn transport_selection_flags() {
assert_eq!(TransportSelection::Didcomm.as_flags(), (true, false));
assert_eq!(TransportSelection::Tsp.as_flags(), (false, true));
assert_eq!(TransportSelection::Both.as_flags(), (true, true));
}
#[test]
fn transport_selection_parse_accepts_known_values() {
assert_eq!(
TransportSelection::parse("didcomm").unwrap(),
TransportSelection::Didcomm
);
assert_eq!(
TransportSelection::parse("TSP").unwrap(),
TransportSelection::Tsp
);
assert_eq!(
TransportSelection::parse(" Both ").unwrap(),
TransportSelection::Both
);
assert_eq!(
TransportSelection::parse("didcomm+tsp").unwrap(),
TransportSelection::Both
);
}
#[test]
fn transport_selection_parse_rejects_unknown() {
assert!(TransportSelection::parse("carrier-pigeon").is_err());
}
#[test]
fn transport_selection_from_flags() {
assert_eq!(
TransportSelection::from_flags(true, true),
Some(TransportSelection::Both)
);
assert_eq!(
TransportSelection::from_flags(true, false),
Some(TransportSelection::Didcomm)
);
assert_eq!(
TransportSelection::from_flags(false, true),
Some(TransportSelection::Tsp)
);
assert_eq!(TransportSelection::from_flags(false, false), None);
}
#[test]
fn transport_selection_flags_round_trip() {
for sel in [
TransportSelection::Didcomm,
TransportSelection::Tsp,
TransportSelection::Both,
] {
let (d, t) = sel.as_flags();
assert_eq!(TransportSelection::from_flags(d, t), Some(sel));
}
}
#[test]
fn transport_selection_str_round_trips() {
for sel in [
TransportSelection::Didcomm,
TransportSelection::Tsp,
TransportSelection::Both,
] {
assert_eq!(TransportSelection::parse(sel.as_str()).unwrap(), sel);
}
}
}