120 lines
4.8 KiB
Rust
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;
|
|
}
|