//! 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, } 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) -> (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 { 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::().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 seeds a minima + 30 open articles, the two variants counting once. assert_eq!(docs[0]["toVote"], 33); assert_eq!(docs[0]["withoutMyVote"], 33); 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", "inForce"), "for").await; let d = decisions(&mut c, "essai").await; assert_eq!((d["docs"][0]["toVote"].as_u64(), d["docs"][0]["withoutMyVote"].as_u64()), (Some(33), Some(31))); // 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"], 33); } #[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(32), Some(32))); // 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"], 33); yvv.vote(&added, "against").await; assert_eq!(decisions(&mut yvv, "essai").await["docs"][0]["withoutMyVote"], 32); // 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"], 33); } #[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"], 33); 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"], 33); }