v3 : liens d'équipe, publication sur git.open.us.org, huitième relecture
Liens d'équipe (Yvv, 2026-10-21) : un lien réutilisable jusqu'à une date
et pour un nombre de personnes, révocable. Chaque ouverture demande un nom
et crée sa propre identité d'invitation, jamais administratrice ; un nom
déjà pris est refusé ; une personne déjà connectée reçoit les groupes.
trustinWallet montre par quel lien chaque identité est venue, et donne un
nouveau lien de connexion à une personne invitée (autre appareil).
Migration additive 0002 (table team_links, colonne identities.team_link).
Publication : yvv/libredecision-textes sur git.open.us.org, pour
l'instant (un dépôt pour l'ouverture, puis le dépôt officiel, viendront).
Huitième relecture (c3d6bf8 : ok), points corrigés :
- une négation n'est jamais coupée ni formée par collage (« ja mais ») ;
- caractères invisibles : tout l'ensemble Unicode des ignorables et les
contrôles autres que saut de ligne et tabulation ;
- sauts de ligne et indentation font partie de la forme (listes) ;
- un titre tient sur une ligne.
Boucle : 65 tests Rust, svelte-check, 9 vitest, e2e 13/13 (C12 ajouté).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -251,6 +251,11 @@ async fn editing_is_not_voting() {
|
||||
)
|
||||
.await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("hidden_characters")));
|
||||
// A title is one line.
|
||||
let (s, v) = b
|
||||
.call("PATCH", &format!("/api/proposals/{e2_id}"), Some(json!({ "title": "Récipro\ncité", "body": body })))
|
||||
.await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("title")));
|
||||
// The title never absorbs the start of the body.
|
||||
let title = e2["title"].as_str().expect("title");
|
||||
let (head, rest) = body.split_once(' ').expect("two words");
|
||||
@@ -460,6 +465,89 @@ async fn access_management_is_for_administrators_only() {
|
||||
assert!(ov["identities"].as_array().expect("i").iter().any(|i| i["displayName"] == "Chiara"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_team_link_makes_one_identity_per_opening() {
|
||||
let (mut admin, _) = Client::new().await;
|
||||
admin.login("Yvv", true).await;
|
||||
let mut user = admin.with(None);
|
||||
user.login("Chiara", false).await;
|
||||
let team = |groups: Value, days: i64, max: i64| json!({ "label": "Équipe dev", "groups": groups, "days": days, "maxUses": max });
|
||||
let (s, _) = user.call("POST", "/api/access/team-links", Some(team(json!(["essai"]), 30, 2))).await;
|
||||
assert_eq!(s, StatusCode::FORBIDDEN, "administrators only");
|
||||
for (body, code) in [
|
||||
(team(json!([]), 30, 2), "groups"),
|
||||
(team(json!(["nope"]), 30, 2), "groups"),
|
||||
(team(json!(["essai"]), 0, 2), "team_limits"),
|
||||
(team(json!(["essai"]), 30, 501), "team_limits"),
|
||||
] {
|
||||
let (s, v) = admin.call("POST", "/api/access/team-links", Some(body)).await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some(code)));
|
||||
}
|
||||
let (s, v) = admin.call("POST", "/api/access/team-links", Some(team(json!(["essai"]), 30, 2))).await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
let link = v["link"].as_str().expect("link").to_owned();
|
||||
assert!(link.contains("/equipe#"), "{link}");
|
||||
let token = link.split('#').nth(1).expect("token").to_owned();
|
||||
|
||||
let mut first = admin.with(None);
|
||||
let (s, v) = first.call("POST", "/api/auth/team/peek", Some(json!({ "token": token }))).await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
assert_eq!(v["label"], "Équipe dev");
|
||||
assert_eq!(v["groups"].as_array().expect("g").len(), 1);
|
||||
assert_eq!(v["signedInAs"], Value::Null);
|
||||
let (s, v) = first.call("POST", "/api/auth/team", Some(json!({ "token": token, "name": "Lu\u{202e}na" }))).await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("name")));
|
||||
let (s, v) = first.call("POST", "/api/auth/team", Some(json!({ "token": token, "name": " Luna " }))).await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
assert_eq!((v["me"]["kind"].as_str(), v["me"]["admin"].as_bool()), (Some("invite"), Some(false)));
|
||||
assert_eq!(v["me"]["displayName"], "Luna");
|
||||
let luna = v["me"]["id"].as_str().expect("id").to_owned();
|
||||
let (_, g) = first.call("GET", "/api/groups", None).await;
|
||||
assert!(g["groups"].as_array().expect("g").iter().any(|g| g["slug"] == "essai" && g["open"] == true));
|
||||
|
||||
// Nobody joins under a name already there; the refusal does not count as an opening.
|
||||
let mut second = admin.with(None);
|
||||
let (s, v) = second.call("POST", "/api/auth/team", Some(json!({ "token": token, "name": "luna" }))).await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("name_taken")));
|
||||
let (s, v) = second.call("POST", "/api/auth/team", Some(json!({ "token": token, "name": "Paul" }))).await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
assert_ne!(v["me"]["id"].as_str(), Some(luna.as_str()), "one identity per opening");
|
||||
// Two openings allowed: the third is refused.
|
||||
let mut third = admin.with(None);
|
||||
let (s, v) = third.call("POST", "/api/auth/team", Some(json!({ "token": token, "name": "Zoé" }))).await;
|
||||
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("invitation_invalid")));
|
||||
|
||||
// Someone signed in keeps their identity and receives the groups.
|
||||
let (_, v) = admin.call("POST", "/api/access/team-links", Some(team(json!(["monnaie-libre"]), 7, 5))).await;
|
||||
let token2 = v["link"].as_str().expect("link").split('#').nth(1).expect("token").to_owned();
|
||||
let (_, v) = first.call("POST", "/api/auth/team/peek", Some(json!({ "token": token2 }))).await;
|
||||
assert_eq!(v["signedInAs"], "Luna");
|
||||
let (s, v) = first.call("POST", "/api/auth/team", Some(json!({ "token": token2 }))).await;
|
||||
assert_eq!(s, StatusCode::OK, "{v}");
|
||||
assert_eq!(v["me"]["id"].as_str(), Some(luna.as_str()));
|
||||
let (s, _) = first.call("GET", "/api/groups/monnaie-libre", None).await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
|
||||
// Administrators see the links still open and where each identity came from; a link revokes.
|
||||
let (_, ov) = admin.call("GET", "/api/access", None).await;
|
||||
let links = ov["teamLinks"].as_array().expect("links");
|
||||
assert_eq!(links.len(), 2, "{ov}");
|
||||
assert!(
|
||||
ov["identities"]
|
||||
.as_array()
|
||||
.expect("i")
|
||||
.iter()
|
||||
.any(|i| i["displayName"] == "Paul" && i["teamLink"] == "Équipe dev")
|
||||
);
|
||||
let open = links.iter().find(|l| l["uses"] == 1).expect("second link");
|
||||
let (s, _) =
|
||||
admin.call("DELETE", &format!("/api/access/team-links/{}", open["id"].as_str().expect("id")), None).await;
|
||||
assert_eq!(s, StatusCode::OK);
|
||||
let mut late = admin.with(None);
|
||||
let (s, _) = late.call("POST", "/api/auth/team", Some(json!({ "token": token2, "name": "Tardif" }))).await;
|
||||
assert_eq!(s, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn in_monnaie_libre_only_web_of_trust_members_vote() {
|
||||
let (mut admin, _) = Client::new().await;
|
||||
|
||||
Reference in New Issue
Block a user