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

318 lines
12 KiB
Rust

//! Transport evidence for the published R6 contract, not a second model fixture.
use super::*;
use oa_core::source_project::{ProjectLimits, SourceBundle, compile_bundle};
use oa_core::{DataTypeDefinition, DataValue, SharedBody, TransmissionProtocol};
use std::collections::BTreeMap;
fn documented_data() -> Project {
let contract = include_str!("../../../../docs/language.rst");
let example = contract
.split("This complete data contract example is also exercised through the REST API:")
.nth(1)
.unwrap()
.split(".. code-block:: text")
.nth(1)
.unwrap();
let source = example
.lines()
.skip_while(|line| line.trim().is_empty())
.take_while(|line| line.starts_with(" ") || line.is_empty())
.map(|line| line.strip_prefix(" ").unwrap_or(line))
.collect::<Vec<_>>()
.join("\n");
let model = oa_core::language::parse(&source).unwrap();
oa_core::validate_candidate(&model).unwrap();
model
}
fn id(model: &Project, alias: &str) -> Id {
model
.identities()
.into_iter()
.find(|(_, a)| *a == alias)
.unwrap()
.0
}
fn assert_pair(stored: &StoredProject) {
let compiled = compile_bundle(&stored.source_project, ProjectLimits::default()).unwrap();
assert_eq!(compiled.decoded.project, stored.model);
assert_eq!(compiled.bundle, stored.source_project);
}
async fn commands(
server: &TestServer,
current: &StoredProject,
commands: Vec<Command>,
) -> reqwest::Response {
server
.client
.post(server.url(&format!("/projects/{}/transactions", current.resource_id)))
.header("if-match", current.etag())
.json(&json!({"commands":commands}))
.send()
.await
.unwrap()
}
#[tokio::test]
async fn published_data_contract_queries_atomic_edits_identity_and_restart() {
let directory = tempfile::tempdir().unwrap();
let server = TestServer::start(directory.path(), None).await;
let model = documented_data();
let (resource, _) = server.import(&model).await;
let before = server.get(resource).await;
assert_eq!(before.model, model);
assert_pair(&before);
for (kind, count) in [("InterfaceItemAllocation", 2), ("DataAssociation", 1)] {
let result: Value = server
.client
.get(server.url(&format!("/projects/{resource}/relations?kind={kind}")))
.send()
.await
.unwrap()
.error_for_status()
.unwrap()
.json()
.await
.unwrap();
assert_eq!(result["items"].as_array().unwrap().len(), count);
}
let voltage = id(&model, "voltage");
let mut invalid = model.elements[&voltage].clone();
let Some(SharedBody::DataType {
definition: DataTypeDefinition::Real { maximum, .. },
..
}) = invalid.shared_mut()
else {
panic!()
};
*maximum = Some(12.0); // Existing default24V is outside this bound.
let filename = directory.path().join(format!("{resource}.json"));
let disk_before = std::fs::read(&filename).unwrap();
assert_eq!(
commands(
&server,
&before,
vec![Command::EditElement(voltage, invalid.clone())]
)
.await
.status(),
StatusCode::UNPROCESSABLE_ENTITY
);
assert_eq!(server.get(resource).await.etag(), before.etag());
assert_eq!(std::fs::read(&filename).unwrap(), disk_before);
let Some(SharedBody::DataType { default, .. }) = invalid.shared_mut() else {
panic!()
};
*default = Some(DataValue::Real(12.0));
let nav = id(&model, "nav_operation");
let other = id(&model, "monitor_operation");
let mut allocation = model.relations[&nav].clone();
let oa_core::RelationBody::InterfaceItemAllocation { transmission, .. } = &mut allocation.body
else {
panic!()
};
*transmission = TransmissionProtocol::Unicast;
let artifact = oa_core::ArtifactLink {
id: Id::new_v4(),
alias: "navigation_evidence".into(),
name: "Navigation CAD evidence".into(),
description: "Independent allocation evidence".into(),
owner: nav,
relation: oa_core::ArtifactRelation::Verifies,
uri: "cad://navigation/connector".into(),
revision: Some("review-17".into()),
fragment: Some("pin-3".into()),
};
let response = commands(
&server,
&before,
vec![
Command::EditElement(voltage, invalid),
Command::EditRelation(nav, allocation.clone()),
Command::CreateArtifact(artifact.clone()),
],
)
.await;
assert_eq!(
response.status(),
StatusCode::OK,
"{}",
response.text().await.unwrap()
);
let changed = server.get(resource).await;
assert_eq!(changed.revision, before.revision + 1);
assert_eq!(changed.model.relations[&other], model.relations[&other]);
assert_eq!(changed.model.relations[&nav], allocation);
assert_eq!(changed.model.artifact_links[&artifact.id], artifact);
assert_pair(&changed);
assert_eq!(
commands(&server, &before, vec![Command::Rename(nav, "Stale".into())])
.await
.status(),
StatusCode::PRECONDITION_FAILED
);
let renamed = Id::new_v4();
assert_eq!(
commands(&server, &changed, vec![Command::Reidentify(nav, renamed)])
.await
.status(),
StatusCode::OK
);
let reidentified = server.get(resource).await;
assert_eq!(
reidentified.model.artifact_links[&artifact.id].owner,
renamed
);
// This allocation is not enforcing a live boundary, so removal is valid.
assert_eq!(
commands(&server, &reidentified, vec![Command::Delete(renamed)])
.await
.status(),
StatusCode::OK
);
let removed = server.get(resource).await;
assert!(removed.model.elements.contains_key(&id(&model, "navigate")));
assert_eq!(removed.model.relations[&other], model.relations[&other]);
assert!(!removed.model.artifact_links.contains_key(&artifact.id));
assert_pair(&removed);
let source = server
.client
.get(server.url(&format!("/projects/{resource}/source")))
.send()
.await
.unwrap()
.text()
.await
.unwrap();
assert!(source.starts_with("arcadia 4;"));
assert_eq!(oa_core::language::parse(&source).unwrap(), removed.model);
server.stop().await;
let server = TestServer::start(directory.path(), None).await;
let reopened = server.get(resource).await;
assert_eq!(reopened.model, removed.model);
assert_eq!(reopened.etag(), removed.etag());
assert_pair(&reopened);
server.stop().await;
}
#[tokio::test]
async fn authored_schema3_bundle_upgrade_reports_data_defaults_and_preserves_files() {
let directory = tempfile::tempdir().unwrap();
let server = TestServer::start(directory.path(), None).await;
let old = SourceBundle {
entry: "legacy.arcadia".into(), files: BTreeMap::from([
("legacy.arcadia".into(), "// Entry rationale\narcadia 3; project old_data \"Old data\" { include \"dictionary.arcadia\"; }\n".into()),
("dictionary.arcadia".into(), "// Event rationale\nsa_exchange_item notification \"Notification\" { kind Event; }\n// Interface rationale\nsa_interface contract \"Contract\" { items [@notification]; }\n".into())
])
};
let response = server
.client
.post(server.url("/projects/import-source-project"))
.json(&old)
.send()
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
let summary: Value = response.json().await.unwrap();
let resource = summary["resource_id"].as_str().unwrap().parse().unwrap();
let stored = server.get(resource).await;
let report = stored.migration.as_ref().unwrap();
assert_eq!((report.from_schema, report.to_schema), (3, 4));
assert_eq!(report.generated_interface_allocations.len(), 1);
assert!(!report.semantic_defaults.is_empty());
let validated: Value = server
.client
.post(server.url(&format!("/projects/{resource}/validation")))
.json(&json!({"format":"source-project","source_project":old}))
.send()
.await
.unwrap()
.json()
.await
.unwrap();
assert_eq!(validated["valid"], true);
assert_eq!(validated["migration"]["from_schema"], 3);
assert_eq!(
validated["migration"]["generated_interface_allocations"]
.as_array()
.unwrap()
.len(),
1
);
assert_eq!(stored.source_project.files.len(), 2);
assert!(stored.source_project.files["legacy.arcadia"].contains("arcadia 4;"));
for text in [
"// Event rationale",
"// Interface rationale",
"interface_item_allocation",
] {
assert!(stored.source_project.files["dictionary.arcadia"].contains(text));
}
assert_pair(&stored);
let filename = directory.path().join(format!("{resource}.json"));
let bytes = std::fs::read(&filename).unwrap();
server.stop().await;
let server = TestServer::start(directory.path(), None).await;
let reopened = server.get(resource).await;
assert_eq!(reopened.model, stored.model);
assert_eq!(reopened.source_project, stored.source_project);
assert!(reopened.migration.is_none());
assert_eq!(std::fs::read(filename).unwrap(), bytes);
server.stop().await;
}
#[tokio::test]
async fn existing_schema3_coupled_envelope_upgrades_in_memory_until_explicit_save() {
let directory = tempfile::tempdir().unwrap();
let resource = Id::new_v4();
let filename = directory.path().join(format!("{resource}.json"));
let old_model: Value = serde_json::from_str(include_str!(
"../../../oa-core/tests/fixtures/v3-data-contract.json"
))
.unwrap();
let old_bundle = SourceBundle {
entry: "project.arcadia".into(),
files: BTreeMap::from([(
"project.arcadia".into(),
include_str!("../../../oa-core/tests/fixtures/v3-data-contract.arcadia").into(),
)]),
};
let original = serde_json::to_vec(&json!({"resource_id":resource,"revision":17,"model":old_model,"source_project":old_bundle})).unwrap();
std::fs::write(&filename, &original).unwrap();
let server = TestServer::start(directory.path(), None).await;
let opened = server.get(resource).await;
assert_eq!(opened.revision, 17);
assert_eq!(opened.model.schema_version, 4);
assert_eq!(opened.migration.as_ref().unwrap().from_schema, 3);
assert!(opened.source_project.files["project.arcadia"].contains("arcadia 4;"));
assert_pair(&opened);
assert_eq!(std::fs::read(&filename).unwrap(), original);
assert_eq!(
commands(
&server,
&opened,
vec![Command::Rename(
opened.model.id,
"Reviewed data migration".into()
)]
)
.await
.status(),
StatusCode::OK
);
let saved = server.get(resource).await;
assert_eq!(saved.revision, 18);
let bytes = std::fs::read(&filename).unwrap();
assert_ne!(bytes, original);
server.stop().await;
let server = TestServer::start(directory.path(), None).await;
let reopened = server.get(resource).await;
assert_eq!(reopened.model, saved.model);
assert_eq!(reopened.source_project, saved.source_project);
assert_eq!(reopened.etag(), saved.etag());
assert!(reopened.migration.is_none());
assert_eq!(std::fs::read(&filename).unwrap(), bytes);
server.stop().await;
}