//! 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::>() .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, ) -> 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; }