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

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, &current, &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, &current, &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(&current).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, &current, &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(&current);
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(&current.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(&current);
assert_eq!(
put(&server, &current, &current.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(&current);
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, &current, &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;
}