Files
ARCADIA-Studio/crates/oa-server/tests/http/communication_limits.rs
T

120 lines
4.8 KiB
Rust

//! HTTP admission limits must reject endpoint decisions before transition planning.
use super::*;
use oa_core::{semantic::SemanticTransitionRequest, transitions::*};
async fn post(
server: &TestServer,
id: Id,
route: &str,
etag: &str,
body: &Value,
) -> reqwest::Response {
server
.client
.post(server.url(&format!("/projects/{id}/{route}")))
.header("if-match", etag)
.json(body)
.send()
.await
.unwrap()
}
async fn unchanged(server: &TestServer, before: &StoredProject, disk: &Path, bytes: &[u8]) {
let after = server.get(before.resource_id).await;
assert_eq!(after.revision, before.revision);
assert_eq!(after.etag(), before.etag());
assert_eq!(after.model, before.model);
assert_eq!(after.source_project, before.source_project);
assert_eq!(std::fs::read(disk).unwrap(), bytes);
}
async fn endpoint_limit(semantic: bool) {
let directory = tempfile::tempdir().unwrap();
let server = TestServer::start(directory.path(), None).await;
let mut model = Project::empty();
let source = model
.add_element(Kind::Activity, Layer::Oa, "Inspect service", None)
.unwrap();
let (id, etag) = server.import(&model).await;
let before = server.get(id).await;
let disk = directory.path().join(format!("{id}.json"));
let bytes = std::fs::read(&disk).unwrap();
let transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![source]);
let (route, mut request, decision_path, applied_decision_path) = if semantic {
(
"semantic-transitions",
serde_json::to_value(SemanticTransitionRequest::new(transition)).unwrap(),
"/transition/communication_endpoints",
"/plan/request/transition/communication_endpoints",
)
} else {
(
"transitions",
serde_json::to_value(transition).unwrap(),
"/communication_endpoints",
"/plan/request/communication_endpoints",
)
};
// Obtain a real prepared envelope first: apply must enforce the same bound,
// even when a client modifies a previously valid plan's embedded request.
let response = post(&server, id, &format!("{route}/preview"), &etag, &request).await;
let status = response.status();
let mut plan: Value = response.json().await.unwrap();
assert_eq!(status, StatusCode::OK, "{plan}");
assert_eq!(plan["plan"]["conflicts"], json!([]));
unchanged(&server, &before, &disk, &bytes).await;
// Syntactically typed but semantically inapplicable decisions deliberately
// distinguish capacity admission (400) from core semantic rejection (422 or
// a preview containing conflicts). The bound counts entries, not unique IDs.
let decision = CommunicationEndpointDecision {
source,
source_port: CommunicationPortChoice::Reuse { port: source },
target_port: CommunicationPortChoice::Reuse { port: source },
};
*request.pointer_mut(decision_path).unwrap() = json!(vec![decision.clone(); 200]);
let response = post(&server, id, &format!("{route}/preview"), &etag, &request).await;
let status = response.status();
let body: Value = response.json().await.unwrap();
assert!(
matches!(status, StatusCode::OK | StatusCode::UNPROCESSABLE_ENTITY),
"Exactly 200 decisions must pass capacity admission: {status} {body}"
);
unchanged(&server, &before, &disk, &bytes).await;
let over_limit = json!(vec![decision; 201]);
*request.pointer_mut(decision_path).unwrap() = over_limit.clone();
*plan.pointer_mut(applied_decision_path).unwrap() = over_limit;
for (operation, body) in [("preview", request), ("apply", plan)] {
let response = post(&server, id, &format!("{route}/{operation}"), &etag, &body).await;
let status = response.status();
let error: Value = response.json().await.unwrap();
assert_eq!(
status,
StatusCode::BAD_REQUEST,
"{route}/{operation}: {error}"
);
assert_eq!(error["code"], "invalid_request", "{error}");
let detail = error["detail"].as_str().unwrap().to_lowercase();
assert!(
detail.contains("200") && detail.contains("endpoint"),
"Capacity rejection must identify endpoint decisions: {error}"
);
unchanged(&server, &before, &disk, &bytes).await;
}
server.stop().await;
let server = TestServer::start(directory.path(), None).await;
unchanged(&server, &before, &disk, &bytes).await;
server.stop().await;
}
#[tokio::test]
async fn ordinary_transition_endpoint_decision_limit_rejects_201_without_mutation() {
endpoint_limit(false).await;
}
#[tokio::test]
async fn semantic_transition_endpoint_decision_limit_rejects_201_without_mutation() {
endpoint_limit(true).await;
}