Review pass

This commit is contained in:
Andras Schmelczer 2026-09-19 19:51:08 +01:00
commit 99fa298b92
2 changed files with 621 additions and 94 deletions

View file

@ -7,7 +7,7 @@ use crate::consts::{
DEFAULT_MERGEABLE_FILE_EXTENSIONS, DEFAULT_PORT, DEFAULT_RESPONSE_TIMEOUT_SECONDS,
};
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ServerConfig {
#[serde(default = "default_host")]
pub host: String,
@ -28,6 +28,19 @@ pub struct ServerConfig {
pub mergeable_file_extensions: Vec<String>,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
host: default_host(),
port: default_port(),
max_body_size_mb: default_max_body_size_mb(),
max_clients_per_vault: default_max_clients_per_vault(),
response_timeout: default_response_timeout(),
mergeable_file_extensions: default_mergeable_file_extensions(),
}
}
}
fn default_host() -> String {
debug!("Using default server host: {DEFAULT_HOST}");
DEFAULT_HOST.to_owned()
@ -60,3 +73,36 @@ fn default_mergeable_file_extensions() -> Vec<String> {
.map(|s| (*s).to_owned())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
#[tokio::test]
async fn fresh_and_omitted_server_configs_use_the_documented_defaults() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("config.yml");
let fresh = Config::read_or_create(&path).await.unwrap();
let reread = Config::load_from_file(&path).await.unwrap();
let omitted: Config = serde_yaml::from_str("{}").unwrap();
let explicit: Config = serde_yaml::from_str("server: {}").unwrap();
for config in [fresh, reread, omitted, explicit] {
assert_eq!(config.server.host, DEFAULT_HOST);
assert_eq!(config.server.port, DEFAULT_PORT);
assert_eq!(config.server.max_body_size_mb, DEFAULT_MAX_BODY_SIZE_MB);
assert_eq!(
config.server.max_clients_per_vault,
DEFAULT_MAX_CLIENTS_PER_VAULT
);
assert_eq!(
config.server.response_timeout,
DEFAULT_RESPONSE_TIMEOUT_SECONDS
);
assert_eq!(
config.server.mergeable_file_extensions,
DEFAULT_MERGEABLE_FILE_EXTENSIONS
);
}
}
}

View file

@ -2,7 +2,10 @@ use bimap::BiHashMap;
use rand::{Rng, distr::Alphanumeric, rng};
use serde::{Deserialize, Deserializer, Serialize, de::Error};
use crate::app_state::database::models::VaultId;
use crate::{
app_state::database::models::VaultId,
utils::normalize_vault_id::{normalize_string, validate_vault_id},
};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct UserConfig {
@ -71,9 +74,23 @@ pub enum VaultAccess {
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct AllowListedVaults {
#[serde(deserialize_with = "deserialize_allowed_vaults")]
pub allowed: Vec<VaultId>,
}
fn deserialize_allowed_vaults<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<VaultId>, D::Error> {
Vec::<String>::deserialize(deserializer)?
.into_iter()
.map(|name| {
let vault = normalize_string(&name);
validate_vault_id(&vault).map_err(D::Error::custom)?;
Ok(vault)
})
.collect()
}
fn default_users() -> Vec<User> {
vec![User {
name: "admin".to_owned(),
@ -95,6 +112,24 @@ mod tests {
use super::*;
#[test]
fn allowlists_normalize_case_and_whitespace_but_preserve_unicode_identity() {
let list: AllowListedVaults = serde_json::from_value(json!({
"allowed": [" MyVault ", "CAFÉ", "CAFE\u{301}"]
}))
.unwrap();
assert_eq!(list.allowed, ["myvault", "café", "cafe\u{301}"]);
}
#[test]
fn allowlists_reject_invalid_vault_names() {
for name in ["", " ", ".", "..", "../escaped", "a/b", "a\\b", "a\0b"] {
assert!(
serde_json::from_value::<AllowListedVaults>(json!({"allowed": [name]})).is_err()
);
}
}
#[test]
fn test_validate_users_unique_names_and_tokens() {
let config_json = json!({