forked from yvv/decision
Fusion du lot A : accueil sur le choix du groupe, logo Clé de voûte, quatre ambiances, menu, À venir, Décisions
Contrôles du lot R renumérotés C18 (Raison d'être) et C19 (versions d'un article). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
//! 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() {
|
||||
let mut c = Client::new().await;
|
||||
c.login("Chiara", false).await;
|
||||
let d = decisions(&mut c, "monnaie-libre").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, "monnaie-libre").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, "monnaie-libre").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, "monnaie-libre").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);
|
||||
}
|
||||
Reference in New Issue
Block a user