@@ -0,0 +1,596 @@
//! Modules (ADR 0009): the title of the document (level 4) and the order of the
//! articles of each section (level 2), voted like articles; the guarded
//! addition that gives them to a document born without them.
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 {
fn of ( state : & AppState ) -> Self {
Self { router : app ( state . clone ( ) ) , cookie : None }
}
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 ::of ( & state ) , state )
}
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 ok ( & mut self , method : & str , uri : & str , body : Option < Value > ) -> Value {
let ( s , v ) = self . call ( method , uri , body ) . await ;
assert_eq! ( s , StatusCode ::OK , " {method} {uri}: {v} " ) ;
v
}
async fn login ( & mut self , name : & str ) {
self . ok ( " POST " , " /api/auth/dev " , Some ( json! ( { " name " : name , " admin " : false } ) ) ) . await ;
}
async fn doc ( & mut self , group : & str ) -> Value {
let g = self . ok ( " GET " , & format! ( " /api/groups/ {group} " ) , None ) . await ;
let id = g [ " docs " ] [ 0 ] [ " id " ] . as_str ( ) . expect ( " doc " ) . to_owned ( ) ;
self . ok ( " GET " , & format! ( " /api/docs/ {id} " ) , None ) . await
}
async fn vote ( & mut self , proposal : & str , choice : & str ) -> Value {
self . ok ( " POST " , & format! ( " /api/proposals/ {proposal} /vote " ) , Some ( json! ( { " choice " : choice } ) ) ) . await
}
}
fn unit < ' a > ( doc : & ' a Value , code : & str ) -> & ' a Value {
doc [ " units " ] . as_array ( ) . expect ( " units " ) . iter ( ) . find ( | u | u [ " code " ] = = code ) . expect ( " unit " )
}
fn id_of ( doc : & Value , code : & str ) -> String {
unit ( doc , code ) [ " id " ] . as_str ( ) . expect ( " id " ) . to_owned ( )
}
fn order_of < ' a > ( doc : & ' a Value , section : & str ) -> & ' a Value {
doc [ " modules " ] [ " orders " ] . as_array ( ) . expect ( " orders " ) . iter ( ) . find ( | o | o [ " section " ] = = section ) . expect ( " order " )
}
/// The codes of the articles of a section one can see (a text in force or an open
/// version), in the order the document reads them.
fn read_order ( doc : & Value , section : & str ) -> Vec < String > {
let seen = | u : & Value | {
! u [ " inForce " ] . is_null ( )
| | u [ " proposals " ]
. as_array ( )
. is_some_and ( | p | p . iter ( ) . any ( | p | p [ " status " ] = = " open " & & p [ " kind " ] ! = " inertia " ) )
} ;
let mut units : Vec < & Value > =
doc [ " units " ] . as_array ( ) . expect ( " units " ) . iter ( ) . filter ( | u | u [ " section " ] = = section & & seen ( u ) ) . collect ( ) ;
units . sort_by ( | a , b | a [ " position " ] . as_f64 ( ) . unwrap_or ( 0.0 ) . total_cmp ( & b [ " position " ] . as_f64 ( ) . unwrap_or ( 0.0 ) ) ) ;
units . iter ( ) . map ( | u | u [ " code " ] . as_str ( ) . expect ( " code " ) . to_owned ( ) ) . collect ( )
}
fn codes ( doc : & Value , ids : & Value ) -> Vec < String > {
ids . as_array ( )
. expect ( " ids " )
. iter ( )
. map ( | id | {
doc [ " units " ]
. as_array ( )
. expect ( " units " )
. iter ( )
. find ( | u | u [ " id " ] = = * id )
. map ( | u | u [ " code " ] . as_str ( ) . expect ( " code " ) . to_owned ( ) )
. unwrap_or_else ( | | format! ( " ? {id} " ) )
} )
. collect ( )
}
/// Two people of the test group (W = 2, C = 1): two « for » adopt.
async fn two ( ) -> ( Client , Client , AppState ) {
let ( mut a , state ) = Client ::new ( ) . await ;
a . login ( " Yvv " ) . await ;
let mut b = a . other ( ) ;
b . login ( " Chiara " ) . await ;
( a , b , state )
}
#[ tokio::test ]
async fn a_new_base_has_its_title_and_the_order_of_each_section ( ) {
let ( mut c , state ) = Client ::new ( ) . await ;
c . login ( " Chiara " ) . await ;
for group in [ " monnaie-libre " , " essai " ] {
let doc = c . doc ( group ) . await ;
// The articles stay the articles: 33 of them, none of the modules.
assert_eq! ( doc [ " units " ] . as_array ( ) . expect ( " units " ) . len ( ) , 33 , " {group} " ) ;
let title = & doc [ " modules " ] [ " title " ] ;
assert_eq! (
( title [ " kind " ] . as_str ( ) , title [ " code " ] . as_str ( ) , title [ " inertia " ] . as_u64 ( ) ) ,
( Some ( " title " ) , Some ( " titre " ) , Some ( 4 ) )
) ;
let v = & title [ " inForce " ] ;
assert_eq! (
( v [ " title " ] . as_str ( ) , v [ " seedMinimal " ] . as_bool ( ) ) ,
( Some ( " Acte d'engagement Certification " ) , Some ( true ) )
) ;
assert_eq! ( v [ " translations " ] [ " en " ] [ " title " ] , " Certification Commitment Act " ) ;
assert_eq! ( v [ " translations " ] [ " es " ] [ " stale " ] , false ) ;
// One order per section, in the order of the document, each at level 2.
let orders = doc [ " modules " ] [ " orders " ] . as_array ( ) . expect ( " orders " ) ;
let sections : Vec < & str > = orders . iter ( ) . filter_map ( | o | o [ " section " ] . as_str ( ) ) . collect ( ) ;
assert_eq! (
sections ,
[
" introduction " ,
" fondamental " ,
" technique " ,
" conclusion " ,
" annexe " ,
" formule " ,
" inertie " ,
" ordonnancement "
]
) ;
assert! ( orders . iter ( ) . all ( | o | o [ " inertia " ] = = 2 & & o [ " inForce " ] [ " seedMinimal " ] = = true ) ) ;
// Its version in force is the order the section reads in now.
let fondamental = order_of ( & doc , " fondamental " ) ;
assert_eq! ( codes ( & doc , & fondamental [ " members " ] ) , [ " T1 " , " E1 " , " E2 " ] ) ;
assert_eq! ( fondamental [ " inForce " ] [ " order " ] [ " ids " ] , fondamental [ " members " ] ) ;
assert_eq! ( fondamental [ " moot " ] , false ) ;
// « Ordonnancement » holds one article: nothing to decide.
assert_eq! ( order_of ( & doc , " ordonnancement " ) [ " moot " ] , true ) ;
}
// Born with them: the addition says so, once per document.
let rows : Vec < ( String , String ) > = sqlx ::query_as (
" SELECT d.group_slug, m.outcome FROM module_amendments m JOIN documents d ON d.id = m.doc_id ORDER BY d.group_slug " ,
)
. fetch_all ( & state . db )
. await
. expect ( " decisions " ) ;
assert_eq! ( rows , [ ( " essai " . into ( ) , " present " . into ( ) ) , ( " monnaie-libre " . into ( ) , " present " . into ( ) ) ] ) ;
// The previews show articles only.
let doc = c . doc ( " monnaie-libre " ) . await ;
let id = doc [ " doc " ] [ " id " ] . as_str ( ) . expect ( " id " ) ;
let all = c . ok ( " GET " , & format! ( " /api/docs/ {id} /preview?mode=all " ) , None ) . await ;
assert_eq! ( all [ " units " ] . as_array ( ) . expect ( " units " ) . len ( ) , 33 ) ;
// The list of the group counts articles only.
let g = c . ok ( " GET " , " /api/groups/monnaie-libre " , None ) . await ;
assert_eq! ( ( g [ " docs " ] [ 0 ] [ " inForce " ] . as_u64 ( ) , g [ " docs " ] [ 0 ] [ " open " ] . as_u64 ( ) ) , ( Some ( 3 ) , Some ( 32 ) ) ) ;
}
#[ tokio::test ]
async fn an_order_is_proposed_with_the_articles_of_its_section_then_voted_into_force ( ) {
let ( mut yvv , mut chiara , _ ) = two ( ) . await ;
let doc = yvv . doc ( " essai " ) . await ;
let conclusion = order_of ( & doc , " conclusion " ) . clone ( ) ;
let module = conclusion [ " id " ] . as_str ( ) . expect ( " id " ) . to_owned ( ) ;
let ( k1 , k2 ) = ( id_of ( & doc , " K1 " ) , id_of ( & doc , " K2 " ) ) ;
let uri = format! ( " /api/units/ {module} /order " ) ;
// Every article of the section, each once, in another order.
for ( wrong , code ) in [
( json! ( [ k1 ] ) , " order " ) ,
( json! ( [ k2 , k1 , k1 ] ) , " order " ) ,
( json! ( [ k2 , id_of ( & doc , " E1 " ) ] ) , " order " ) ,
( json! ( [ k1 , k2 ] ) , " same_order " ) ,
] {
let ( s , v ) = yvv . call ( " POST " , & uri , Some ( json! ( { " order " : wrong } ) ) ) . await ;
assert_eq! ( ( s , v [ " error " ] . as_str ( ) ) , ( StatusCode ::BAD_REQUEST , Some ( code ) ) , " {wrong} " ) ;
}
let first = yvv . ok ( " POST " , & uri , Some ( json! ( { " order " : [ k2 , k1 ] } ) ) ) . await ;
assert_eq! ( first [ " existing " ] , false ) ;
// The same order again: the open one is given back.
let again = chiara . ok ( " POST " , & uri , Some ( json! ( { " order " : [ k2 , k1 ] } ) ) ) . await ;
assert_eq! ( ( again [ " id " ] . as_str ( ) , again [ " existing " ] . as_bool ( ) ) , ( first [ " id " ] . as_str ( ) , Some ( true ) ) ) ;
let p = first [ " id " ] . as_str ( ) . expect ( " id " ) . to_owned ( ) ;
let doc = yvv . doc ( " essai " ) . await ;
let proposal = & order_of ( & doc , " conclusion " ) [ " proposals " ] [ 0 ] ;
assert_eq! ( ( proposal [ " kind " ] . as_str ( ) , proposal [ " votedAtInertia " ] . as_u64 ( ) ) , ( Some ( " order " ) , Some ( 2 ) ) ) ;
assert_eq! ( codes ( & doc , & proposal [ " order " ] [ " ids " ] ) , [ " K2 " , " K1 " ] ) ;
assert_eq! ( proposal [ " order " ] [ " same " ] , false ) ;
// Voted like an article: one voice of two is not enough, the second brings it into force.
assert! ( yvv . vote ( & p , " for " ) . await [ " entered " ] . as_array ( ) . expect ( " e " ) . is_empty ( ) ) ;
let v = chiara . vote ( & p , " for " ) . await ;
assert_eq! (
( v [ " entered " ] [ 0 ] [ " code " ] . as_str ( ) , v [ " entered " ] [ 0 ] [ " kind " ] . as_str ( ) ) ,
( Some ( " ordre.conclusion " ) , Some ( " order " ) )
) ;
let doc = yvv . doc ( " essai " ) . await ;
assert_eq! ( read_order ( & doc , " conclusion " ) , [ " K2 " , " K1 " ] ) ;
let now = order_of ( & doc , " conclusion " ) ;
assert_eq! ( now [ " inForce " ] [ " id " ] , p . as_str ( ) ) ;
assert_eq! ( codes ( & doc , & now [ " inForce " ] [ " order " ] [ " ids " ] ) , [ " K2 " , " K1 " ] ) ;
// The order of before goes back among the proposals, with its votes, and could return.
let back = & now [ " proposals " ] [ 0 ] ;
assert_eq! ( ( back [ " status " ] . as_str ( ) , back [ " origin " ] . as_str ( ) ) , ( Some ( " open " ) , Some ( " seed " ) ) ) ;
assert_eq! ( codes ( & doc , & back [ " order " ] [ " ids " ] ) , [ " K1 " , " K2 " ] ) ;
// Nothing else moved: the other sections read as before, the texts are the same.
assert_eq! ( read_order ( & doc , " fondamental " ) , [ " T1 " , " E1 " , " E2 " ] ) ;
assert_eq! ( read_order ( & doc , " technique " ) [ 0 ] , " T2 " ) ;
}
#[ tokio::test ]
async fn articles_that_enter_or_leave_a_section_complete_an_order_under_vote ( ) {
let ( mut yvv , mut chiara , _ ) = two ( ) . await ;
let doc = yvv . doc ( " essai " ) . await ;
let module = order_of ( & doc , " inertie " ) [ " id " ] . as_str ( ) . expect ( " id " ) . to_owned ( ) ;
let doc_id = doc [ " doc " ] [ " id " ] . as_str ( ) . expect ( " id " ) . to_owned ( ) ;
let ( n1 , n11 ) = ( id_of ( & doc , " N1 " ) , id_of ( & doc , " N1.1 " ) ) ;
let p = yvv . ok ( " POST " , & format! ( " /api/units/ {module} /order " ) , Some ( json! ( { " order " : [ n11 , n1 ] } ) ) ) . await [ " id " ]
. as_str ( )
. expect ( " id " )
. to_owned ( ) ;
yvv . vote ( & p , " for " ) . await ;
// A new article enters the section: at its end, and at the end of the order under vote.
let created = chiara
. ok (
" POST " ,
& format! ( " /api/docs/ {doc_id} /proposals " ) ,
Some ( json! ( { " section " : " inertie " , " title " : " Inertie des modules " , " body " : " Le titre du document est à 4. " , " submit " : true } ) ) ,
)
. await ;
let doc = yvv . doc ( " essai " ) . await ;
let fresh = unit ( & doc , " N1 " ) [ " code " ]
. as_str ( )
. map ( | _ | ( ) )
. and_then ( | ( ) | doc [ " units " ] . as_array ( ) . expect ( " u " ) . iter ( ) . find ( | u | u [ " id " ] = = created [ " unitId " ] ) . cloned ( ) ) ;
let fresh = fresh . expect ( " the new article " ) ;
let new_code = fresh [ " code " ] . as_str ( ) . expect ( " code " ) . to_owned ( ) ;
assert_eq! ( read_order ( & doc , " inertie " ) , [ " N1 " , " N1.1 " , new_code . as_str ( ) ] , " a new article goes at the end " ) ;
let module_now = order_of ( & doc , " inertie " ) ;
let open = module_now [ " proposals " ] . as_array ( ) . expect ( " p " ) . iter ( ) . find ( | x | x [ " id " ] = = p . as_str ( ) ) . expect ( " p " ) ;
assert_eq! ( codes ( & doc , & open [ " order " ] [ " ids " ] ) , [ " N1.1 " , " N1 " , new_code . as_str ( ) ] ) ;
assert_eq! ( codes ( & doc , & open [ " order " ] [ " added " ] ) , [ new_code . as_str ( ) ] ) ;
// Its vote stays: completing an order never asks the voters again.
assert_eq! ( ( open [ " tally " ] [ " for " ] . as_u64 ( ) , open [ " myVote " ] . as_str ( ) ) , ( Some ( 1 ) , Some ( " for " ) ) ) ;
// The new article leaves again (withdrawn by its author): dropped, as if never there.
let added = created [ " id " ] . as_str ( ) . expect ( " id " ) ;
chiara . ok ( " POST " , & format! ( " /api/proposals/ {added} /withdraw " ) , None ) . await ;
let doc = yvv . doc ( " essai " ) . await ;
let open = order_of ( & doc , " inertie " ) [ " proposals " ]
. as_array ( )
. expect ( " p " )
. iter ( )
. find ( | x | x [ " id " ] = = p . as_str ( ) )
. cloned ( )
. expect ( " p " ) ;
assert_eq! ( codes ( & doc , & open [ " order " ] [ " ids " ] ) , [ " N1.1 " , " N1 " ] ) ;
assert! ( open [ " order " ] [ " added " ] . as_array ( ) . expect ( " a " ) . is_empty ( ) ) ;
// An order proposed with three articles, one of which then leaves: dropped from it.
let created = chiara
. ok (
" POST " ,
& format! ( " /api/docs/ {doc_id} /proposals " ) ,
Some ( json! ( { " section " : " inertie " , " title " : " Autre " , " body " : " Un article de passage dans la section. " , " submit " : true } ) ) ,
)
. await ;
let other = created [ " unitId " ] . as_str ( ) . expect ( " unit " ) . to_owned ( ) ;
let three = yvv . ok ( " POST " , & format! ( " /api/units/ {module} /order " ) , Some ( json! ( { " order " : [ other , n11 , n1 ] } ) ) ) . await
[ " id " ]
. as_str ( )
. expect ( " id " )
. to_owned ( ) ;
chiara . ok ( " POST " , & format! ( " /api/proposals/ {} /withdraw " , created [ " id " ] . as_str ( ) . expect ( " id " ) ) , None ) . await ;
let doc = yvv . doc ( " essai " ) . await ;
let o = order_of ( & doc , " inertie " ) [ " proposals " ]
. as_array ( )
. expect ( " p " )
. iter ( )
. find ( | x | x [ " id " ] = = three . as_str ( ) )
. cloned ( )
. expect ( " o " ) ;
assert_eq! ( codes ( & doc , & o [ " order " ] [ " ids " ] ) , [ " N1.1 " , " N1 " ] ) ;
assert_eq! ( o [ " order " ] [ " dropped " ] , json! ( [ other ] ) ) ;
// Completed, it reads like the first one: the second voice brings that order in.
let v = chiara . vote ( & p , " for " ) . await ;
assert_eq! ( v [ " entered " ] [ 0 ] [ " kind " ] , " order " ) ;
let doc = yvv . doc ( " essai " ) . await ;
assert_eq! ( read_order ( & doc , " inertie " ) , [ " N1.1 " , " N1 " ] ) ;
// Now the order in force: the other one changes nothing more and cannot enter.
let o = order_of ( & doc , " inertie " ) [ " proposals " ]
. as_array ( )
. expect ( " p " )
. iter ( )
. find ( | x | x [ " id " ] = = three . as_str ( ) )
. cloned ( )
. expect ( " o " ) ;
assert_eq! ( o [ " order " ] [ " same " ] , true ) ;
yvv . vote ( & three , " for " ) . await ;
assert! ( chiara . vote ( & three , " for " ) . await [ " entered " ] . as_array ( ) . expect ( " e " ) . is_empty ( ) ) ;
}
#[ tokio::test ]
async fn the_generic_writing_stops_at_a_module_and_an_order_is_never_corrected ( ) {
let ( mut yvv , _ , _ ) = two ( ) . await ;
let doc = yvv . doc ( " essai " ) . await ;
let doc_id = doc [ " doc " ] [ " id " ] . as_str ( ) . expect ( " id " ) . to_owned ( ) ;
let conclusion = order_of ( & doc , " conclusion " ) ;
let module = conclusion [ " id " ] . as_str ( ) . expect ( " id " ) ;
let seed_order = conclusion [ " inForce " ] [ " id " ] . as_str ( ) . expect ( " id " ) ;
let ( s , v ) = yvv
. call (
" POST " ,
& format! ( " /api/docs/ {doc_id} /proposals " ) ,
Some ( json! ( { " unitId " : module , " title " : " Texte " , " body " : " Un texte sur un module. " , " submit " : true } ) ) ,
)
. await ;
assert_eq! ( ( s , v [ " error " ] . as_str ( ) ) , ( StatusCode ::BAD_REQUEST , Some ( " module " ) ) ) ;
let ( s , v ) = yvv
. call (
" PATCH " ,
& format! ( " /api/proposals/ {seed_order} " ) ,
Some ( json! ( { " title " : " ordre.conclusion " , " body " : " x \n y " } ) ) ,
)
. await ;
assert_eq! ( ( s , v [ " error " ] . as_str ( ) ) , ( StatusCode ::CONFLICT , Some ( " order_fixed " ) ) ) ;
let ( s , v ) = yvv
. call (
" PUT " ,
& format! ( " /api/proposals/ {seed_order} /translations/en " ) ,
Some ( json! ( { " title " : " order " , " body " : " " } ) ) ,
)
. await ;
assert_eq! ( ( s , v [ " error " ] . as_str ( ) ) , ( StatusCode ::CONFLICT , Some ( " order_fixed " ) ) ) ;
// A title and an order each have their own door, and only that one.
let title = doc [ " modules " ] [ " title " ] [ " id " ] . as_str ( ) . expect ( " title " ) ;
let ( s , v ) = yvv . call ( " POST " , & format! ( " /api/units/ {title} /order " ) , Some ( json! ( { " order " : [ ] } ) ) ) . await ;
assert_eq! ( ( s , v [ " error " ] . as_str ( ) ) , ( StatusCode ::BAD_REQUEST , Some ( " not_an_order " ) ) ) ;
let ( s , v ) = yvv . call ( " POST " , & format! ( " /api/units/ {module} /title " ) , Some ( json! ( { " title " : " Autre " } ) ) ) . await ;
assert_eq! ( ( s , v [ " error " ] . as_str ( ) ) , ( StatusCode ::BAD_REQUEST , Some ( " not_a_title " ) ) ) ;
// A module's inertia is voted like an article's.
let i = yvv . ok ( " POST " , & format! ( " /api/units/ {module} /inertia " ) , Some ( json! ( { " target " : 3 } ) ) ) . await ;
assert_eq! ( i [ " existing " ] , false ) ;
}
#[ tokio::test ]
async fn another_title_enters_into_force_and_its_translations_are_to_review ( ) {
let ( mut yvv , mut chiara , state ) = two ( ) . await ;
let doc = yvv . doc ( " essai " ) . await ;
let module = doc [ " modules " ] [ " title " ] [ " id " ] . as_str ( ) . expect ( " id " ) . to_owned ( ) ;
let uri = format! ( " /api/units/ {module} /title " ) ;
for ( wrong , code ) in [
( " " , " title " ) ,
( " Deux \n lignes " , " title " ) ,
( " Acte d'engagement Certification " , " same_title " ) ,
( " Acte \u{200b} d'engagement " , " hidden_characters " ) ,
] {
let ( s , v ) = yvv . call ( " POST " , & uri , Some ( json! ( { " title " : wrong } ) ) ) . await ;
assert_eq! ( ( s , v [ " error " ] . as_str ( ) ) , ( StatusCode ::BAD_REQUEST , Some ( code ) ) , " {wrong:?} " ) ;
}
let new_title = " Acte d'engagement de la certification " ;
let p = yvv . ok ( " POST " , & uri , Some ( json! ( { " title " : new_title } ) ) ) . await [ " id " ] . as_str ( ) . expect ( " id " ) . to_owned ( ) ;
let doc = yvv . doc ( " essai " ) . await ;
let proposal = & doc [ " modules " ] [ " title " ] [ " proposals " ] [ 0 ] ;
assert_eq! ( ( proposal [ " kind " ] . as_str ( ) , proposal [ " votedAtInertia " ] . as_u64 ( ) ) , ( Some ( " title " ) , Some ( 4 ) ) ) ;
yvv . vote ( & p , " for " ) . await ;
let v = chiara . vote ( & p , " for " ) . await ;
assert_eq! ( ( v [ " entered " ] [ 0 ] [ " code " ] . as_str ( ) , v [ " entered " ] [ 0 ] [ " kind " ] . as_str ( ) ) , ( Some ( " titre " ) , Some ( " title " ) ) ) ;
// The document changes title in French; English and Spanish stay, « à revoir ».
let doc = yvv . doc ( " essai " ) . await ;
assert_eq! ( doc [ " doc " ] [ " title " ] [ " fr " ] , new_title ) ;
assert_eq! ( doc [ " doc " ] [ " title " ] [ " en " ] , " Certification Commitment Act " ) ;
let tr = & doc [ " modules " ] [ " title " ] [ " inForce " ] [ " translations " ] ;
assert_eq! ( ( tr [ " en " ] [ " stale " ] . as_bool ( ) , tr [ " es " ] [ " stale " ] . as_bool ( ) ) , ( Some ( true ) , Some ( true ) ) ) ;
let g = yvv . ok ( " GET " , " /api/groups/essai " , None ) . await ;
assert_eq! ( g [ " docs " ] [ 0 ] [ " title " ] [ " fr " ] , new_title ) ;
let d = yvv . ok ( " GET " , " /api/groups/essai/decisions " , None ) . await ;
assert_eq! ( d [ " docs " ] [ 0 ] [ " title " ] [ " fr " ] , new_title ) ;
// A member translates it again: the document's English title follows, up to date.
yvv . ok (
" PUT " ,
& format! ( " /api/proposals/ {p} /translations/en " ) ,
Some ( json! ( { " title " : " Commitment Act of Certification " , " body " : " " } ) ) ,
)
. await ;
let doc = yvv . doc ( " essai " ) . await ;
assert_eq! ( doc [ " doc " ] [ " title " ] [ " en " ] , " Commitment Act of Certification " ) ;
assert_eq! ( doc [ " modules " ] [ " title " ] [ " inForce " ] [ " translations " ] [ " en " ] [ " stale " ] , false ) ;
// A typo of the title in force is corrected without a vote, and the document follows.
let fixed = " Acte d’ engagement de la certification " ;
let r = chiara . ok ( " PATCH " , & format! ( " /api/proposals/ {p} " ) , Some ( json! ( { " title " : fixed , " body " : " " } ) ) ) . await ;
assert_eq! ( r [ " edit " ] , " typo " ) ;
assert_eq! ( yvv . doc ( " essai " ) . await [ " doc " ] [ " title " ] [ " fr " ] , fixed ) ;
// The test group never publishes; the title of the publication is covered by `render`.
let n : i64 = sqlx ::query_scalar (
" SELECT COUNT(*) FROM outbox o JOIN documents d ON d.id = o.doc_id WHERE d.group_slug = 'essai' " ,
)
. fetch_one ( & state . db )
. await
. expect ( " outbox " ) ;
assert_eq! ( n , 0 ) ;
}
#[ tokio::test ]
async fn a_new_article_goes_at_the_end_of_the_section_asked_for ( ) {
let ( mut yvv , _ , _ ) = two ( ) . await ;
let doc = yvv . doc ( " essai " ) . await ;
let doc_id = doc [ " doc " ] [ " id " ] . as_str ( ) . expect ( " id " ) . to_owned ( ) ;
let new = | section : Option < & str > , after : Option < String > | json! ( { " section " : section , " afterUnitId " : after , " title " : " Nouveau " , " body " : " Un nouvel article. " , " submit " : true } ) ;
let a = yvv . ok ( " POST " , & format! ( " /api/docs/ {doc_id} /proposals " ) , Some ( new ( Some ( " fondamental " ) , None ) ) ) . await ;
// An older client names an article: the end of its section, not right after it.
let b = yvv . ok ( " POST " , & format! ( " /api/docs/ {doc_id} /proposals " ) , Some ( new ( None , Some ( id_of ( & doc , " I1 " ) ) ) ) ) . await ;
let ( s , v ) = yvv . call ( " POST " , & format! ( " /api/docs/ {doc_id} /proposals " ) , Some ( new ( Some ( " nulle-part " ) , None ) ) ) . await ;
assert_eq! ( ( s , v [ " error " ] . as_str ( ) ) , ( StatusCode ::BAD_REQUEST , Some ( " section " ) ) ) ;
let doc = yvv . doc ( " essai " ) . await ;
let code = | created : & Value | {
doc [ " units " ] . as_array ( ) . expect ( " u " ) . iter ( ) . find ( | u | u [ " id " ] = = created [ " unitId " ] ) . expect ( " unit " ) [ " code " ]
. as_str ( )
. expect ( " code " )
. to_owned ( )
} ;
let ( ca , cb ) = ( code ( & a ) , code ( & b ) ) ;
assert_eq! ( read_order ( & doc , " fondamental " ) , [ " T1 " , " E1 " , " E2 " , ca . as_str ( ) ] ) ;
assert_eq! ( read_order ( & doc , " introduction " ) , [ " I1 " , " I2 " , cb . as_str ( ) ] ) ;
// It joins the order of its section, at its end.
assert_eq! ( codes ( & doc , & order_of ( & doc , " fondamental " ) [ " members " ] ) , [ " T1 " , " E1 " , " E2 " , ca . as_str ( ) ] ) ;
}
// ---------------------------------------------------------------------------
// A base born without modules, as production holds it, then the new version
// ---------------------------------------------------------------------------
fn seed_without_modules ( tag : & str ) -> std ::path ::PathBuf {
let today = Config ::for_tests ( ) . seed_dir . join ( " acte-engagement-certification.json " ) ;
let mut seed : Value = serde_json ::from_str ( & std ::fs ::read_to_string ( today ) . expect ( " seed " ) ) . expect ( " json " ) ;
seed . as_object_mut ( ) . expect ( " object " ) . remove ( " modules " ) ;
let dir = std ::env ::temp_dir ( ) . join ( format! ( " ld-modules-seed- {tag} - {} " , std ::process ::id ( ) ) ) ;
let _ = std ::fs ::remove_dir_all ( & dir ) ;
std ::fs ::create_dir_all ( & dir ) . expect ( " dir " ) ;
std ::fs ::write ( dir . join ( " acte-engagement-certification.json " ) , seed . to_string ( ) ) . expect ( " write " ) ;
dir
}
/// What must not move: every unit of the base (place, text in force, section,
/// inertia), every version (status, text, revision) and every vote.
async fn snapshot (
state : & AppState ,
) -> ( Vec < ( String , f64 , Option < String > , Option < String > , String ) > , Vec < ( String , String , String , i64 ) > , i64 ) {
let units = sqlx ::query_as (
" SELECT id, position, in_force_id, section, inertia FROM units WHERE kind = 'article' ORDER BY id " ,
)
. fetch_all ( & state . db )
. await
. expect ( " units " ) ;
let proposals = sqlx ::query_as (
" SELECT p.id, p.status, p.body, p.revision FROM proposals p JOIN units u ON u.id = p.unit_id
WHERE u . kind = ' article ' ORDER BY p . id " ,
)
. fetch_all ( & state . db )
. await
. expect ( " proposals " ) ;
let votes = sqlx ::query_scalar ( " SELECT COUNT(*) FROM votes " ) . fetch_one ( & state . db ) . await . expect ( " votes " ) ;
( units , proposals , votes )
}
#[ tokio::test ]
async fn an_existing_document_receives_its_modules_once_from_what_it_holds ( ) {
// Production before this version: the documents, no module.
let mut cfg = Config ::for_tests ( ) ;
cfg . seed_dir = seed_without_modules ( " applied " ) ;
let db = open_db ( " sqlite::memory: " ) . await . expect ( " db " ) ;
let before = AppState ::new ( cfg , db ) . await . expect ( " state " ) ;
seed ::documents ( & before ) . await . expect ( " documents " ) ;
let mut c = Client ::of ( & before ) ;
c . login ( " Yvv " ) . await ;
let mut chiara = c . other ( ) ;
chiara . login ( " Chiara " ) . await ;
let doc = c . doc ( " essai " ) . await ;
assert! ( doc [ " modules " ] [ " title " ] . is_null ( ) ) ;
// Life goes on there: votes, an article adopted, another one proposed in « conclusion ».
let e3 = unit ( & doc , " E3 " ) [ " proposals " ] [ 0 ] [ " id " ] . as_str ( ) . expect ( " e3 " ) . to_owned ( ) ;
c . vote ( & e3 , " for " ) . await ;
chiara . vote ( & e3 , " for " ) . await ;
let doc_id = doc [ " doc " ] [ " id " ] . as_str ( ) . expect ( " id " ) . to_owned ( ) ;
c . ok (
" POST " ,
& format! ( " /api/docs/ {doc_id} /proposals " ) ,
Some ( json! ( { " section " : " conclusion " , " title " : " Rappel " , " body " : " Un rappel final, proposé avant les modules. " , " submit " : true } ) ) ,
)
. await ;
let kept = snapshot ( & before ) . await ;
let reads = read_order ( & c . doc ( " essai " ) . await , " conclusion " ) ;
// The new version starts: each document receives its modules, from what it holds.
let state = AppState ::new ( Config ::for_tests ( ) , before . db . clone ( ) ) . await . expect ( " state " ) ;
seed ::ensure ( & state ) . await . expect ( " seed " ) ;
let rows : Vec < ( String , String , Option < String > , Option < String > ) > = sqlx ::query_as (
" SELECT d.group_slug, m.outcome, m.reason, m.created FROM module_amendments m JOIN documents d ON d.id = m.doc_id
ORDER BY d . group_slug " ,
)
. fetch_all ( & state . db )
. await
. expect ( " decisions " ) ;
let created = r # "["titre","ordre.introduction","ordre.fondamental","ordre.technique","ordre.conclusion","ordre.annexe","ordre.formule","ordre.inertie","ordre.ordonnancement"]"# ;
assert_eq! (
rows ,
[
( " essai " . into ( ) , " applied " . into ( ) , None , Some ( created . into ( ) ) ) ,
( " monnaie-libre " . into ( ) , " applied " . into ( ) , None , Some ( created . into ( ) ) ) ,
]
) ;
// Nothing else moved: articles, versions, votes.
assert_eq! ( snapshot ( & state ) . await , kept ) ;
let mut c = Client ::of ( & state ) ;
c . login ( " Yvv " ) . await ;
let doc = c . doc ( " essai " ) . await ;
assert_eq! ( doc [ " modules " ] [ " title " ] [ " inForce " ] [ " title " ] , " Acte d'engagement Certification " ) ;
assert_eq! (
doc [ " modules " ] [ " title " ] [ " inForce " ] [ " translations " ] [ " es " ] [ " title " ] ,
" Acta de compromiso de Certificación "
) ;
// The order of each section in force is the one it read in, the new article included.
let conclusion = order_of ( & doc , " conclusion " ) ;
assert_eq! ( codes ( & doc , & conclusion [ " inForce " ] [ " order " ] [ " ids " ] ) , reads ) ;
assert_eq! ( reads . len ( ) , 3 ) ;
assert_eq! ( conclusion [ " inForce " ] [ " seedMinimal " ] , true ) ;
// « Décisions » counts them: 41 + the new article in the test group, E3 adopted there.
let d = c . ok ( " GET " , " /api/groups/essai/decisions " , None ) . await ;
assert_eq! ( d [ " docs " ] [ 0 ] [ " toVote " ] , 41 ) ;
// Republished where the document publishes, never from the test group.
let queued : Vec < ( String , String ) > = sqlx ::query_as (
" SELECT d.group_slug, o.reason FROM outbox o JOIN documents d ON d.id = o.doc_id WHERE o.reason LIKE 'modules%' " ,
)
. fetch_all ( & state . db )
. await
. expect ( " outbox " ) ;
assert_eq! (
queued ,
[ (
" monnaie-libre " . to_owned ( ) ,
" modules ajoutés : le titre du document, l'ordre des articles de chaque section " . to_owned ( )
) ]
) ;
// What is published: the same text, and the modules in document.json.
let ml = c . doc ( " monnaie-libre " ) . await ;
let st = ld_server ::docs ::load_state ( & state , ml [ " doc " ] [ " id " ] . as_str ( ) . expect ( " id " ) ) . await . expect ( " state " ) ;
let files = ld_server ::publish ::render ( & state , & st ) . await . expect ( " render " ) ;
let file = | name : & str | files . iter ( ) . find ( | ( n , _ ) | n = = name ) . map ( | ( _ , c ) | c . clone ( ) ) . expect ( " file " ) ;
assert! ( ! file ( " fr.md " ) . contains ( " ordre. " ) & & file ( " fr.md " ) . contains ( " # Acte d'engagement Certification " ) ) ;
let json : Value = serde_json ::from_str ( & file ( " document.json " ) ) . expect ( " json " ) ;
assert_eq! ( json [ " units " ] . as_array ( ) . expect ( " units " ) . len ( ) , 3 , " the articles in force, no module " ) ;
assert_eq! (
( json [ " modules " ] [ " title " ] [ " inertia " ] . as_u64 ( ) , json [ " modules " ] [ " title " ] [ " status " ] . as_str ( ) ) ,
( Some ( 4 ) , Some ( " seed-minimal " ) )
) ;
assert_eq! ( json [ " modules " ] [ " orders " ] . as_array ( ) . expect ( " orders " ) . len ( ) , 8 ) ;
assert_eq! ( json [ " modules " ] [ " orders " ] [ 1 ] [ " articles " ] , json! ( [ " T1 " , " E1 " , " E2 " ] ) ) ;
// Once only: the next start adds nothing.
let again = AppState ::new ( Config ::for_tests ( ) , state . db . clone ( ) ) . await . expect ( " state " ) ;
seed ::ensure ( & again ) . await . expect ( " seed " ) ;
let n : i64 =
sqlx ::query_scalar ( " SELECT COUNT(*) FROM units WHERE kind <> 'article' " ) . fetch_one ( & again . db ) . await . expect ( " n " ) ;
assert_eq! ( n , 18 , " 9 modules in each of the two documents " ) ;
let decided : i64 =
sqlx ::query_scalar ( " SELECT COUNT(*) FROM module_amendments " ) . fetch_one ( & again . db ) . await . expect ( " n " ) ;
assert_eq! ( decided , 2 ) ;
}