- Clé de publication ed25519 générée au démarrage dans le volume ; sa partie publique et l'état de la file d'envoi s'affichent dans /acces. - Publication initiale à l'instanciation d'un document seedé publiable. - Reprise au démarrage : chaque document publié repasse une fois dans la file ; la date publiée est celle du dernier changement, un document inchangé ne fait pas de commit (testé contre un dépôt nu). - Le travailleur fait un premier passage sans attendre ; journaux sans codes couleur hors terminal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
455 lines
21 KiB
Rust
455 lines
21 KiB
Rust
//! End-to-end API scenarios on an in-memory database seeded like production.
|
|
|
|
use axum::body::Body;
|
|
use axum::http::{Request, StatusCode, header};
|
|
use http_body_util::BodyExt;
|
|
use ld_server::{AppState, CSRF_HEADER, CSRF_VALUE, Config, app, open_db, seed};
|
|
use serde_json::{Value, json};
|
|
use tower::ServiceExt;
|
|
|
|
struct Client {
|
|
router: axum::Router,
|
|
cookie: Option<String>,
|
|
}
|
|
|
|
impl Client {
|
|
async fn new() -> (Self, AppState) {
|
|
let db = open_db("sqlite::memory:").await.expect("db");
|
|
let state = AppState::new(Config::for_tests(), db).await.expect("state");
|
|
seed::ensure(&state).await.expect("seed");
|
|
(Self { router: app(state.clone()), cookie: None }, state)
|
|
}
|
|
|
|
fn with(&self, cookie: Option<String>) -> Self {
|
|
Self { router: self.router.clone(), cookie }
|
|
}
|
|
|
|
async fn call(&mut self, method: &str, uri: &str, body: Option<Value>) -> (StatusCode, Value) {
|
|
let mut req = Request::builder().method(method).uri(uri).header(CSRF_HEADER, CSRF_VALUE);
|
|
if let Some(c) = &self.cookie {
|
|
req = req.header(header::COOKIE, c);
|
|
}
|
|
let req = match body {
|
|
Some(b) => req.header(header::CONTENT_TYPE, "application/json").body(Body::from(b.to_string())),
|
|
None => req.body(Body::empty()),
|
|
}
|
|
.expect("request");
|
|
let res = self.router.clone().oneshot(req).await.expect("response");
|
|
let status = res.status();
|
|
if let Some(set) = res.headers().get(header::SET_COOKIE) {
|
|
let raw = set.to_str().expect("cookie").split(';').next().unwrap_or_default().to_owned();
|
|
if raw.starts_with("ld_session=") {
|
|
self.cookie = Some(raw);
|
|
}
|
|
}
|
|
let bytes = res.into_body().collect().await.expect("body").to_bytes();
|
|
(status, serde_json::from_slice(&bytes).unwrap_or(Value::Null))
|
|
}
|
|
|
|
async fn login(&mut self, name: &str, admin: bool) {
|
|
let (s, v) = self.call("POST", "/api/auth/dev", Some(json!({ "name": name, "admin": admin }))).await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
}
|
|
}
|
|
|
|
async fn doc_id(c: &mut Client, group: &str) -> String {
|
|
let (s, v) = c.call("GET", &format!("/api/groups/{group}"), None).await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
v["docs"][0]["id"].as_str().expect("doc").to_owned()
|
|
}
|
|
|
|
fn unit<'a>(doc: &'a Value, code: &str) -> &'a Value {
|
|
doc["units"].as_array().expect("units").iter().find(|u| u["code"] == code).expect("unit")
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn anonymous_sees_nothing_and_csrf_is_enforced() {
|
|
let (mut c, _) = Client::new().await;
|
|
let (s, _) = c.call("GET", "/api/groups", None).await;
|
|
assert_eq!(s, StatusCode::UNAUTHORIZED);
|
|
let (s, v) = c.call("GET", "/api/me", None).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
assert!(v["me"].is_null());
|
|
// A mutating call without the header is refused.
|
|
let req = Request::builder()
|
|
.method("POST")
|
|
.uri("/api/auth/dev")
|
|
.header(header::CONTENT_TYPE, "application/json")
|
|
.body(Body::from(r#"{"name":"x"}"#))
|
|
.expect("request");
|
|
let res = c.router.clone().oneshot(req).await.expect("response");
|
|
assert_eq!(res.status(), StatusCode::FORBIDDEN);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn the_seed_a_minima_and_the_open_proposals() {
|
|
let (mut c, _) = Client::new().await;
|
|
c.login("Chiara", false).await;
|
|
let (_, groups) = c.call("GET", "/api/groups", None).await;
|
|
let slugs: Vec<&str> =
|
|
groups["groups"].as_array().expect("groups").iter().filter_map(|g| g["slug"].as_str()).collect();
|
|
assert_eq!(slugs, ["monnaie-libre", "axiom", "librodrome", "vallee-drome-crestois", "essai"]);
|
|
|
|
let id = doc_id(&mut c, "monnaie-libre").await;
|
|
let (s, doc) = c.call("GET", &format!("/api/docs/{id}"), None).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
let in_force: Vec<&str> = doc["units"]
|
|
.as_array()
|
|
.expect("units")
|
|
.iter()
|
|
.filter(|u| !u["inForce"].is_null())
|
|
.filter_map(|u| u["code"].as_str())
|
|
.collect();
|
|
assert_eq!(in_force, ["I1", "E1", "E2"]);
|
|
let e1 = &unit(&doc, "E1")["inForce"];
|
|
assert_eq!(e1["seedMinimal"], true);
|
|
assert_eq!(e1["tally"]["for"], 0);
|
|
assert_eq!(doc["doc"]["voteFloor"], 50.0);
|
|
assert_eq!(doc["doc"]["electorate"], 5745);
|
|
// Every other article is an open proposal at zero votes, standard inertia.
|
|
let e3 = unit(&doc, "E3");
|
|
assert!(e3["inForce"].is_null());
|
|
assert_eq!(e3["proposals"].as_array().expect("p").len(), 2, "E3 and its 0.4.0 variant");
|
|
assert_eq!(e3["inertia"], "standard");
|
|
// Translations seeded.
|
|
assert!(e1["translations"]["en"]["body"].as_str().expect("en").contains("commit"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn one_vote_never_adopts_in_monnaie_libre() {
|
|
let (mut c, _) = Client::new().await;
|
|
c.login("Yvv", false).await;
|
|
let id = doc_id(&mut c, "monnaie-libre").await;
|
|
let (_, doc) = c.call("GET", &format!("/api/docs/{id}"), None).await;
|
|
let p = unit(&doc, "E3")["proposals"][0]["id"].as_str().expect("p").to_owned();
|
|
let (s, v) = c.call("POST", &format!("/api/proposals/{p}/vote"), Some(json!({ "choice": "for" }))).await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
assert_eq!(v["entered"].as_array().expect("e").len(), 0);
|
|
let (_, d) = c.call("GET", &format!("/api/proposals/{p}"), None).await;
|
|
assert_eq!(d["proposal"]["evaluation"]["required"], 50);
|
|
assert_eq!(d["proposal"]["evaluation"]["wellRated"], true);
|
|
assert_eq!(d["proposal"]["myVote"], "for");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn adoption_in_the_test_group_and_publication_queued_only_where_published() {
|
|
let (mut a, state) = Client::new().await;
|
|
a.login("Yvv", false).await;
|
|
let mut b = a.with(None);
|
|
b.login("Chiara", false).await;
|
|
// Dev identities are members of every group: W(essai) = 2, C = 1.
|
|
let id = doc_id(&mut a, "essai").await;
|
|
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
|
let e3 = unit(&doc, "E3");
|
|
let p = e3["proposals"][0]["id"].as_str().expect("p").to_owned();
|
|
let (_, v) = a.call("POST", &format!("/api/proposals/{p}/vote"), Some(json!({ "choice": "for" }))).await;
|
|
assert!(v["entered"].as_array().expect("e").is_empty(), "one of two is not enough");
|
|
let (_, v) = b.call("POST", &format!("/api/proposals/{p}/vote"), Some(json!({ "choice": "for" }))).await;
|
|
assert_eq!(v["entered"][0]["code"], "E3");
|
|
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
|
let e3 = unit(&doc, "E3");
|
|
assert_eq!(e3["inForce"]["id"], p.as_str());
|
|
assert!(e3["inForce"]["enteredAt"].is_string());
|
|
// The test group publishes nothing.
|
|
let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM outbox WHERE doc_id = ?")
|
|
.bind(&id)
|
|
.fetch_one(&state.db)
|
|
.await
|
|
.expect("count");
|
|
assert_eq!(n, 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_rival_replaces_the_seed_and_the_seed_returns_among_proposals() {
|
|
let (mut a, _) = Client::new().await;
|
|
a.login("Yvv", false).await;
|
|
let mut b = a.with(None);
|
|
b.login("Chiara", false).await;
|
|
let id = doc_id(&mut a, "essai").await;
|
|
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
|
let e1 = unit(&doc, "E1");
|
|
let (uid, seed_id) =
|
|
(e1["id"].as_str().expect("u").to_owned(), e1["inForce"]["id"].as_str().expect("v").to_owned());
|
|
let (s, v) = a
|
|
.call("POST", &format!("/api/docs/{id}/proposals"), Some(json!({
|
|
"unitId": uid, "title": "Unicité du compte",
|
|
"body": "Je m'engage sur l'honneur à n'avoir jamais qu'un seul compte cocréateur de monnaie Ğ1.", "submit": true,
|
|
})))
|
|
.await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
let rival = v["id"].as_str().expect("id").to_owned();
|
|
a.call("POST", &format!("/api/proposals/{rival}/vote"), Some(json!({ "choice": "for" }))).await;
|
|
let (_, v) = b.call("POST", &format!("/api/proposals/{rival}/vote"), Some(json!({ "choice": "for" }))).await;
|
|
assert_eq!(v["entered"][0]["kind"], "text");
|
|
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
|
let e1 = unit(&doc, "E1");
|
|
assert_eq!(e1["inForce"]["id"], rival.as_str());
|
|
assert!(
|
|
e1["proposals"].as_array().expect("p").iter().any(|p| p["id"] == seed_id.as_str() && p["status"] == "open")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn editing_is_not_voting() {
|
|
let (mut a, _) = Client::new().await;
|
|
a.login("Yvv", false).await;
|
|
let mut b = a.with(None);
|
|
b.login("Chiara", false).await;
|
|
let id = doc_id(&mut a, "monnaie-libre").await;
|
|
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
|
let i1 = unit(&doc, "I1")["inForce"].clone();
|
|
let pid = i1["id"].as_str().expect("id").to_owned();
|
|
// Anyone may fix the glued word of the preamble in force, without a vote.
|
|
let fixed = i1["body"].as_str().expect("body").replace("latransmission", "la transmission");
|
|
let (s, v) =
|
|
b.call("PATCH", &format!("/api/proposals/{pid}"), Some(json!({ "title": i1["title"], "body": fixed }))).await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
assert_eq!(v["edit"], "typo");
|
|
// A change of meaning in force is refused: it goes through a proposal.
|
|
let (s, v) = b
|
|
.call(
|
|
"PATCH",
|
|
&format!("/api/proposals/{pid}"),
|
|
Some(json!({ "title": i1["title"], "body": "Tout autre chose." })),
|
|
)
|
|
.await;
|
|
assert_eq!(s, StatusCode::CONFLICT);
|
|
assert_eq!(v["error"], "substantive_in_force");
|
|
|
|
// A member proposal: typo keeps votes, substance asks to reconfirm.
|
|
let uid = unit(&doc, "E2")["id"].as_str().expect("u").to_owned();
|
|
let (_, v) = a
|
|
.call("POST", &format!("/api/docs/{id}/proposals"), Some(json!({
|
|
"unitId": uid, "title": "Réciprocité", "body": "Je m'engage à ne certifier que des persones qui respectent ces engagements.", "submit": true,
|
|
})))
|
|
.await;
|
|
let p = v["id"].as_str().expect("id").to_owned();
|
|
b.call("POST", &format!("/api/proposals/{p}/vote"), Some(json!({ "choice": "for" }))).await;
|
|
// A misspelt word fixed: the vote stays.
|
|
let (_, v) = a
|
|
.call("PATCH", &format!("/api/proposals/{p}"), Some(json!({
|
|
"title": "Réciprocité", "body": "Je m'engage à ne certifier que des personnes qui respectent ces engagements.",
|
|
})))
|
|
.await;
|
|
assert_eq!(v["edit"], "typo");
|
|
let (_, d) = b.call("GET", &format!("/api/proposals/{p}"), None).await;
|
|
assert_eq!(d["proposal"]["tally"]["for"], 1, "a typo keeps the votes");
|
|
// Words added: a new revision, the vote waits for reconfirmation.
|
|
let (_, v) = a
|
|
.call("PATCH", &format!("/api/proposals/{p}"), Some(json!({
|
|
"title": "Réciprocité", "body": "Je m'engage à ne certifier que des personnes qui respectent ces deux engagements fondamentaux.",
|
|
})))
|
|
.await;
|
|
assert_eq!(v["edit"], "substantive");
|
|
let (_, d) = b.call("GET", &format!("/api/proposals/{p}"), None).await;
|
|
assert_eq!(d["proposal"]["myVoteStale"], true);
|
|
assert_eq!(d["proposal"]["tally"]["for"], 0);
|
|
b.call("POST", &format!("/api/proposals/{p}/vote"), Some(json!({ "choice": "for" }))).await;
|
|
let (_, d) = b.call("GET", &format!("/api/proposals/{p}"), None).await;
|
|
assert_eq!(d["proposal"]["myVoteStale"], false);
|
|
assert_eq!(d["proposal"]["tally"]["for"], 1);
|
|
assert_eq!(d["history"].as_array().expect("h").len(), 3);
|
|
// Someone else cannot change the substance.
|
|
let (s, _) = b
|
|
.call(
|
|
"PATCH",
|
|
&format!("/api/proposals/{p}"),
|
|
Some(json!({ "title": "Réciprocité", "body": "Autre chose entièrement." })),
|
|
)
|
|
.await;
|
|
assert_eq!(s, StatusCode::CONFLICT);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_contradiction_signal_blocks_entering_into_force() {
|
|
let (mut a, _) = Client::new().await;
|
|
a.login("Yvv", false).await;
|
|
let mut b = a.with(None);
|
|
b.login("Chiara", false).await;
|
|
let id = doc_id(&mut a, "essai").await;
|
|
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
|
let e2 = unit(&doc, "E2")["inForce"]["id"].as_str().expect("e2").to_owned();
|
|
let k1 = unit(&doc, "K1")["proposals"][0]["id"].as_str().expect("k1").to_owned();
|
|
let (s, v) = b
|
|
.call(
|
|
"POST",
|
|
&format!("/api/proposals/{k1}/signals"),
|
|
Some(json!({ "targetId": e2, "reason": "Ces règles contredisent la réciprocité en vigueur." })),
|
|
)
|
|
.await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
let sid = v["id"].as_str().expect("sid").to_owned();
|
|
a.call("POST", &format!("/api/proposals/{k1}/vote"), Some(json!({ "choice": "for" }))).await;
|
|
let (_, v) = b.call("POST", &format!("/api/proposals/{k1}/vote"), Some(json!({ "choice": "for" }))).await;
|
|
assert!(v["entered"].as_array().expect("e").is_empty(), "blocked while the signal stands");
|
|
// Only its author lifts it; then the pass lets the text in.
|
|
let (s, _) = a.call("POST", &format!("/api/signals/{sid}/lift"), None).await;
|
|
assert_eq!(s, StatusCode::FORBIDDEN);
|
|
let (_, v) = b.call("POST", &format!("/api/signals/{sid}/lift"), None).await;
|
|
assert_eq!(v["entered"][0]["code"], "K1");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn inertia_change_is_voted_one_level_up() {
|
|
let (mut a, _) = Client::new().await;
|
|
a.login("Yvv", false).await;
|
|
let id = doc_id(&mut a, "monnaie-libre").await;
|
|
let (_, doc) = a.call("GET", &format!("/api/docs/{id}"), None).await;
|
|
let uid = unit(&doc, "E1")["id"].as_str().expect("u").to_owned();
|
|
let (s, v) = a.call("POST", &format!("/api/units/{uid}/inertia"), Some(json!({ "target": "high" }))).await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
let p = v["id"].as_str().expect("id").to_owned();
|
|
let (_, again) = a.call("POST", &format!("/api/units/{uid}/inertia"), Some(json!({ "target": "high" }))).await;
|
|
assert_eq!(again["id"], p.as_str());
|
|
let (_, d) = a.call("GET", &format!("/api/proposals/{p}"), None).await;
|
|
assert_eq!(d["proposal"]["votedAtInertia"], "high");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn previews_and_assistant() {
|
|
let (mut a, _) = Client::new().await;
|
|
a.login("Yvv", false).await;
|
|
let id = doc_id(&mut a, "monnaie-libre").await;
|
|
let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=all"), None).await;
|
|
assert_eq!(v["units"].as_array().expect("u").len(), 33);
|
|
let (_, v) = a.call("GET", &format!("/api/docs/{id}/preview?mode=wellrated"), None).await;
|
|
assert_eq!(v["units"].as_array().expect("u").len(), 3, "nothing is well rated at zero votes");
|
|
let (_, v) = a
|
|
.call("POST", &format!("/api/docs/{id}/assist"), Some(json!({
|
|
"text": "Je m'engage sur l'honneur à n'avoir jamais qu'un seul compte cocréateur de monnaie G1.\n\nJe m'engage à rester joignable par mes certifieurs.",
|
|
})))
|
|
.await;
|
|
let kinds: Vec<&str> = v["hints"].as_array().expect("h").iter().filter_map(|h| h["kind"].as_str()).collect();
|
|
assert!(kinds.contains(&"split"), "{v}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn split_a_draft() {
|
|
let (mut a, _) = Client::new().await;
|
|
a.login("Yvv", false).await;
|
|
let id = doc_id(&mut a, "essai").await;
|
|
let (_, v) = a
|
|
.call("POST", &format!("/api/docs/{id}/proposals"), Some(json!({ "title": "Deux choses", "body": "a\n\nb" })))
|
|
.await;
|
|
let p = v["id"].as_str().expect("id").to_owned();
|
|
let (s, v) =
|
|
a.call("POST", &format!("/api/proposals/{p}/split"), Some(json!({ "parts": ["Première.", "Seconde."] }))).await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
assert_eq!(v["drafts"].as_array().expect("d").len(), 2);
|
|
let (s, _) = a.call("POST", &format!("/api/proposals/{p}/submit"), None).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn access_management_is_for_administrators_only() {
|
|
let (mut user, _) = Client::new().await;
|
|
user.login("Chiara", false).await;
|
|
let (s, _) = user.call("GET", "/api/access", None).await;
|
|
assert_eq!(s, StatusCode::FORBIDDEN);
|
|
let mut admin = user.with(None);
|
|
admin.login("Yvv", true).await;
|
|
let (s, v) =
|
|
admin.call("POST", "/api/access/invitations", Some(json!({ "label": "Chiara", "groups": ["essai"] }))).await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
let link = v["link"].as_str().expect("link").to_owned();
|
|
let token = link.split('#').nth(1).expect("token").to_owned();
|
|
let mut invited = user.with(None);
|
|
let (s, v) = invited.call("POST", "/api/auth/invitation", Some(json!({ "token": token }))).await;
|
|
assert_eq!(s, StatusCode::OK, "{v}");
|
|
assert_eq!(v["me"]["kind"], "invite");
|
|
let (_, g) = invited.call("GET", "/api/groups", None).await;
|
|
let open: Vec<&str> = g["groups"]
|
|
.as_array()
|
|
.expect("g")
|
|
.iter()
|
|
.filter(|g| g["open"] == true)
|
|
.filter_map(|g| g["slug"].as_str())
|
|
.collect();
|
|
assert_eq!(open, ["essai"]);
|
|
// Single use.
|
|
let mut again = user.with(None);
|
|
let (s, _) = again.call("POST", "/api/auth/invitation", Some(json!({ "token": token }))).await;
|
|
assert_eq!(s, StatusCode::BAD_REQUEST);
|
|
// Monnaie libre is seen but closed to an invited identity.
|
|
let (s, _) = invited.call("GET", "/api/groups/axiom", None).await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
let (_, ov) = admin.call("GET", "/api/access", None).await;
|
|
assert!(ov["identities"].as_array().expect("i").iter().any(|i| i["displayName"] == "Chiara"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn feedback_reaches_administrators() {
|
|
let (mut user, _) = Client::new().await;
|
|
user.login("Chiara", false).await;
|
|
let (s, _) = user
|
|
.call("POST", "/api/feedback", Some(json!({ "route": "/g/essai", "body": "Le bouton est trop discret." })))
|
|
.await;
|
|
assert_eq!(s, StatusCode::OK);
|
|
let mut admin = user.with(None);
|
|
admin.login("Yvv", true).await;
|
|
let (_, v) = admin.call("GET", "/api/access/feedback", None).await;
|
|
assert_eq!(v["feedback"][0]["body"], "Le bouton est trop discret.");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn publication_writes_the_texts_in_force_to_the_repository() {
|
|
use std::process::Command;
|
|
let dir = std::env::temp_dir().join(format!("ld-pub-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).expect("dir");
|
|
let remote = dir.join("textes.git");
|
|
assert!(Command::new("git").args(["init", "-q", "--bare"]).arg(&remote).status().expect("git").success());
|
|
let mut cfg = Config::for_tests();
|
|
cfg.data_dir = dir.join("data");
|
|
cfg.publish = Some(ld_server::config::Publish {
|
|
remote: remote.display().to_string(),
|
|
branch: "main".into(),
|
|
key: dir.join("data/keys/publish_ed25519"),
|
|
author_name: "libreDecision".into(),
|
|
author_email: "decision@example.org".into(),
|
|
});
|
|
let db = open_db("sqlite::memory:").await.expect("db");
|
|
let state = AppState::new(cfg, db).await.expect("state");
|
|
seed::ensure(&state).await.expect("seed");
|
|
ld_server::publish::pass(&state).await.expect("pass");
|
|
let show = |path: &str| {
|
|
let out = Command::new("git")
|
|
.arg("-C")
|
|
.arg(&remote)
|
|
.args(["show", &format!("main:{path}")])
|
|
.output()
|
|
.expect("git show");
|
|
String::from_utf8(out.stdout).expect("utf8")
|
|
};
|
|
let fr = show("monnaie-libre/acte-engagement-certification/fr.md");
|
|
assert!(fr.contains("# Acte d'engagement Certification"), "{fr}");
|
|
assert!(fr.contains("## Préambule") && fr.contains("## Unicité du compte") && fr.contains("## Réciprocité"));
|
|
assert!(!fr.contains("Connaissance suffisante"), "only the texts in force are published");
|
|
assert!(show("monnaie-libre/acte-engagement-certification/en.md").contains("traduction: editoriale"));
|
|
let json: Value =
|
|
serde_json::from_str(&show("monnaie-libre/acte-engagement-certification/document.json")).expect("json");
|
|
assert_eq!(json["units"].as_array().expect("units").len(), 3);
|
|
assert_eq!(json["units"][0]["status"], "seed-minimal");
|
|
assert!(show("README.md").contains("libreDecision"));
|
|
let done: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM outbox WHERE done_at IS NULL")
|
|
.fetch_one(&state.db)
|
|
.await
|
|
.expect("count");
|
|
assert_eq!(done, 0);
|
|
// A restart queues the document again; nothing changed, so nothing is committed.
|
|
let head = || {
|
|
let out = Command::new("git").arg("-C").arg(&remote).args(["rev-parse", "main"]).output().expect("rev-parse");
|
|
String::from_utf8(out.stdout).expect("utf8")
|
|
};
|
|
let before = head();
|
|
ld_server::publish::resync(&state).await.expect("resync");
|
|
ld_server::publish::pass(&state).await.expect("pass");
|
|
assert_eq!(head(), before, "an unchanged document produces no commit");
|
|
let pending: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM outbox WHERE done_at IS NULL")
|
|
.fetch_one(&state.db)
|
|
.await
|
|
.expect("count");
|
|
assert_eq!(pending, 0);
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|