Files
decision/crates/ld-server/tests/overview.rs
T
YvvandClaude Opus 5.5 aaa653e1a7 Lot S : tests serveur repris sans Propositions seed en vigueur (modules, overview), rustfmt
modules.rs : 0 article en vigueur et 35 propositions en cours dans une base
neuve (I1, E1 et E2 sont des Propositions seed) ; l'ajout des modules à une
base existante se rejoue sur le seed de production du 02/11 (textes en
vigueur), comme inertia.rs. overview.rs : le vote porte sur la Proposition
seed I1 ; 41 modules à voter, inchangé.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:12:20 +01:00

257 lines
11 KiB
Rust

//! A group at a glance: what is coming in each tool, and what is left to vote.
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 {
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), cookie: None }
}
fn other(&self) -> Self {
Self { router: self.router.clone(), cookie: None }
}
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 get(&mut self, uri: &str) -> Value {
let (s, v) = self.call("GET", uri, None).await;
assert_eq!(s, StatusCode::OK, "{uri}: {v}");
v
}
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}");
}
/// An invitation identity that opens only `groups`.
async fn invited(&mut self, admin: &mut Client, groups: &[&str]) {
let (s, v) =
admin.call("POST", "/api/access/invitations", Some(json!({ "label": "Invitée", "groups": groups }))).await;
assert_eq!(s, StatusCode::OK, "{v}");
let token = v["link"].as_str().expect("link").split('#').nth(1).expect("token").to_owned();
let (s, v) = self.call("POST", "/api/auth/invitation", Some(json!({ "token": token }))).await;
assert_eq!(s, StatusCode::OK, "{v}");
}
async fn vote(&mut self, proposal: &str, choice: &str) -> Value {
let (s, v) =
self.call("POST", &format!("/api/proposals/{proposal}/vote"), Some(json!({ "choice": choice }))).await;
assert_eq!(s, StatusCode::OK, "{v}");
v
}
}
fn titles(items: &Value, lang: &str) -> Vec<String> {
items.as_array().expect("items").iter().map(|i| i["title"][lang].as_str().expect("title").to_owned()).collect()
}
async fn the_doc(c: &mut Client, group: &str) -> Value {
let v = c.get(&format!("/api/groups/{group}")).await;
let id = v["docs"][0]["id"].as_str().expect("doc").to_owned();
c.get(&format!("/api/docs/{id}")).await
}
fn version(doc: &Value, code: &str, which: &str) -> String {
let unit = doc["units"].as_array().expect("units").iter().find(|u| u["code"] == code).expect("unit");
let v = if which == "inForce" { &unit["inForce"] } else { &unit["proposals"][which.parse::<usize>().expect("n")] };
v["id"].as_str().expect("id").to_owned()
}
async fn decisions(c: &mut Client, group: &str) -> Value {
c.get(&format!("/api/groups/{group}/decisions")).await
}
#[tokio::test]
async fn what_is_coming_is_served_with_the_group_in_three_languages() {
let mut c = Client::new().await;
c.login("Chiara", false).await;
let g = c.get("/api/groups/monnaie-libre").await;
let up = &g["upcoming"];
assert_eq!(titles(&up["docs"], "fr"), ["Acte d'engagement forgerons"]);
assert_eq!(titles(&up["mandats"], "fr"), ["Administration forum Duniter", "Administration forum Monnaie libre"]);
assert_eq!(titles(&up["protocoles"], "fr"), ["Runtime upgrade — décision on-chain"]);
for tool in ["docs", "mandats", "protocoles"] {
for lang in ["en", "es"] {
assert!(titles(&up[tool], lang).iter().all(|t| !t.is_empty()), "{tool} {lang}");
}
}
// The docs in progress are still there, and only Monnaie libre has something coming.
assert_eq!(g["docs"].as_array().expect("docs").len(), 1);
let essai = c.get("/api/groups/essai").await;
for tool in ["docs", "mandats", "protocoles"] {
assert_eq!(essai["upcoming"][tool], json!([]), "{tool}");
}
}
#[tokio::test]
async fn a_group_one_does_not_open_shows_nothing_of_what_is_coming() {
let mut admin = Client::new().await;
admin.login("Yvv", true).await;
let mut guest = admin.other();
guest.invited(&mut admin, &["essai"]).await;
let g = guest.get("/api/groups/axiom").await;
assert_eq!(g["group"]["open"], false);
assert_eq!(g["docs"], json!([]));
for tool in ["docs", "mandats", "protocoles"] {
assert_eq!(g["upcoming"][tool], json!([]), "{tool}");
}
let (s, _) = guest.call("GET", "/api/groups/axiom/decisions", None).await;
assert_eq!(s, StatusCode::FORBIDDEN);
let (s, _) = guest.other().call("GET", "/api/groups/monnaie-libre/decisions", None).await;
assert_eq!(s, StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn decisions_one_line_per_doc_with_the_modules_left_to_vote() {
// In the test group, which votes on the server (Monnaie libre votes on the chain:
// tests/ballots.rs counts « sans ton vote » there).
let mut c = Client::new().await;
c.login("Chiara", false).await;
let d = decisions(&mut c, "essai").await;
assert_eq!(d["canVote"], true);
let docs = d["docs"].as_array().expect("docs");
assert_eq!(docs.len(), 1);
assert_eq!(docs[0]["slug"], "acte-engagement-certification");
assert_eq!(docs[0]["title"]["fr"], "Acte d'engagement Certification");
// 3 seed proposals (ADR 0011) + 30 open articles, the two variants counting once; the title
// of the document and the order of 7 sections (« ordonnancement » holds one article: nothing
// to decide).
assert_eq!(docs[0]["toVote"], 41);
assert_eq!(docs[0]["withoutMyVote"], 41);
let doc = the_doc(&mut c, "essai").await;
c.vote(&version(&doc, "E3", "0"), "for").await;
c.vote(&version(&doc, "E3", "1"), "against").await;
c.vote(&version(&doc, "I1", "0"), "for").await;
let d = decisions(&mut c, "essai").await;
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(41), Some(39)));
// Someone else has not voted yet.
let mut yvv = c.other();
yvv.login("Yvv", false).await;
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 41);
}
#[tokio::test]
async fn an_adopted_module_leaves_the_count_and_a_vote_to_reconfirm_does_not_count() {
let mut yvv = Client::new().await;
yvv.login("Yvv", false).await;
let mut chiara = yvv.other();
chiara.login("Chiara", false).await;
// Dev identities are members of every group: W(essai) = 2, C = 1; two « for » adopt.
let doc = the_doc(&mut yvv, "essai").await;
let e3 = version(&doc, "E3", "0");
yvv.vote(&e3, "for").await;
let v = chiara.vote(&e3, "for").await;
assert_eq!(v["entered"][0]["code"], "E3");
let d = decisions(&mut yvv, "essai").await;
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(40), Some(40)));
// A new article by Chiara: one more module to vote.
let doc_id = doc["doc"]["id"].as_str().expect("id").to_owned();
let (s, v) = chiara
.call(
"POST",
&format!("/api/docs/{doc_id}/proposals"),
Some(json!({
"title": "Joignabilité", "body": "Je m'engage à rester joignable par mes certifieurs.", "submit": true,
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{v}");
let added = v["id"].as_str().expect("id").to_owned();
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["toVote"], 41);
yvv.vote(&added, "against").await;
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 40);
// Chiara changes its meaning: Yvv's vote is to reconfirm, it no longer counts as his vote.
let (s, v) = chiara
.call(
"PATCH",
&format!("/api/proposals/{added}"),
Some(json!({
"title": "Joignabilité", "body": "Je m'engage à répondre à mes certifieurs sous huit jours.",
})),
)
.await;
assert_eq!(s, StatusCode::OK, "{v}");
assert_eq!(v["edit"], "substantive");
assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 41);
}
#[tokio::test]
async fn a_version_held_by_an_objection_is_not_to_vote_while_it_is_held() {
let mut yvv = Client::new().await;
yvv.login("Yvv", true).await;
let doc = the_doc(&mut yvv, "essai").await;
// Two articles of a single open version each: one of them is held (ADR 0010).
let (i2, k1) = (version(&doc, "I2", "0"), version(&doc, "K1", "0"));
let (s, v) = yvv
.call(
"POST",
&format!("/api/proposals/{i2}/objections"),
Some(json!({ "otherId": k1, "kind": "contredit", "reason": "Les deux disent l'inverse." })),
)
.await;
assert_eq!(s, StatusCode::OK, "{v}");
// Nobody can vote on it while it is held: one module fewer to vote, for everyone.
let d = decisions(&mut yvv, "essai").await;
assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(40), Some(40)));
// The list of the group still counts it among the proposals in progress: it is open.
let g = yvv.get("/api/groups/essai").await;
let open = doc["units"]
.as_array()
.expect("units")
.iter()
.map(|u| u["proposals"].as_array().expect("p").iter().filter(|p| p["status"] == "open").count() as u64);
assert_eq!(g["docs"][0]["open"].as_u64(), Some(open.sum::<u64>()));
}
#[tokio::test]
async fn where_one_does_not_vote_there_is_no_count_of_ones_own() {
let mut admin = Client::new().await;
admin.login("Yvv", true).await;
let mut guest = admin.other();
guest.invited(&mut admin, &["monnaie-libre", "essai"]).await;
let d = decisions(&mut guest, "monnaie-libre").await;
assert_eq!(d["canVote"], false);
assert_eq!(d["docs"][0]["toVote"], 41);
assert!(d["docs"][0]["withoutMyVote"].is_null());
// In the test group, the same person votes.
let d = decisions(&mut guest, "essai").await;
assert_eq!(d["canVote"], true);
assert_eq!(d["docs"][0]["withoutMyVote"], 41);
}