807 lines
30 KiB
Rust
807 lines
30 KiB
Rust
use super::*;
|
|
use oa_core::source_project::*;
|
|
use std::collections::BTreeMap;
|
|
|
|
fn authored() -> SourceBundle {
|
|
let bundle = SourceBundle {
|
|
entry: "harbor.arcadia".into(),
|
|
files: BTreeMap::from([
|
|
("harbor.arcadia".into(), "// Harbor engineering rationale 航海\r\narcadia 3; project harbor \"Harbor rescue\" {\r\n include \"team/first.arcadia\";\r\n include \"team/second.arcadia\";\r\n include \"contracts.arcadia\";\r\n include \"work.arcadia\";\r\n}\r\n".into()),
|
|
("team/first.arcadia".into(), "// First payload rationale\r\nsa_exchange_item first \"First payload\" { description \"Reviewed first\"; }\r\n".into()),
|
|
("team/second.arcadia".into(), "// Second payload rationale\r\nsa_exchange_item second \"Second payload\" {}\r\n".into()),
|
|
("contracts.arcadia".into(), "// Contract ownership stays here\r\nsa_interface contract \"Contract\" { items [\r\n @first // first reference rationale\r\n ,\r\n @second // second reference rationale\r\n ]; }\r\n".into()),
|
|
("work.arcadia".into(), "// Operational rationale\r\noa_activity assess \"Assess assistance\" {}\r\n".into()),
|
|
]),
|
|
};
|
|
compile_bundle(&bundle, ProjectLimits::default())
|
|
.unwrap()
|
|
.bundle
|
|
}
|
|
|
|
fn id_for(model: &Project, alias: &str) -> Id {
|
|
model
|
|
.identities()
|
|
.into_iter()
|
|
.find(|(_, name)| *name == alias)
|
|
.unwrap()
|
|
.0
|
|
}
|
|
|
|
fn assert_pair(stored: &StoredProject) -> CompiledSourceProject {
|
|
let compiled = compile_bundle(&stored.source_project, ProjectLimits::default()).unwrap();
|
|
assert_eq!(compiled.decoded.project, stored.model);
|
|
assert_eq!(
|
|
compiled.bundle, stored.source_project,
|
|
"Stored IDs already stabilized"
|
|
);
|
|
compiled
|
|
}
|
|
|
|
async fn import(server: &TestServer, bundle: &SourceBundle) -> StoredProject {
|
|
let response = server
|
|
.client
|
|
.post(server.url("/projects/import-source-project"))
|
|
.json(bundle)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let status = response.status();
|
|
let body: Value = response.json().await.unwrap();
|
|
assert_eq!(status, StatusCode::CREATED, "{body}");
|
|
server
|
|
.get(body["resource_id"].as_str().unwrap().parse().unwrap())
|
|
.await
|
|
}
|
|
|
|
async fn put(server: &TestServer, current: &StoredProject, bundle: &SourceBundle) -> Value {
|
|
let response = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{}/source-project", current.resource_id)))
|
|
.header("if-match", current.etag())
|
|
.json(bundle)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let status = response.status();
|
|
let body: Value = response.json().await.unwrap();
|
|
assert_eq!(status, StatusCode::OK, "{body}");
|
|
body
|
|
}
|
|
|
|
async fn transact(
|
|
server: &TestServer,
|
|
current: &StoredProject,
|
|
commands: &[Command],
|
|
dry_run: bool,
|
|
) -> Value {
|
|
let response = server
|
|
.client
|
|
.post(server.url(&format!("/projects/{}/transactions", current.resource_id)))
|
|
.header("if-match", current.etag())
|
|
.json(&json!({"commands":commands,"dry_run":dry_run}))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let status = response.status();
|
|
let body: Value = response.json().await.unwrap();
|
|
assert_eq!(status, StatusCode::OK, "{body}");
|
|
body
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn source_projects_preserve_exact_files_comments_and_comment_only_revisions() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let source = authored();
|
|
let original = import(&server, &source).await;
|
|
let id = original.resource_id;
|
|
assert_eq!(original.source_project, source);
|
|
assert_pair(&original);
|
|
let url = server.url(&format!("/projects/{id}/source-project"));
|
|
let exported = server.client.get(&url).send().await.unwrap();
|
|
assert_eq!(
|
|
exported.headers()["etag"].to_str().unwrap(),
|
|
original.etag()
|
|
);
|
|
assert_eq!(exported.json::<SourceBundle>().await.unwrap(), source);
|
|
assert_eq!(
|
|
server
|
|
.client
|
|
.get(&url)
|
|
.header("if-none-match", original.etag())
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.status(),
|
|
StatusCode::NOT_MODIFIED
|
|
);
|
|
let mut edited = source.clone();
|
|
edited
|
|
.files
|
|
.get_mut("team/first.arcadia")
|
|
.unwrap()
|
|
.push_str("// Reviewed through automation\r\n");
|
|
let response = put(&server, &original, &edited).await;
|
|
assert_eq!(response["changed"], true);
|
|
assert_eq!(
|
|
response["source_project"],
|
|
serde_json::to_value(&edited).unwrap()
|
|
);
|
|
let current = server.get(id).await;
|
|
assert_eq!(current.revision, 2);
|
|
assert_eq!(current.model, original.model);
|
|
assert_eq!(current.source_project, edited);
|
|
let stale = server
|
|
.client
|
|
.post(server.url(&format!("/projects/{id}/transactions")))
|
|
.header("if-match", original.etag())
|
|
.json(&json!({"commands":[Command::Rename(current.model.id,"Stale change".into())]}))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(stale.status(), StatusCode::PRECONDITION_FAILED);
|
|
let filename = directory.path().join(format!("{id}.json"));
|
|
let saved_bytes = std::fs::read(&filename).unwrap();
|
|
let no_op = put(&server, ¤t, &edited).await;
|
|
assert_eq!(no_op["changed"], false);
|
|
assert_eq!(no_op["revision"], 2);
|
|
assert_eq!(std::fs::read(&filename).unwrap(), saved_bytes);
|
|
let mut renamed = edited;
|
|
let text = renamed.files.remove(&renamed.entry).unwrap();
|
|
renamed.entry = "rescue.arcadia".into();
|
|
renamed.files.insert(renamed.entry.clone(), text);
|
|
assert_eq!(put(&server, ¤t, &renamed).await["changed"], true);
|
|
let current = server.get(id).await;
|
|
assert_eq!(current.revision, 3);
|
|
assert_eq!(current.model, original.model);
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let restored = server.get(id).await;
|
|
assert_eq!(restored.source_project, renamed);
|
|
assert_eq!(restored.etag(), current.etag());
|
|
assert_pair(&restored);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn source_projects_failed_atomic_comment_write_preserves_both_representations() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let current = import(&server, &authored()).await;
|
|
let id = current.resource_id;
|
|
let envelope = directory.path().join(format!("{id}.json"));
|
|
let bytes = std::fs::read(&envelope).unwrap();
|
|
let mut edited = current.source_project.clone();
|
|
edited
|
|
.files
|
|
.get_mut("work.arcadia")
|
|
.unwrap()
|
|
.push_str("// Must not be adopted\r\n");
|
|
std::fs::remove_file(&envelope).unwrap();
|
|
std::fs::create_dir(&envelope).unwrap();
|
|
let failed = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/source-project")))
|
|
.header("if-match", current.etag())
|
|
.json(&edited)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(failed.status(), StatusCode::SERVICE_UNAVAILABLE);
|
|
let unchanged = server.get(id).await;
|
|
assert_eq!(unchanged.etag(), current.etag());
|
|
assert_eq!(unchanged.model, current.model);
|
|
assert_eq!(unchanged.source_project, current.source_project);
|
|
std::fs::remove_dir(&envelope).unwrap();
|
|
std::fs::write(&envelope, &bytes).unwrap();
|
|
assert!(!std::fs::read_dir(directory.path()).unwrap().any(|entry| {
|
|
entry
|
|
.unwrap()
|
|
.file_name()
|
|
.to_string_lossy()
|
|
.starts_with(".arcadia-write-")
|
|
}));
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
assert_eq!(server.get(id).await.source_project, current.source_project);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[test]
|
|
fn source_projects_openapi_describes_the_coupled_envelope_and_source_diagnostics() {
|
|
let spec: Value = serde_json::from_str(include_str!("../../../../docs/openapi.json")).unwrap();
|
|
let schemas = &spec["components"]["schemas"];
|
|
assert!(
|
|
schemas["StoredProject"]["required"]
|
|
.as_array()
|
|
.unwrap()
|
|
.contains(&json!("source_project"))
|
|
);
|
|
assert_eq!(
|
|
schemas["SourceBundle"]["required"],
|
|
json!(["entry", "files"])
|
|
);
|
|
assert_eq!(schemas["SourceBundle"]["additionalProperties"], false);
|
|
assert_eq!(
|
|
schemas["SourceBundle"]["properties"]["files"]["maxProperties"],
|
|
256
|
|
);
|
|
for property in ["path", "span", "related"] {
|
|
assert!(schemas["Issue"]["properties"].get(property).is_some());
|
|
}
|
|
assert_eq!(
|
|
schemas["TransactionResult"]["properties"]["source_project"]["$ref"],
|
|
"#/components/schemas/SourceBundle"
|
|
);
|
|
assert!(
|
|
schemas["ValidationRequest"]["oneOf"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|candidate| candidate["properties"]["format"]["const"] == "source-project")
|
|
);
|
|
let routes = &spec["paths"];
|
|
assert_eq!(
|
|
routes["/api/v1/projects/import-source-project"]["post"]["requestBody"]["content"]["application/json"]
|
|
["schema"]["$ref"],
|
|
"#/components/schemas/SourceBundle"
|
|
);
|
|
assert_eq!(
|
|
routes["/api/v1/projects/{id}/source-project"]["get"]["responses"]["200"]["content"]["application/json"]
|
|
["schema"]["$ref"],
|
|
"#/components/schemas/SourceBundle"
|
|
);
|
|
assert_eq!(
|
|
routes["/api/v1/projects/{id}/source-project"]["put"]["responses"]["200"]["content"]["application/json"]
|
|
["schema"]["$ref"],
|
|
"#/components/schemas/SourceProjectMutationResult"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn source_projects_command_dry_runs_and_identity_swaps_preserve_lexical_ownership() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let original = import(&server, &authored()).await;
|
|
let first = id_for(&original.model, "first");
|
|
let second = id_for(&original.model, "second");
|
|
let changed_id = Id::new_v4();
|
|
let mut element = original.model.elements[&first].clone();
|
|
element.id = changed_id;
|
|
element.alias = "revised_first".into();
|
|
element.name = "Reviewed payload".into();
|
|
let commands = vec![Command::EditElement(first, element)];
|
|
let filename = directory
|
|
.path()
|
|
.join(format!("{}.json", original.resource_id));
|
|
let before_bytes = std::fs::read(&filename).unwrap();
|
|
let dry_run = transact(&server, &original, &commands, true).await;
|
|
let proposed: SourceBundle = serde_json::from_value(dry_run["source_project"].clone()).unwrap();
|
|
assert!(proposed.files["team/first.arcadia"].contains(&changed_id.to_string()));
|
|
assert!(proposed.files["team/first.arcadia"].starts_with("// First payload rationale\r\n"));
|
|
assert_eq!(
|
|
proposed.files["team/second.arcadia"],
|
|
original.source_project.files["team/second.arcadia"]
|
|
);
|
|
assert!(
|
|
proposed.files["contracts.arcadia"].contains("@revised_first // first reference rationale")
|
|
);
|
|
assert_eq!(
|
|
server.get(original.resource_id).await.source_project,
|
|
original.source_project
|
|
);
|
|
assert_eq!(std::fs::read(&filename).unwrap(), before_bytes);
|
|
transact(&server, &original, &commands, false).await;
|
|
let current = server.get(original.resource_id).await;
|
|
assert_eq!(current.source_project, proposed);
|
|
assert_eq!(
|
|
assert_pair(¤t).index.declarations[&changed_id].path,
|
|
"team/first.arcadia"
|
|
);
|
|
let temporary = Id::new_v4();
|
|
let swaps = vec![Command::Batch(vec![
|
|
Command::Reidentify(changed_id, temporary),
|
|
Command::Reidentify(second, changed_id),
|
|
Command::Reidentify(temporary, second),
|
|
])];
|
|
transact(&server, ¤t, &swaps, false).await;
|
|
let swapped = server.get(current.resource_id).await;
|
|
let compiled = assert_pair(&swapped);
|
|
assert_eq!(
|
|
compiled.index.declarations[&second].path,
|
|
"team/first.arcadia"
|
|
);
|
|
assert_eq!(
|
|
compiled.index.declarations[&changed_id].path,
|
|
"team/second.arcadia"
|
|
);
|
|
assert!(
|
|
swapped.source_project.files["team/first.arcadia"]
|
|
.starts_with("// First payload rationale\r\n")
|
|
);
|
|
assert!(
|
|
swapped.source_project.files["contracts.arcadia"]
|
|
.contains("@revised_first // first reference rationale")
|
|
);
|
|
// Removing and reusing a UUID does not attach the old authored declaration
|
|
// and its rationale to an unrelated new record.
|
|
let mut replacement = swapped.model.elements[&second].clone();
|
|
replacement.alias = "new_payload".into();
|
|
replacement.name = "Independent new payload".into();
|
|
replacement.description.clear();
|
|
transact(
|
|
&server,
|
|
&swapped,
|
|
&[Command::Delete(second), Command::CreateElement(replacement)],
|
|
false,
|
|
)
|
|
.await;
|
|
let replaced = server.get(swapped.resource_id).await;
|
|
let compiled = assert_pair(&replaced);
|
|
assert_ne!(
|
|
compiled.index.declarations[&second].path,
|
|
"team/first.arcadia"
|
|
);
|
|
assert!(
|
|
replaced.source_project.files["team/first.arcadia"].contains("First payload rationale")
|
|
);
|
|
assert!(!replaced.source_project.files["team/first.arcadia"].contains("new_payload"));
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
assert_eq!(
|
|
server.get(replaced.resource_id).await.source_project,
|
|
replaced.source_project
|
|
);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn source_projects_equal_models_still_apply_changed_identity_provenance() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let original = import(&server, &authored()).await;
|
|
let work = id_for(&original.model, "assess");
|
|
let commands = [
|
|
Command::Delete(work),
|
|
Command::CreateElement(original.model.elements[&work].clone()),
|
|
];
|
|
let prepared =
|
|
oa_core::prepare_command(&original.model, &Command::Batch(commands.to_vec())).unwrap();
|
|
assert_eq!(prepared.project, original.model);
|
|
assert!(!prepared.identity_map.contains_key(&work));
|
|
let dry_run = transact(&server, &original, &commands, true).await;
|
|
assert_eq!(
|
|
dry_run["changed"], true,
|
|
"Recreated record must not inherit the deleted record's declaration ownership"
|
|
);
|
|
assert_eq!(
|
|
dry_run["model"],
|
|
serde_json::to_value(&original.model).unwrap()
|
|
);
|
|
let response = transact(&server, &original, &commands, false).await;
|
|
assert_eq!(response["changed"], true);
|
|
let changed = server.get(original.resource_id).await;
|
|
assert_eq!(changed.revision, original.revision + 1);
|
|
assert_eq!(changed.model, original.model);
|
|
let compiled = assert_pair(&changed);
|
|
assert_ne!(compiled.index.declarations[&work].path, "work.arcadia");
|
|
assert!(changed.source_project.files["work.arcadia"].contains("Operational rationale"));
|
|
assert!(!changed.source_project.files["work.arcadia"].contains("oa_activity"));
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn source_projects_model_interchange_and_transitions_keep_authored_modules() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let original = import(&server, &authored()).await;
|
|
let id = original.resource_id;
|
|
let work = id_for(&original.model, "assess");
|
|
let mut model = original.model.clone();
|
|
model.elements.get_mut(&work).unwrap().name = "Review assistance".into();
|
|
let response = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/model")))
|
|
.header("if-match", original.etag())
|
|
.json(&model)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::OK,
|
|
"{}",
|
|
response.text().await.unwrap()
|
|
);
|
|
let current = server.get(id).await;
|
|
assert_eq!(current.source_project.entry, "harbor.arcadia");
|
|
assert_eq!(
|
|
current.source_project.files["team/first.arcadia"],
|
|
original.source_project.files["team/first.arcadia"]
|
|
);
|
|
assert!(
|
|
current.source_project.files["work.arcadia"].starts_with("// Operational rationale\r\n")
|
|
);
|
|
model.elements.get_mut(&work).unwrap().description =
|
|
"Changed through canonical interchange".into();
|
|
let response = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/source")))
|
|
.header("if-match", current.etag())
|
|
.header("content-type", "text/plain")
|
|
.body(oa_core::language::format(&model))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::OK,
|
|
"{}",
|
|
response.text().await.unwrap()
|
|
);
|
|
let current = server.get(id).await;
|
|
assert_pair(¤t);
|
|
let request = oa_core::transitions::TransitionRequest::new(
|
|
oa_core::transitions::TransitionDirection::OaToSa,
|
|
vec![work],
|
|
);
|
|
let response = server
|
|
.client
|
|
.post(server.url(&format!("/projects/{id}/transitions/preview")))
|
|
.header("if-match", current.etag())
|
|
.json(&request)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
let status = response.status();
|
|
let preview: Value = response.json().await.unwrap();
|
|
assert_eq!(status, StatusCode::OK, "{preview}");
|
|
assert_eq!(server.get(id).await.source_project, current.source_project);
|
|
let response = server
|
|
.client
|
|
.post(server.url(&format!("/projects/{id}/transitions/apply")))
|
|
.header("if-match", current.etag())
|
|
.json(&preview)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::OK,
|
|
"{}",
|
|
response.text().await.unwrap()
|
|
);
|
|
let final_state = server.get(id).await;
|
|
assert_pair(&final_state);
|
|
assert!(
|
|
final_state
|
|
.model
|
|
.elements
|
|
.values()
|
|
.any(|e| e.kind() == Kind::Function)
|
|
);
|
|
assert_eq!(final_state.model.traces.len(), 1);
|
|
for path in [
|
|
"team/first.arcadia",
|
|
"team/second.arcadia",
|
|
"contracts.arcadia",
|
|
] {
|
|
assert_eq!(
|
|
final_state.source_project.files[path],
|
|
original.source_project.files[path]
|
|
);
|
|
}
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn source_projects_invalid_dependencies_and_duplicate_paths_never_change_the_head() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let current = import(&server, &authored()).await;
|
|
let id = current.resource_id;
|
|
let filename = directory.path().join(format!("{id}.json"));
|
|
let original_bytes = std::fs::read(&filename).unwrap();
|
|
// A same-named server file cannot satisfy a logical source include.
|
|
std::fs::write(
|
|
directory.path().join("server_only.arcadia"),
|
|
"oa_activity external \"Must not load\" {}\n",
|
|
)
|
|
.unwrap();
|
|
let mut cases = vec![];
|
|
let mut missing = current.source_project.clone();
|
|
missing
|
|
.files
|
|
.get_mut("work.arcadia")
|
|
.unwrap()
|
|
.push_str("include \"server_only.arcadia\";\n");
|
|
cases.push((missing, "missing_include"));
|
|
let mut duplicate = current.source_project.clone();
|
|
let first = duplicate.files["team/first.arcadia"].clone();
|
|
duplicate
|
|
.files
|
|
.get_mut("team/second.arcadia")
|
|
.unwrap()
|
|
.push_str(&first);
|
|
cases.push((duplicate, "duplicate_declaration"));
|
|
let mut invalid = current.source_project.clone();
|
|
invalid
|
|
.files
|
|
.get_mut("team/second.arcadia")
|
|
.unwrap()
|
|
.push('?');
|
|
cases.push((invalid, ""));
|
|
let mut cycle = current.source_project.clone();
|
|
cycle
|
|
.files
|
|
.get_mut("work.arcadia")
|
|
.unwrap()
|
|
.push_str("include \"harbor.arcadia\";\n");
|
|
cases.push((cycle, "include_cycle"));
|
|
let mut orphan = current.source_project.clone();
|
|
orphan
|
|
.files
|
|
.insert("orphan.arcadia".into(), "// not included\n".into());
|
|
cases.push((orphan, "unreachable_file"));
|
|
let mut escaped = current.source_project.clone();
|
|
escaped
|
|
.files
|
|
.insert("../outside.arcadia".into(), String::new());
|
|
cases.push((escaped, "invalid_path"));
|
|
let mut excessive = current.source_project.clone();
|
|
for n in 0..256 {
|
|
excessive
|
|
.files
|
|
.insert(format!("empty{n}.arcadia"), String::new());
|
|
}
|
|
cases.push((excessive, "project_limit"));
|
|
for (bundle, code) in cases {
|
|
let response = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/source-project")))
|
|
.header("if-match", current.etag())
|
|
.json(&bundle)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
let error: Value = response.json().await.unwrap();
|
|
let issue = &error["issues"][0];
|
|
if !code.is_empty() {
|
|
assert_eq!(issue["code"], code, "{error}");
|
|
}
|
|
assert!(issue["path"].is_string(), "{error}");
|
|
assert!(issue["span"]["start"].is_number(), "{error}");
|
|
assert!(issue["line"].as_u64().unwrap() >= 1);
|
|
if code == "duplicate_declaration" {
|
|
assert_eq!(issue["path"], "team/second.arcadia");
|
|
assert_eq!(issue["related"][0]["path"], "team/first.arcadia");
|
|
}
|
|
let response = server
|
|
.client
|
|
.post(server.url(&format!("/projects/{id}/validation")))
|
|
.json(&json!({"format":"source-project","source_project":bundle}))
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
let validation: Value = response.json().await.unwrap();
|
|
assert_eq!(validation["valid"], false, "{validation}");
|
|
assert_eq!(validation["issues"][0]["path"], issue["path"]);
|
|
assert_eq!(server.get(id).await.source_project, current.source_project);
|
|
assert_eq!(std::fs::read(&filename).unwrap(), original_bytes);
|
|
}
|
|
let body = "{\"entry\":\"a.arcadia\",\"files\":{\"a.arcadia\":\"one\",\"a.arcadia\":\"two\"}}";
|
|
let duplicate_keys = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/source-project")))
|
|
.header("if-match", current.etag())
|
|
.header("content-type", "application/json")
|
|
.body(body)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(duplicate_keys.status(), StatusCode::BAD_REQUEST);
|
|
assert_eq!(std::fs::read(&filename).unwrap(), original_bytes);
|
|
let missing_precondition = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/source-project")))
|
|
.json(¤t.source_project)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
missing_precondition.status(),
|
|
StatusCode::PRECONDITION_REQUIRED
|
|
);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn source_projects_stabilize_once_and_enforce_size_after_insertions() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let bundle = SourceBundle {
|
|
entry: "entry.arcadia".into(),
|
|
files: BTreeMap::from([
|
|
(
|
|
"entry.arcadia".into(),
|
|
"// Header rationale\narcadia 3; project p \"P\" { include \"work.arcadia\"; }\n"
|
|
.into(),
|
|
),
|
|
(
|
|
"work.arcadia".into(),
|
|
"// Work rationale\noa_activity work \"Work\" {}\n".into(),
|
|
),
|
|
]),
|
|
};
|
|
let current = import(&server, &bundle).await;
|
|
assert_ne!(current.source_project, bundle);
|
|
assert!(current.source_project.files["work.arcadia"].starts_with("// Work rationale\n"));
|
|
assert_pair(¤t);
|
|
assert_eq!(
|
|
put(&server, ¤t, ¤t.source_project).await["changed"],
|
|
false
|
|
);
|
|
let mut large = SourceBundle {
|
|
entry: "entry.arcadia".into(),
|
|
files: BTreeMap::from([(
|
|
"entry.arcadia".into(),
|
|
"arcadia 3; project p \"P\" {}\n// ".into(),
|
|
)]),
|
|
};
|
|
let base = serde_json::to_vec(&large).unwrap().len();
|
|
large
|
|
.files
|
|
.get_mut("entry.arcadia")
|
|
.unwrap()
|
|
.push_str(&"x".repeat(oa_server::store::MAX_SOURCE_PROJECT_BYTES - base - 8));
|
|
assert!(serde_json::to_vec(&large).unwrap().len() < oa_server::store::MAX_MODEL_BYTES);
|
|
let response = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{}/source-project", current.resource_id)))
|
|
.header("if-match", current.etag())
|
|
.json(&large)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
|
let error: Value = response.json().await.unwrap();
|
|
assert_eq!(error["code"], "source_project_too_large");
|
|
assert_eq!(
|
|
server.get(current.resource_id).await.source_project,
|
|
current.source_project
|
|
);
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let restored = server.get(current.resource_id).await;
|
|
assert_eq!(restored.source_project, current.source_project);
|
|
assert_eq!(restored.etag(), current.etag());
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn source_projects_concurrent_comment_and_semantic_writes_share_one_revision() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let original = import(&server, &authored()).await;
|
|
let id = original.resource_id;
|
|
let mut bundle = original.source_project.clone();
|
|
bundle
|
|
.files
|
|
.get_mut("work.arcadia")
|
|
.unwrap()
|
|
.push_str("// Comment writer\r\n");
|
|
let comment = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{id}/source-project")))
|
|
.header("if-match", original.etag())
|
|
.json(&bundle)
|
|
.send();
|
|
let semantic = server
|
|
.client
|
|
.patch(server.url(&format!("/projects/{id}")))
|
|
.header("if-match", original.etag())
|
|
.json(&json!({"name":"Semantic writer"}))
|
|
.send();
|
|
let (comment, semantic) = tokio::join!(comment, semantic);
|
|
let statuses = [comment.unwrap().status(), semantic.unwrap().status()];
|
|
assert_eq!(statuses.iter().filter(|s| **s == StatusCode::OK).count(), 1);
|
|
assert_eq!(
|
|
statuses
|
|
.iter()
|
|
.filter(|s| **s == StatusCode::PRECONDITION_FAILED)
|
|
.count(),
|
|
1
|
|
);
|
|
let accepted = server.get(id).await;
|
|
assert_eq!(accepted.revision, 2);
|
|
assert_pair(&accepted);
|
|
if accepted.model.name == "Semantic writer" {
|
|
assert_eq!(
|
|
accepted.source_project.files["work.arcadia"],
|
|
original.source_project.files["work.arcadia"]
|
|
);
|
|
} else {
|
|
assert_eq!(accepted.model, original.model);
|
|
assert_eq!(accepted.source_project, bundle);
|
|
}
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
assert_eq!(server.get(id).await.source_project, accepted.source_project);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn source_projects_upgrade_old_envelopes_without_writes_and_reject_inconsistent_pairs() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let resource = Id::new_v4();
|
|
let model = graph();
|
|
let filename = directory.path().join(format!("{resource}.json"));
|
|
let original =
|
|
serde_json::to_vec(&json!({"resource_id":resource,"revision":9,"model":model})).unwrap();
|
|
std::fs::write(&filename, &original).unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let current = server.get(resource).await;
|
|
assert_eq!(current.model, model);
|
|
assert_eq!(current.revision, 9);
|
|
assert_pair(¤t);
|
|
assert_eq!(std::fs::read(&filename).unwrap(), original);
|
|
let response = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{resource}/model")))
|
|
.header("if-match", current.etag())
|
|
.json(&model)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
assert_eq!(response.json::<Value>().await.unwrap()["changed"], false);
|
|
assert_eq!(std::fs::read(&filename).unwrap(), original);
|
|
let mut bundle = current.source_project.clone();
|
|
bundle
|
|
.files
|
|
.get_mut(&bundle.entry)
|
|
.unwrap()
|
|
.push_str("// Reviewed source upgrade\n");
|
|
put(&server, ¤t, &bundle).await;
|
|
let accepted = server.get(resource).await;
|
|
assert_eq!(accepted.revision, 10);
|
|
server.stop().await;
|
|
let saved = std::fs::read(&filename).unwrap();
|
|
let mut envelope: Value = serde_json::from_slice(&saved).unwrap();
|
|
assert!(envelope.get("source_project").is_some());
|
|
envelope["model"]["name"] = json!("Corrupted cache");
|
|
std::fs::write(&filename, serde_json::to_vec(&envelope).unwrap()).unwrap();
|
|
assert!(Store::open(directory.path()).is_err());
|
|
let mut envelope: Value = serde_json::from_slice(&saved).unwrap();
|
|
envelope["source_project"] = Value::Null;
|
|
std::fs::write(&filename, serde_json::to_vec(&envelope).unwrap()).unwrap();
|
|
assert!(
|
|
Store::open(directory.path()).is_err(),
|
|
"Present null source is not a legacy envelope"
|
|
);
|
|
let mut envelope: Value = serde_json::from_slice(&saved).unwrap();
|
|
let entry = envelope["source_project"]["entry"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_owned();
|
|
let header_id = format!(" id \"{}\"", accepted.model.id);
|
|
let source = envelope["source_project"]["files"][&entry]
|
|
.as_str()
|
|
.unwrap();
|
|
assert!(source.contains(&header_id));
|
|
envelope["source_project"]["files"][&entry] = json!(source.replacen(&header_id, "", 1));
|
|
let unstable_bytes = serde_json::to_vec(&envelope).unwrap();
|
|
std::fs::write(&filename, &unstable_bytes).unwrap();
|
|
assert!(
|
|
Store::open(directory.path()).is_err(),
|
|
"Accepted sources must not silently acquire new IDs on restart"
|
|
);
|
|
assert_eq!(std::fs::read(&filename).unwrap(), unstable_bytes);
|
|
std::fs::write(&filename, &saved).unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
assert_eq!(server.get(resource).await.source_project, bundle);
|
|
server.stop().await;
|
|
}
|