Complete source 6 engineering workflows and preserve release validation evidence
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
//! 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;
|
||||
}
|
||||
Reference in New Issue
Block a user