#[path = "test_support/state_guard.rs"]
mod state_guard;
use std::path::PathBuf;
use std::sync::Arc;
use aion::EngineBuilder;
use aion_server::api::http::http_router;
use aion_server::config::{NamespaceConfig, ServerConfig};
use aion_server::{NamespaceResolver, ServerState};
use aion_store::{EventStore, InMemoryStore};
use axum::{
body,
http::{Request, StatusCode},
response::Response,
};
use serde_json::{Value, json};
use tower::ServiceExt;
use state_guard::StateUnderTest;
type TestError = Box<dyn std::error::Error>;
struct Harness {
server: StateUnderTest,
_scratch: tempfile::TempDir,
workspace: PathBuf,
router: axum::Router,
}
impl Harness {
async fn new() -> Result<Self, TestError> {
let scratch = private_tempdir()?;
let config = ServerConfig::from_slice_with_home(b"", scratch.path())?;
let (_, mut runtime) = config.into_parts();
let configured = runtime
.authoring
.workspace_dir
.as_deref()
.ok_or("stock config omitted the AWL workspace")?;
let workspace = scratch.path().join(configured);
runtime.authoring.workspace_dir = Some(workspace.clone());
let store = Arc::new(InMemoryStore::default());
let event_store: Arc<dyn EventStore> = store;
let engine = Arc::new(
EngineBuilder::new()
.stop_drain_timeout(std::time::Duration::from_secs(5))
.store_arc(event_store)
.in_memory_visibility()
.scheduler_threads(1)
.build()
.await?,
);
let resolver = NamespaceResolver::from_config(NamespaceConfig::default(), engine);
let server = StateUnderTest::new(ServerState::from_parts(resolver, runtime));
let router = http_router(server.state.clone())?;
Ok(Self {
server,
_scratch: scratch,
workspace,
router,
})
}
}
async fn request(
router: &axum::Router,
method: &str,
uri: &str,
value: Option<&Value>,
) -> Result<Response, TestError> {
let mut builder = Request::builder()
.method(method)
.uri(uri)
.header("x-aion-subject", "nested-doc-test")
.header("x-aion-namespaces", "default")
.header("x-aion-deploy", "true");
let payload = match value {
Some(value) => {
builder = builder.header("content-type", "application/json");
body::Body::from(serde_json::to_vec(value)?)
}
None => body::Body::empty(),
};
Ok(router.clone().oneshot(builder.body(payload)?).await?)
}
async fn request_json(
router: &axum::Router,
method: &str,
uri: &str,
value: Option<&Value>,
status: StatusCode,
) -> Result<Value, TestError> {
let response = request(router, method, uri, value).await?;
assert_eq!(response.status(), status, "{method} {uri}");
let bytes = body::to_bytes(response.into_body(), usize::MAX).await?;
Ok(serde_json::from_slice(&bytes)?)
}
#[tokio::test]
async fn posting_a_nested_document_then_getting_it_over_http() -> Result<(), TestError> {
let harness = Harness::new().await?;
let created = request_json(
&harness.router,
"POST",
"/awl/documents",
Some(&json!({ "name": "order_flow", "directory": "billing/invoices" })),
StatusCode::CREATED,
)
.await?;
assert_eq!(created["path"], "billing/invoices/order_flow.awl");
assert!(
harness
.workspace
.join("billing/invoices/order_flow.awl")
.is_file()
);
let read = request_json(
&harness.router,
"GET",
"/awl/documents/billing%2Finvoices%2Forder_flow.awl",
None,
StatusCode::OK,
)
.await?;
assert_eq!(read["source"], created["source"]);
let listed = request_json(
&harness.router,
"GET",
"/awl/documents",
None,
StatusCode::OK,
)
.await?;
assert_eq!(listed[0]["path"], "billing/invoices/order_flow.awl");
let traversal = request_json(
&harness.router,
"GET",
"/awl/documents/billing%2F%2E%2E%2Fescape.awl",
None,
StatusCode::BAD_REQUEST,
)
.await?;
assert_eq!(traversal["error_type"], "InvalidDocumentPath");
let invalid = request_json(
&harness.router,
"POST",
"/awl/documents",
Some(&json!({ "name": "order_flow", "directory": "Bad/path" })),
StatusCode::BAD_REQUEST,
)
.await?;
assert_eq!(invalid["error_type"], "InvalidDocumentPath");
harness.server.shutdown()?;
Ok(())
}
fn private_tempdir() -> std::io::Result<tempfile::TempDir> {
let dir = tempfile::tempdir()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700))?;
}
Ok(dir)
}