vault-link/sync-server/src/server/tests.rs
2026-09-19 19:51:08 +01:00

121 lines
4.1 KiB
Rust

use super::*;
use axum::{
body::{Body, to_bytes},
http::StatusCode,
};
use serde_json::{Value, json};
use tower::ServiceExt;
async fn app(directory: &tempfile::TempDir) -> Router {
let mut config: Config = serde_json::from_value(json!({
"users": { "user_configs": [
{ "name": "admin", "token": "admin", "vault_access": { "type": "allow_access_to_all" } },
{ "name": "mixed", "token": "mixed", "vault_access": { "type": "allow_list", "allowed": [" MyVault "] } },
{ "name": "composed", "token": "composed", "vault_access": { "type": "allow_list", "allowed": ["café"] } },
{ "name": "decomposed", "token": "decomposed", "vault_access": { "type": "allow_list", "allowed": ["cafe\u{301}"] } }
] }
})).unwrap();
config.database.databases_directory_path = directory.path().join("databases");
let state = AppState::try_new(config).await.unwrap();
get_authed_routes(state.clone()).with_state(state)
}
async fn request(app: Router, path: &str, token: &str, body: Option<Value>) -> (StatusCode, Value) {
let response = app
.oneshot(
Request::builder()
.uri(path)
.method(if body.is_some() {
Method::PUT
} else {
Method::GET
})
.header(http::header::AUTHORIZATION, format!("Bearer {token}"))
.header(http::header::CONTENT_TYPE, "application/json")
.header("Device-Id", "test")
.body(body.map_or_else(Body::empty, |value| Body::from(value.to_string())))
.unwrap(),
)
.await
.unwrap();
let status = response.status();
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
(
status,
serde_json::from_slice(&bytes).unwrap_or(Value::Null),
)
}
#[tokio::test]
async fn http_rejects_encoded_traversal_and_accepts_normalized_allowlists() {
let directory = tempfile::tempdir().unwrap();
let app = app(&directory).await;
for vault in [
"..%2Fescaped",
"%2Fabsolute",
"a%5Cb",
"%2E%2E",
"%20",
"a%00b",
] {
let (status, _) = request(
app.clone(),
&format!("/vaults/{vault}/vault_snapshot"),
"admin",
None,
)
.await;
assert_eq!(status, StatusCode::BAD_REQUEST, "vault: {vault}");
}
assert!(!directory.path().join("escaped.sqlite").exists());
for vault in ["MyVault", "myvault", "%20MYVAULT%20"] {
let (status, _) = request(
app.clone(),
&format!("/vaults/{vault}/file_manifest"),
"mixed",
None,
)
.await;
assert_eq!(status, StatusCode::OK, "vault: {vault}");
}
assert_eq!(
request(app, "/vaults/other/file_manifest", "mixed", None)
.await
.0,
StatusCode::FORBIDDEN
);
}
#[tokio::test]
async fn http_keeps_unicode_vaults_separate_across_restart() {
let directory = tempfile::tempdir().unwrap();
let router = app(&directory).await;
let document = uuid::Uuid::new_v4();
let composed = format!("/vaults/caf%C3%A9/documents/{document}");
let decomposed = format!("/vaults/cafe%CC%81/documents/{document}");
let push = json!({
"requestId": uuid::Uuid::new_v4(), "parentVersionId": null,
"content": { "type": "Snapshot", "value": "aGVsbG8=" }
});
assert_eq!(
request(router, &composed, "composed", Some(push)).await.0,
StatusCode::OK
);
let reopened = app(&directory).await;
assert_eq!(
request(reopened.clone(), &composed, "decomposed", None)
.await
.0,
StatusCode::FORBIDDEN
);
assert_eq!(
request(reopened.clone(), &decomposed, "decomposed", None)
.await
.0,
StatusCode::NOT_FOUND
);
let (status, version) = request(reopened, &composed, "composed", None).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(version["contentBase64"], "aGVsbG8=");
}