2026-09-24 14:35:05 +03:00
#[path = "support/fake_jsm.rs" ]
mod fake_jsm ;
mod support ;
use chrono ::{ Duration , Utc };
use cot ::db ::query ;
use meguri ::{
domain ::* ,
engine ,
error ::Error ,
models ::{ AuditRow , HistoryRow , SyncJobRow },
providers , service , worker ,
};
use serde_json ::json ;
use support ::Fixture ;
use uuid ::Uuid ;
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn generation_is_workspace_scoped_atomic_and_revision_checked () {
use meguri ::{
demo ,
generation_service ::{ self , Draft },
};
let mut f = Fixture ::new (). await ;
let actor = f . people [ 0 ]. id ;
let id = f . team . id ;
let ( variables , rules ) = demo ::project_matrix ( Utc ::now (). date_naive (), actor , f . people [ 1 ]. id );
let mut draft = Draft {
2026-10-07 15:30:31 +01:00
shared_revision : None ,
2026-09-24 14:35:05 +03:00
revision : f . team . revision ,
variables ,
rules ,
route_position : 0 ,
review_id : None ,
};
let preview = generation_service ::preview ( & f . db , actor , id , draft . clone ())
. await
. unwrap ();
assert_eq! ( preview [ "issues" ], json! ([]));
assert_eq! ( f . db . workspace ( id ). await . unwrap (). revision , draft . revision );
for kind in [ "rotation" , "priority" , "escalation" , "route" ] {
assert! (
preview [ "result" ][ "managed" ]
. as_array ()
. unwrap ()
. iter ()
. any ( | m | m [ "kind" ] == kind )
);
}
assert! (
generation_service ::apply ( & f . db , actor , id , draft . clone ())
. await
. is_err ()
);
draft . review_id = preview [ "review_id" ]. as_str (). map ( str ::to_owned );
2026-10-07 13:28:43 +01:00
let other = f
. extra_workspace ( "Independent workspace" , & f . people [ 0 ])
. await ;
2026-09-24 14:35:05 +03:00
assert! (
generation_service ::preview ( & f . db , f . people [ 1 ]. id , other . id , draft . clone ())
. await
. is_err ()
);
let ( a , b ) = tokio ::join! (
generation_service ::apply ( & f . db , actor , id , draft . clone ()),
generation_service ::apply ( & f . db , actor , id , draft . clone ())
);
assert_ne! ( a . is_ok (), b . is_ok ());
f . team = f . db . workspace ( id ). await . unwrap ();
assert_eq! ( f . team . revision , draft . revision + 1 );
assert_eq! ( f . team . document . generation . variables . len (), 3 );
assert_eq! (
f . db . workspace ( other . id ). await . unwrap (). revision ,
other . revision
);
assert! (
f . db . workspace ( other . id )
. await
. unwrap ()
. document
. generation
. variables
. is_empty ()
);
assert_eq! (
f . db . sync_status ( id ). await . unwrap ()[ "requested_revision" ],
f . team . revision
);
assert! (
f . db . audit ( id )
. await
. unwrap ()
. iter ()
. any ( | e | e [ "action" ] == "generation.applied" )
);
let mut stale = draft ;
stale . revision = f . team . revision ;
stale . rules [ 0 ]. name = "Changed after review" . into ();
assert! (
generation_service ::apply ( & f . db , actor , id , stale )
. await
. is_err ()
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn absence_preview_is_read_only_and_bulk_ranges_need_the_right_confirmations () {
use meguri ::absence ::{ CoverRange , Draft };
let mut f = Fixture ::new (). await ;
let owner = f . people [ 0 ]. id ;
let absent = f . people [ 1 ]. id ;
let second = f . people [ 2 ]. id ;
let team = f . team . id ;
let mut doc = f . team . document . clone ();
if let ScheduleKind ::Rotation { participants , .. } = & mut doc . schedules [ 0 ]. kind {
* participants = vec! [ absent ];
}
doc . schedules . push ( Schedule {
id : Uuid ::new_v4 (),
name : "Backup" . into (),
handover : default_handover (),
kind : ScheduleKind ::Rotation {
participants : vec ! [ absent ],
anchor : "2026-09-21" . parse (). unwrap (),
shift_days : 7 ,
},
});
doc . schedules . push ( Schedule {
id : Uuid ::new_v4 (),
name : "Project" . into (),
handover : default_handover (),
kind : ScheduleKind ::Priority {
participants : vec ! [ absent , second ],
fallback : f . schedule ,
},
});
f . team = service ::update_document ( & f . db , owner , team , f . team . revision , doc )
. await
. unwrap ();
let start = engine ::schedule_duty_day ( & f . team . document , f . schedule , f . start ). unwrap ();
let draft = Draft {
absence : Absence {
id : Uuid ::new_v4 (),
user_id : absent ,
start ,
end : start + Duration ::days ( 13 ),
},
ranges : vec ! [
CoverRange {
local : None ,
exact : None ,
start ,
end : start + Duration ::days ( 6 ),
replacement : owner ,
replace_projects : false ,
},
CoverRange {
local : None ,
exact : None ,
start : start + Duration ::days ( 7 ),
end : start + Duration ::days ( 13 ),
replacement : second ,
replace_projects : false ,
},
],
};
let revision = f . team . revision ;
let input = | force | service ::NewAbsence {
revision ,
draft : draft . clone (),
people : vec ! [],
force ,
reason : "Vacation" . into (),
};
let preview = service ::preview_absence ( & f . db , absent , team , input ( false ))
. await
. unwrap ();
assert_eq! ( preview [ "plan" ][ "schedules" ], 3 );
assert_eq! ( preview [ "plan" ][ "uncovered" ], 0 );
assert_eq! ( f . db . workspace ( team ). await . unwrap (). revision , revision );
assert! (
service ::create_absence ( & f . db , absent , team , input ( true ))
. await
. is_err ()
);
assert! (
service ::preview_absence ( & f . db , absent , Uuid ::new_v4 (), input ( false ))
. await
. is_err ()
);
f . team = service ::create_absence ( & f . db , absent , team , input ( false ))
. await
. unwrap ();
assert_eq! ( f . team . revision , revision + 1 );
assert_eq! ( f . team . document . requests . len (), 2 );
assert! (
f . team
. document
. requests
. iter ()
. all ( | r | r . status == RequestStatus ::Pending && r . legs . len () == 2 )
);
assert_eq! (
f . db . audit ( team ). await . unwrap ()[ 0 ][ "action" ],
"absence.created"
);
assert_eq! (
f . db . sync_status ( team ). await . unwrap ()[ "requested_revision" ],
f . team . revision
);
assert! ( matches! (
service ::create_absence ( & f . db , absent , team , input ( false )). await ,
Err ( Error ::Conflict ( _ ))
));
let end = engine ::schedule_handover ( & f . team . document , f . schedule , start + Duration ::days ( 14 ))
. unwrap ();
let timeline = engine ::timeline ( & f . team . document , f . start , end ). unwrap ();
assert! (
providers ::compile ( & f . team , timeline , Utc ::now ()). is_err (),
"Pending coverage cannot publish an absent backup or rotation"
);
let first = f . team . document . requests [ 0 ]. id ;
assert! (
service ::decide_request ( & f . db , second , team , first , f . decision ( "approve" ))
. await
. is_err ()
);
f . team = service ::decide_request ( & f . db , owner , team , first , f . decision ( "approve" ))
. await
. unwrap ();
let next = f . team . document . requests [ 1 ]. id ;
f . team = service ::decide_request ( & f . db , second , team , next , f . decision ( "approve" ))
. await
. unwrap ();
let timeline = engine ::timeline ( & f . team . document , f . start , end ). unwrap ();
assert! (
timeline
. assignments
. iter ()
. all ( | a | ! a . unavailable && a . user_id != absent )
);
assert! ( providers ::compile ( & f . team , timeline , Utc ::now ()). is_ok ());
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn schedule_removal_rewrites_dependencies_and_cancels_pending_requests_atomically () {
let mut f = Fixture ::new (). await ;
let actor = f . people [ 0 ]. id ;
let team = f . team . id ;
let secondary = Uuid ::new_v4 ();
let mut doc = f . team . document . clone ();
doc . schedules . push ( Schedule {
id : secondary ,
name : "Payments secondary" . into (),
handover : default_handover (),
kind : ScheduleKind ::Rotation {
participants : vec ! [ actor ],
anchor : "2026-09-21" . parse (). unwrap (),
shift_days : 7 ,
},
});
doc . escalations . push ( Escalation {
id : Uuid ::new_v4 (),
name : "Payments escalation" . into (),
steps : [ f . schedule , secondary ]
. into_iter ()
. enumerate ()
. map ( | ( i , id ) | EscalationStep {
delay_minutes : ( i * 10 ) as u16 ,
condition : EscalationCondition ::IfNotAcked ,
recipient : Recipient ::Schedule { id },
})
. collect (),
repeat : Default ::default (),
});
f . team = service ::update_document ( & f . db , actor , team , f . team . revision , doc )
. await
. unwrap ();
let mut leg = f . leg ();
leg . schedule_id = secondary ;
f . propose ( vec! [ leg ], false ). await ;
let revision = f . team . revision ;
let input = || service ::RemoveSchedule {
revision ,
redirect_to : None ,
};
assert! ( matches! (
service ::remove_schedule ( & f . db , f . people [ 1 ]. id , team , secondary , input ()). await ,
Err ( Error ::Forbidden )
));
assert! (
service ::remove_schedule ( & f . db , actor , Uuid ::new_v4 (), secondary , input ())
. await
. is_err ()
);
assert! ( matches! (
service ::remove_schedule ( & f . db , actor , team , Uuid ::new_v4 (), input ()). await ,
Err ( Error ::NotFound )
));
let before = f . db . audit ( team ). await . unwrap (). len ();
f . team = service ::remove_schedule ( & f . db , actor , team , secondary , input ())
. await
. unwrap ();
assert_eq! ( f . team . revision , revision + 1 );
assert! ( f . team . document . schedule ( secondary ). is_none ());
assert_eq! ( f . team . document . escalations [ 0 ]. steps . len (), 1 );
assert! (
matches! ( f . team . document . escalations [ 0 ]. steps [ 0 ]. recipient , Recipient ::Schedule { id } if id == f . schedule )
);
assert_eq! ( f . team . document . requests [ 0 ]. status , RequestStatus ::Cancelled );
let events = f . db . audit ( team ). await . unwrap ();
assert_eq! ( events . len (), before + 1 );
assert_eq! ( events [ 0 ][ "action" ], "schedule.removed" );
assert_eq! (
events [ 0 ][ "details" ][ "impact" ][ "pending_requests" ][ 0 ],
f . team . document . requests [ 0 ]. id . to_string ()
);
assert_eq! (
f . db . sync_status ( team ). await . unwrap ()[ "requested_revision" ],
f . team . revision
);
let key = team . to_string ();
let history = query! ( HistoryRow , $workspace_id == key )
. all ( & f . db . db )
. await
. unwrap ();
let old : WorkspaceDocument = serde_json ::from_str (
& history
. iter ()
. find ( | h | h . revision == revision )
. unwrap ()
. document ,
)
. unwrap ();
assert! ( old . schedule ( secondary ). is_some ());
assert_eq! ( old . requests [ 0 ]. status , RequestStatus ::Pending );
assert! ( matches! (
service ::remove_schedule ( & f . db , actor , team , f . schedule , input ()). await ,
Err ( Error ::Conflict ( _ ))
));
let unchanged = f . team . revision ;
assert! (
service ::remove_schedule (
& f . db ,
actor ,
team ,
f . schedule ,
service ::RemoveSchedule {
revision : unchanged ,
redirect_to : None
}
)
. await
. is_err (),
"The last escalation step must not disappear"
);
assert_eq! ( f . db . workspace ( team ). await . unwrap (). revision , unchanged );
f . propose ( vec! [ f . leg ()], true ). await ;
let protected = f . team . revision ;
assert! (
service ::remove_schedule (
& f . db ,
actor ,
team ,
f . schedule ,
service ::RemoveSchedule {
revision : protected ,
redirect_to : None
}
)
. await
. is_err (),
"Approved future replacements protect the schedule"
);
assert_eq! ( f . db . workspace ( team ). await . unwrap (). revision , protected );
assert_eq! (
f . db . sync_status ( team ). await . unwrap ()[ "requested_revision" ],
protected
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn routing_order_is_scoped_revision_checked_and_published_as_one_change () {
let mut f = Fixture ::new (). await ;
let actor = f . people [ 0 ]. id ;
let team = f . team . id ;
let ids = [ Uuid ::new_v4 (), Uuid ::new_v4 (), Uuid ::new_v4 ()];
let mut doc = f . team . document . clone ();
doc . routes = ids
. iter ()
. zip ([ 0 , 10 , 9999 ])
. map ( | ( id , order ) | RoutingRule {
id : * id ,
name : format ! ( "Rule {order}" ),
order ,
conditions : vec ! [ RouteCondition {
field : "tags" . into (),
operation : "contains" . into (),
value : id . to_string (),
negate : false ,
}],
window : None ,
escalation_id : None ,
})
. collect ();
f . team = service ::update_document ( & f . db , actor , team , f . team . revision , doc )
. await
. unwrap ();
let revision = f . team . revision ;
let input = | ids : Vec < Uuid >| service ::ReorderRoutes { revision , ids };
assert! ( matches! (
service ::reorder_routes ( & f . db , f . people [ 1 ]. id , team , input ( ids . to_vec ())). await ,
Err ( Error ::Forbidden )
));
assert! (
service ::reorder_routes ( & f . db , actor , Uuid ::new_v4 (), input ( ids . to_vec ()))
. await
. is_err ()
);
let before = f . db . audit ( team ). await . unwrap (). len ();
for invalid in [
vec! [ ids [ 0 ]],
vec! [ ids [ 0 ], ids [ 0 ], ids [ 2 ]],
vec! [ ids [ 0 ], ids [ 1 ], Uuid ::new_v4 ()],
] {
assert! (
service ::reorder_routes ( & f . db , actor , team , input ( invalid ))
. await
. is_err ()
);
}
assert_eq! ( f . db . workspace ( team ). await . unwrap (). revision , revision );
assert_eq! ( f . db . audit ( team ). await . unwrap (). len (), before );
let wanted = vec! [ ids [ 2 ], ids [ 0 ], ids [ 1 ]];
f . team = service ::reorder_routes ( & f . db , actor , team , input ( wanted . clone ()))
. await
. unwrap ();
assert_eq! (
f . team
. document
. routes
. iter ()
. map ( | r | r . id )
. collect ::< Vec < _ >> (),
wanted
);
assert_eq! (
f . team
. document
. routes
. iter ()
. map ( | r | r . order )
. collect ::< Vec < _ >> (),
vec! [ 0 , 1 , 2 ]
);
for route in & f . team . document . routes {
assert_eq! ( route . conditions [ 0 ]. value , route . id . to_string ());
}
assert_eq! (
f . db . sync_status ( team ). await . unwrap ()[ "requested_revision" ],
revision + 1
);
let events = f . db . audit ( team ). await . unwrap ();
assert_eq! ( events . len (), before + 1 );
assert_eq! ( events [ 0 ][ "action" ], "routing.reordered" );
assert_eq! (
events [ 0 ][ "details" ],
json! ({ "before" : ids , "after" : wanted })
);
assert! ( matches! (
service ::reorder_routes ( & f . db , actor , team , input ( ids . to_vec ())). await ,
Err ( Error ::Conflict ( _ ))
));
let current = f . team . revision ;
let ( left , right ) = tokio ::join! (
service ::reorder_routes (
& f . db ,
actor ,
team ,
service ::ReorderRoutes {
revision : current ,
ids : ids . to_vec ()
}
),
service ::reorder_routes (
& f . db ,
actor ,
team ,
service ::ReorderRoutes {
revision : current ,
ids : vec ! [ ids [ 1 ], ids [ 2 ], ids [ 0 ]]
}
)
);
assert_eq! ( usize ::from ( left . is_ok ()) + usize ::from ( right . is_ok ()), 1 );
let key = team . to_string ();
let history = query! ( HistoryRow , $workspace_id == key )
. all ( & f . db . db )
. await
. unwrap ();
assert_eq! ( history . len () as i64 , current + 1 );
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn email_contacts_are_scoped_people_without_login_or_access_and_import_is_atomic () {
let mut f = Fixture ::new (). await ;
let other = Fixture ::new (). await ;
let actor = f . people [ 0 ]. id ;
let tenant = f . team . id ;
let team = f . team . id ;
assert! (
! f . db
. directory ( tenant )
. await
. unwrap ()
. iter ()
. any ( | u | u . id == other . people [ 0 ]. id )
);
let id = Uuid ::new_v4 ();
let mut doc = f . team . document . clone ();
doc . people . push ( Person {
id ,
user_id : None ,
display_name : "New colleague" . into (),
email : Some ( " New.Person@Example.com " . into ()),
country : Some ( "am" . into ()),
2026-10-07 13:28:43 +01:00
aliases : vec ! [],
2026-09-24 14:35:05 +03:00
});
if let ScheduleKind ::Rotation { participants , .. } = & mut doc . schedules [ 0 ]. kind {
participants . push ( id );
}
doc . absences . push ( Absence {
id : Uuid ::new_v4 (),
user_id : id ,
start : f . start . date_naive () + Duration ::days ( 100 ),
end : f . start . date_naive () + Duration ::days ( 101 ),
});
f . team = service ::update_document ( & f . db , actor , team , f . team . revision , doc )
. await
. unwrap ();
let contact = f . team . document . person ( id ). unwrap ();
assert_eq! ( contact . email . as_deref (), Some ( "new.person@example.com" ));
assert_eq! ( contact . country . as_deref (), Some ( "AM" ));
let key = id . to_string ();
assert! (
! query! ( meguri ::models ::UserRow , $id == key )
. exists ( & f . db . db )
. await
. unwrap ()
);
assert! ( f . db . require ( team , id , Permission ::View ). await . is_err ());
let mut forged = f . team . document . clone ();
forged . people [ 0 ]. user_id = Some ( other . people [ 0 ]. id );
assert! (
service ::update_document ( & f . db , actor , team , f . team . revision , forged )
. await
. is_err ()
);
let mut duplicate = f . team . document . clone ();
let mut person = duplicate . people [ 0 ]. clone ();
person . id = Uuid ::new_v4 ();
duplicate . people . push ( person );
assert! (
service ::update_document ( & f . db , actor , team , f . team . revision , duplicate )
. await
. is_err ()
);
let csv = "country,date,name \n CY,2027-01-01,New Year \n AM,2027-01-01,New Year" ;
let version = f . team . revision ;
let preview = meguri ::holidays ::compare ( csv , & f . team . document . holidays ). unwrap ();
assert_eq! ( preview . additions . len (), 2 );
assert_eq! (
f . db . workspace ( team ). await . unwrap (). revision ,
version ,
"Preview is pure"
);
assert! (
service ::import_holidays ( & f . db , f . people [ 1 ]. id , team , version , csv )
. await
. is_err ()
);
assert! (
service ::import_holidays (
& f . db ,
actor ,
team ,
version ,
"country,date,name \n XX,2027-01-01,Invalid \n CY,2027-01-02,Valid"
)
. await
. is_err ()
);
assert_eq! (
f . db . workspace ( team ). await . unwrap (). revision ,
version ,
"Invalid CSV writes nothing"
);
f . team = service ::import_holidays ( & f . db , actor , team , version , csv )
. await
. unwrap ();
assert_eq! ( f . team . document . holidays . len (), 2 );
assert! ( matches! (
service ::import_holidays (
& f . db ,
actor ,
team ,
version ,
"country,date,name \n CY,2027-12-25,Christmas"
)
. await ,
Err ( Error ::Conflict ( _ ))
));
f . team = service ::import_holidays (
& f . db ,
actor ,
team ,
f . team . revision ,
"country,date,name \n CY,2027-01-01,Changed name \n CY,2027-12-25,Christmas" ,
)
. await
. unwrap ();
assert_eq! ( f . team . document . holidays [ 0 ]. name , "New Year" );
assert_eq! ( f . team . document . holidays . len (), 3 );
let audit = f . db . audit ( team ). await . unwrap ();
assert! ( audit . iter (). any ( | e | e [ "action" ] == "holidays.imported" ));
let job = f . db . sync_status ( team ). await . unwrap ();
assert_eq! ( job [ "requested_revision" ], f . team . revision );
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn workspace_interval_propagates_atomically_and_user_timezone_does_not_move_shifts () {
let mut f = Fixture ::new (). await ;
let actor = f . people [ 0 ]. id ;
let tenant = f . team . id ;
let team = f . team . id ;
2026-10-07 13:28:43 +01:00
let other = f . extra_workspace ( "Another team" , & f . people [ 0 ]). await ;
2026-09-24 14:35:05 +03:00
let mut doc = f . team . document . clone ();
doc . routes . push ( RoutingRule {
id : Uuid ::new_v4 (),
name : "Working only" . into (),
order : 1 ,
conditions : vec ! [],
window : Some ( WorkingWindow ::default ()),
escalation_id : None ,
});
f . team = service ::update_document ( & f . db , actor , team , f . team . revision , doc )
. await
. unwrap ();
let revision = f . team . revision ;
let settings_revision = f . team . revision ;
let interval = WorkingWindow {
timezone : "Asia/Yerevan" . into (),
weekdays : vec ! [ 1 , 3 , 5 ],
start : "09:00:00" . parse (). unwrap (),
end : "18:00:00" . parse (). unwrap (),
};
assert! (
service ::set_working_interval (
& f . db ,
f . people [ 1 ]. id ,
tenant ,
settings_revision ,
interval . clone ()
)
. await
. is_err ()
);
service ::set_working_interval ( & f . db , actor , team , settings_revision , interval . clone ())
. await
. unwrap ();
assert! ( matches! (
service ::set_working_interval ( & f . db , actor , team , settings_revision , interval . clone ())
. await ,
Err ( Error ::Conflict ( _ ))
));
f . team = f . db . workspace ( team ). await . unwrap ();
assert_eq! ( f . team . revision , revision + 1 );
assert_eq! ( f . team . document . timezone , "Asia/Yerevan" );
assert_eq! ( f . team . document . routes [ 0 ]. window . as_ref (), Some ( & interval ));
assert_eq! (
f . db . workspace ( other . id ). await . unwrap (). revision ,
other . revision
);
assert_eq! (
f . db . sync_status ( team ). await . unwrap ()[ "requested_revision" ],
f . team . revision
);
let before = serde_json ::to_value (
engine ::timeline ( & f . team . document , f . start , f . start + Duration ::days ( 30 )). unwrap (),
)
. unwrap ();
let key = actor . to_string ();
let mut user = query! ( meguri ::models ::UserRow , $id == key . clone ())
. get ( & f . db . db )
. await
. unwrap ()
. unwrap ();
user . timezone = Some ( "America/New_York" . into ());
user . language = Some ( "fr" . into ());
service ::save_profile ( & f . db , & user ). await . unwrap ();
let unchanged = f . db . workspace ( team ). await . unwrap ();
assert_eq! (
query! ( meguri ::models ::UserRow , $id == key )
. get ( & f . db . db )
. await
. unwrap ()
. unwrap ()
. language
. as_deref (),
Some ( "fr" )
);
assert_eq! ( unchanged . revision , f . team . revision );
assert_eq! (
serde_json ::to_value (
engine ::timeline ( & unchanged . document , f . start , f . start + Duration ::days ( 30 )). unwrap ()
)
. unwrap (),
before
);
user . country = "AM" . into ();
service ::save_profile ( & f . db , & user ). await . unwrap ();
let changed = f . db . workspace ( team ). await . unwrap ();
assert_eq! ( changed . revision , f . team . revision + 1 );
assert_eq! (
changed . document . person ( actor ). unwrap (). country . as_deref (),
Some ( "AM" )
);
assert_eq! ( changed . document . timezone , "Asia/Yerevan" );
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn workspace_settings_and_configuration_share_one_revision () {
let f = Fixture ::new (). await ;
let actor = f . people [ 0 ]. id ;
let id = f . team . id ;
let a = WorkingWindow {
timezone : "Asia/Yerevan" . into (),
.. WorkingWindow ::default ()
};
let b = WorkingWindow {
timezone : "Asia/Tokyo" . into (),
.. WorkingWindow ::default ()
};
let ( first , second ) = tokio ::join! (
service ::set_working_interval ( & f . db , actor , id , f . team . revision , a ),
service ::set_working_interval ( & f . db , actor , id , f . team . revision , b )
);
assert_ne! ( first . is_ok (), second . is_ok ());
let current = f . db . workspace ( id ). await . unwrap ();
assert_eq! ( current . revision , f . team . revision + 1 );
assert_eq! (
current . document . timezone ,
current . document . working_interval . timezone
);
assert! (
service ::update_document ( & f . db , actor , id , f . team . revision , f . team . document . clone ())
. await
. is_err ()
);
assert! (
service ::set_working_interval (
& f . db ,
f . people [ 1 ]. id ,
id ,
current . revision ,
WorkingWindow ::default ()
)
. await
. is_err ()
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn jsm_resolves_exact_active_email_before_writes_and_explicit_ids_override_lookup () {
let mut f = Fixture ::new (). await ;
let server = fake_jsm ::Server ::start (). await ;
let contact = Uuid ::new_v4 ();
f . team . document . people . push ( Person {
id : contact ,
user_id : None ,
display_name : "Email contact" . into (),
email : Some ( "new@example.com" . into ()),
country : Some ( "CY" . into ()),
2026-10-07 13:28:43 +01:00
aliases : vec ! [],
2026-09-24 14:35:05 +03:00
});
f . team . document . schedules [ 0 ]. kind = ScheduleKind ::Rotation {
participants : vec ! [ contact ],
anchor : f . start . date_naive (),
shift_days : 7 ,
};
f . team . document . integration = Integration ::Jsm {
cloud_id : Uuid ::new_v4 (). to_string (),
team_id : Uuid ::new_v4 (). to_string (),
advanced_escalations : false ,
users : Default ::default (),
};
let plan = providers ::compile (
& f . team ,
engine ::timeline ( & f . team . document , f . start , f . start + Duration ::days ( 7 )). unwrap (),
f . start ,
)
. unwrap ();
let client = providers ::jsm ::Jsm ::new (
server . base . clone (),
"admin@example.com" . into (),
"fake" . into (),
Uuid ::new_v4 (). to_string (),
Default ::default (),
)
. unwrap ()
. with_directory_base ( server . base . clone ());
for users in [
vec! [],
vec! [ json! ({ "active" :true , "accountId" :"hidden" })],
vec! [ json! ({ "active" :false , "emailAddress" :"new@example.com" , "accountId" :"inactive" })],
vec! [ json! ({ "active" :true , "emailAddress" :"wrong@example.com" , "accountId" :"wrong" })],
vec! [
json! ({ "active" :true , "emailAddress" :"new@example.com" , "accountId" :"a" }),
json! ({ "active" :true , "emailAddress" :"new@example.com" , "accountId" :"b" }),
],
] {
server . state . lock (). unwrap (). users = users ;
assert! ( client . publish ( & f . db , & f . team , & plan , false ). await . is_err ());
assert_eq! (
server . state . lock (). unwrap (). writes ,
0 ,
"No remote writes before identity is unambiguous"
);
}
server . state . lock (). unwrap (). users =
vec! [ json! ({ "active" :true , "emailAddress" :"NEW@example.com" , "accountId" :"resolved-id" })];
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
{
let state = server . state . lock (). unwrap ();
let schedule = state . objects . values (). next (). unwrap ();
assert_eq! (
schedule [ "rotations" ][ 0 ][ "participants" ][ 0 ][ "id" ],
"resolved-id"
);
}
let lookups = server . state . lock (). unwrap (). lookups ;
let manual = providers ::jsm ::Jsm ::new (
server . base . clone (),
"admin@example.com" . into (),
"fake" . into (),
Uuid ::new_v4 (). to_string (),
[( contact , "resolved-id" . into ())]. into (),
)
. unwrap ();
manual . publish ( & f . db , & f . team , & plan , true ). await . unwrap ();
assert_eq! ( server . state . lock (). unwrap (). lookups , lookups );
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn approval_swap_tenant_isolation_and_atomic_outbox () {
let mut f = Fixture ::new (). await ;
let owner = f . people [ 0 ]. id ;
let bob = f . people [ 1 ]. id ;
let carol = f . people [ 2 ]. id ;
assert! ( f . db . workspace ( Uuid ::new_v4 ()). await . is_err ());
assert! (
f . db . require ( f . team . id , Uuid ::new_v4 (), Permission ::View )
. await
. is_err ()
);
2026-10-07 13:28:43 +01:00
let other = f . extra_workspace ( "Private workspace" , & f . people [ 0 ]). await ;
2026-09-24 14:35:05 +03:00
assert! ( f . db . require ( other . id , bob , Permission ::View ). await . is_err ());
assert! ( matches! (
service ::update_document (
& f . db ,
bob ,
f . team . id ,
f . team . revision ,
f . team . document . clone ()
)
. await ,
Err ( Error ::Forbidden )
));
assert! (
service ::set_drift_policy ( & f . db , bob , f . team . id , f . team . revision , true )
. await
. is_err ()
);
let leg = f . leg ();
let back = ReplacementLeg {
start : leg . start + Duration ::days ( 7 ),
end : leg . end + Duration ::days ( 7 ),
from_user : bob ,
to_user : owner ,
.. leg . clone ()
};
f . propose ( vec! [ leg . clone (), back . clone ()], false ). await ;
let request = f . team . document . requests . last (). unwrap (). id ;
assert_eq! (
engine ::timeline ( & f . team . document , leg . start , leg . end )
. unwrap ()
. assignments [ 0 ]
. user_id ,
owner
);
assert! (
service ::decide_request ( & f . db , owner , f . team . id , request , f . decision ( "approve" ))
. await
. is_err ()
);
assert! (
service ::decide_request ( & f . db , carol , f . team . id , request , f . decision ( "force" ))
. await
. is_err ()
);
f . team = service ::decide_request ( & f . db , bob , f . team . id , request , f . decision ( "approve" ))
. await
. unwrap ();
assert_eq! ( f . team . document . requests [ 0 ]. status , RequestStatus ::Applied );
assert_eq! (
engine ::timeline ( & f . team . document , leg . start , leg . end )
. unwrap ()
. assignments [ 0 ]
. user_id ,
bob
);
assert_eq! (
engine ::timeline ( & f . team . document , back . start , back . end )
. unwrap ()
. assignments [ 0 ]
. user_id ,
owner
);
assert! (
service ::decide_request ( & f . db , bob , f . team . id , request , f . decision ( "approve" ))
. await
. is_err ()
);
let audit = f . db . audit ( f . team . id ). await . unwrap ();
assert_eq! ( audit [ 0 ][ "actor" ], f . people [ 1 ]. username );
assert_eq! ( audit [ 0 ][ "action" ], "replacement.approve" );
assert_eq! (
f . db . sync_status ( f . team . id ). await . unwrap ()[ "requested_revision" ],
f . team . revision
);
f . prioritize_publication (). await ;
let ( a , b ) = tokio ::join! ( worker ::tick ( & f . db ), worker ::tick ( & f . db ));
a . unwrap ();
b . unwrap ();
let status = f . db . sync_status ( f . team . id ). await . unwrap ();
assert_eq! ( status [ "state" ], "synced" );
assert_eq! ( status [ "published_revision" ], f . team . revision );
let events = f . db . audit ( f . team . id ). await . unwrap ();
assert_eq! (
events
. iter ()
. filter ( | e | e [ "action" ] == "publication.succeeded" )
. count (),
1 ,
"Only one worker may publish a team"
);
assert_eq! ( f . db . objects ( f . team . id ). await . unwrap (). len (), 1 );
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn concurrent_edits_and_constraint_failure_preserve_history_and_job () {
let f = Fixture ::new (). await ;
let actor = f . people [ 0 ]. id ;
let team = f . team . id ;
let mut left = f . team . document . clone ();
left . name = "Left" . into ();
let mut right = left . clone ();
right . name = "Right" . into ();
let ( left , right ) = tokio ::join! (
service ::update_document ( & f . db , actor , team , f . team . revision , left ),
service ::update_document ( & f . db , actor , team , f . team . revision , right )
);
assert_eq! ( usize ::from ( left . is_ok ()) + usize ::from ( right . is_ok ()), 1 );
let record = f . db . workspace ( team ). await . unwrap ();
assert_eq! ( record . revision , f . team . revision + 1 );
let before = f . db . audit ( team ). await . unwrap (). len ();
let mut invalid = record . clone ();
invalid . document . name = "Must roll back" . into ();
2026-10-07 13:28:43 +01:00
// A history row that already owns the next revision's key makes the final statement fail
// after the workspace update and job upsert were evaluated: everything must roll back.
let planted = format! ( " {team} : {} " , record . revision + 1 );
f . db . db
. raw_with (
"INSERT INTO meguri__workspace_history(id,workspace_id,revision,document,effective_at) VALUES($1,$2,$3,'{}',now())" ,
& [ & planted , & team . to_string (), & ( record . revision + 1 )],
)
. await
. unwrap ();
2026-09-24 14:35:05 +03:00
assert! (
f . db . commit (
& mut invalid ,
actor ,
Permission ::ManageMembers ,
"test.invalid" ,
json! ({}),
2026-10-07 13:28:43 +01:00
Some ( Permission ::member ())
2026-09-24 14:35:05 +03:00
)
. await
. is_err ()
);
2026-10-07 13:28:43 +01:00
f . db . db
. raw_with (
"DELETE FROM meguri__workspace_history WHERE id=$1" ,
& [ & planted ],
)
. await
. unwrap ();
2026-09-24 14:35:05 +03:00
assert_eq! (
f . db . workspace ( team ). await . unwrap (). revision ,
record . revision
);
assert_eq! ( f . db . audit ( team ). await . unwrap (). len (), before );
assert_eq! (
f . db . sync_status ( team ). await . unwrap ()[ "requested_revision" ],
record . revision
);
let id = team . to_string ();
let rows = query! ( HistoryRow , $workspace_id == id )
. all ( & f . db . db )
. await
. unwrap ();
assert_eq! ( rows . len () as i64 , record . revision );
// An old publication snapshot must not accidentally read revisions committed later.
let mut old = f . team . clone ();
let mut changed = record . document . clone ();
if let ScheduleKind ::Rotation { participants , .. } = & mut changed . schedules [ 0 ]. kind {
participants . swap ( 0 , 1 );
}
service ::update_document ( & f . db , actor , team , record . revision , changed )
. await
. unwrap ();
let old_plan =
f . db . historical_timeline ( & old , f . start , f . start + Duration ::hours ( 1 ))
. await
. unwrap ();
assert_eq! ( old_plan . assignments [ 0 ]. user_id , actor );
old = f . db . workspace ( team ). await . unwrap ();
let new_plan =
f . db . historical_timeline ( & old , f . start , f . start + Duration ::hours ( 1 ))
. await
. unwrap ();
assert_eq! ( new_plan . assignments [ 0 ]. user_id , f . people [ 1 ]. id );
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn volunteer_cover_preview_owner_approval_optional_comments_and_remaining_shift () {
let mut f = Fixture ::new (). await ;
let [ alice , bob , carol ] = f . people . clone (). map ( | p | p . id );
let team = f . team . id ;
let input = | revision , force , legs | service ::NewRequest {
revision ,
remaining : false ,
reason : String ::new (),
legs ,
force ,
};
let preview = service ::preview_replacement (
& f . db ,
bob ,
team ,
input ( f . team . revision , false , vec! [ f . leg ()]),
)
. await
. unwrap ();
assert_eq! ( preview [ "legs" ][ 0 ][ "to_user" ], bob . to_string ());
assert_eq! (
f . db . workspace ( team ). await . unwrap (). revision ,
f . team . revision
);
assert! (
service ::request_replacement (
& f . db ,
carol ,
team ,
input ( f . team . revision , false , vec! [ f . leg ()])
)
. await
. is_err (),
"Unrelated members cannot transfer others' shifts"
);
assert! (
service ::request_replacement (
& f . db ,
bob ,
team ,
input ( f . team . revision , true , vec! [ f . leg ()])
)
. await
. is_err (),
"Volunteering grants no force permission"
);
f . team = service ::request_replacement (
& f . db ,
bob ,
team ,
input ( f . team . revision , false , vec! [ f . leg ()]),
)
. await
. unwrap ();
let request = f . team . document . requests . last (). unwrap ();
assert_eq! ( request . required_approvers , [ alice ]. into ());
assert_eq! ( request . reason , "" );
let id = request . id ;
assert! (
service ::decide_request ( & f . db , bob , team , id , f . decision ( "approve" ))
. await
. is_err ()
);
f . team = service ::decide_request ( & f . db , alice , team , id , f . decision ( "approve" ))
. await
. unwrap ();
assert_eq! (
f . team . document . requests . last (). unwrap (). status ,
RequestStatus ::Applied
);
let mut next = f . leg ();
next . start += Duration ::days ( 1 );
next . end += Duration ::days ( 1 );
f . team =
service ::request_replacement ( & f . db , alice , team , input ( f . team . revision , true , vec! [ next ]))
. await
. unwrap ();
let forced = f . team . document . requests . last (). unwrap ();
assert_eq! ( forced . force_reason . as_deref (), Some ( "" ));
assert_eq! ( forced . forced_by , Some ( alice ));
assert_eq! ( forced . status , RequestStatus ::Applied );
// Current shifts use the server clock. Approval clips the remaining interval, never history.
let mut doc = f . team . document . clone ();
let current = Uuid ::new_v4 ();
doc . schedules . push ( Schedule {
id : current ,
name : "Current duty" . into (),
handover : default_handover (),
kind : ScheduleKind ::Rotation {
participants : vec ! [ alice ],
anchor : "2026-09-21" . parse (). unwrap (),
shift_days : 7 ,
},
});
f . team = service ::update_document ( & f . db , alice , team , f . team . revision , doc )
. await
. unwrap ();
let mut current_input = input (
f . team . revision ,
false ,
vec! [ ReplacementLeg {
schedule_id : current ,
start : Utc ::now () - Duration ::hours ( 1 ),
end : Utc ::now () + Duration ::hours ( 2 ),
from_user : alice ,
to_user : bob ,
}],
);
current_input . remaining = true ;
current_input . reason = "OPS-123 · vacation" . into ();
let before = Utc ::now ();
f . team = service ::request_replacement ( & f . db , bob , team , current_input )
. await
. unwrap ();
let request = f . team . document . requests . last (). unwrap ();
assert! ( request . legs [ 0 ]. start >= before );
let id = request . id ;
let before_approval = Utc ::now ();
f . team = service ::decide_request ( & f . db , alice , team , id , f . decision ( "approve" ))
. await
. unwrap ();
let approved = f . team . document . requests . last (). unwrap ();
assert! ( approved . legs [ 0 ]. start >= before_approval );
assert_eq! ( approved . reason , "OPS-123 · vacation" );
assert! (
f . db . audit ( team )
. await
. unwrap ()
. iter ()
. any ( | e | e [ "action" ] == "replacement.forced" )
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn approval_revalidates_ownership_absence_and_overlap_and_force_is_audited () {
let mut f = Fixture ::new (). await ;
let owner = f . people [ 0 ]. id ;
let bob = f . people [ 1 ]. id ;
let team = f . team . id ;
let leg = f . leg ();
f . propose ( vec! [ leg . clone ()], false ). await ;
let request = f . team . document . requests [ 0 ]. id ;
let mut doc = f . team . document . clone ();
doc . absences . push ( Absence {
id : Uuid ::new_v4 (),
user_id : bob ,
start : leg . start . date_naive (),
end : leg . start . date_naive (),
});
f . team = service ::update_document ( & f . db , owner , team , f . team . revision , doc )
. await
. unwrap ();
assert! (
service ::decide_request ( & f . db , bob , team , request , f . decision ( "approve" ))
. await
. is_err ()
);
let mut doc = f . team . document . clone ();
doc . absences . clear ();
if let ScheduleKind ::Rotation { participants , .. } = & mut doc . schedules [ 0 ]. kind {
participants . swap ( 0 , 2 );
}
f . team = service ::update_document ( & f . db , owner , team , f . team . revision , doc )
. await
. unwrap ();
assert! (
service ::decide_request ( & f . db , bob , team , request , f . decision ( "approve" ))
. await
. is_err ()
);
let mut doc = f . team . document . clone ();
if let ScheduleKind ::Rotation { participants , .. } = & mut doc . schedules [ 0 ]. kind {
participants . swap ( 0 , 2 );
}
f . team = service ::update_document ( & f . db , owner , team , f . team . revision , doc )
. await
. unwrap ();
f . propose ( vec! [ leg ], true ). await ;
assert! (
service ::decide_request ( & f . db , bob , team , request , f . decision ( "approve" ))
. await
. is_err ()
);
let applied = f . team . document . requests . last (). unwrap ();
assert_eq! ( applied . forced_by , Some ( owner ));
assert_eq! ( applied . status , RequestStatus ::Applied );
assert! (
f . db . audit ( team )
. await
. unwrap ()
. iter ()
. any ( | e | e [ "action" ] == "replacement.forced" )
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn jsm_contract_retries_partial_writes_blocks_drift_and_retains_unmanaged_resources () {
let mut f = Fixture ::new (). await ;
let server = fake_jsm ::Server ::start (). await ;
2026-10-07 15:30:31 +01:00
let remote_team = format! ( "og- {} " , Uuid ::new_v4 ());
// Managed resources sort after these, exercising recovery beyond the first page.
for index in 0 .. 51 {
server . state . lock (). unwrap (). objects . insert (
format! ( "/schedules/000-unmanaged- {index:02} " ),
json! ({ "id" :format ! ( "000-unmanaged-{index:02}" ), "name" :format ! ( "Unmanaged {index}" )}),
);
}
2026-09-24 14:35:05 +03:00
let maps = f
. people
. iter ()
. map ( | p | ( p . id , p . username . clone ()))
. collect ();
let client = providers ::jsm ::Jsm ::new (
server . base . clone (),
"test@example.com" . into (),
"test-token" . into (),
remote_team . clone (),
maps ,
)
. unwrap ();
let policy = Uuid ::new_v4 ();
f . team . document . escalations . push ( Escalation {
id : policy ,
name : "Escalation" . into (),
steps : vec ! [ EscalationStep {
delay_minutes : 0 ,
condition : EscalationCondition ::IfNotAcked ,
recipient : Recipient ::Schedule { id : f . schedule },
}],
repeat : Default ::default (),
});
f . team . document . routes . push ( RoutingRule {
id : Uuid ::new_v4 (),
name : "Catch all" . into (),
order : 9999 ,
conditions : vec ! [],
window : None ,
escalation_id : Some ( policy ),
});
2026-10-07 15:30:31 +01:00
f . team
. document
. escalations
. last_mut ()
. unwrap ()
. steps
. push ( EscalationStep {
delay_minutes : 30 ,
condition : EscalationCondition ::IfNotAcked ,
recipient : Recipient ::TeamAll ,
});
f . team
. document
. escalations
. last_mut ()
. unwrap ()
. steps
. push ( EscalationStep {
delay_minutes : 45 ,
condition : EscalationCondition ::IfNotAcked ,
recipient : Recipient ::TeamRandom ,
});
2026-09-24 14:35:05 +03:00
let plan = providers ::compile (
& f . team ,
engine ::timeline ( & f . team . document , f . start , f . start + Duration ::days ( 20 )). unwrap (),
f . start ,
)
. unwrap ();
server . state . lock (). unwrap (). fail_once = Some ( "/escalations" . into ());
assert! ( client . publish ( & f . db , & f . team , & plan , false ). await . is_err ());
assert_eq! (
f . db . objects ( f . team . id ). await . unwrap (). len (),
1 ,
"Successful schedule write must be checkpointed before retry"
);
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
let writes = server . state . lock (). unwrap (). writes ;
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
assert_eq! (
server . state . lock (). unwrap (). writes ,
writes ,
"Retry must not duplicate or rewrite resources"
);
let objects = f . db . objects ( f . team . id ). await . unwrap ();
let schedule = objects
. iter ()
. find ( | o | o . object_key . starts_with ( "schedule:" ))
. unwrap ();
let path = format! ( "/schedules/ {} " , schedule . remote_id );
{
let state = server . state . lock (). unwrap ();
let value = & state . objects [ & path ];
assert_eq! (
value [ "rotations" ][ 0 ][ "participants" ][ 0 ][ "id" ],
f . people [ 0 ]. username
);
assert_eq! (
2026-10-07 15:30:31 +01:00
value [ "rotations" ][ 0 ][ "participants" ][ 1 ][ "id" ],
2026-09-24 14:35:05 +03:00
f . people [ 1 ]. username
);
let route = state
. objects
. iter ()
. find ( | ( k , _ ) | k . contains ( "routing-rules/" ))
. unwrap ()
. 1 ;
assert_eq! ( route [ "order" ], 0 , "Translate priority to a JSM index" );
assert_eq! ( route [ "notify" ][ "type" ], "escalation" );
2026-10-07 15:30:31 +01:00
assert! (
route [ "name" ]
. as_str ()
. unwrap ()
. starts_with ( "Catch all [meguri:" )
);
assert! (
state
. objects
. iter ()
. find ( | ( key , _ ) | key . contains ( "escalations/" ))
. unwrap ()
. 1 [ "name" ]
. as_str ()
. unwrap ()
. starts_with ( "Escalation [meguri:" )
);
2026-09-24 14:35:05 +03:00
}
2026-10-07 15:30:31 +01:00
{
let state = server . state . lock (). unwrap ();
let escalation = state
. objects
. iter ()
. find ( | ( key , _ ) | key . contains ( "/escalations/" ))
. unwrap ()
. 1 ;
assert_eq! (
escalation [ "rules" ][ 1 ],
json! ({ "condition" :"if-not-acked" , "notifyType" :"all" , "delay" :30 , "recipient" :{ "type" :"team" , "id" :remote_team }})
);
assert_eq! (
escalation [ "rules" ][ 2 ],
json! ({ "condition" :"if-not-acked" , "notifyType" :"random" , "delay" :45 , "recipient" :{ "type" :"team" , "id" :remote_team }})
);
}
// Disabled repeats are omitted by JSM; recovery must adopt a matching
// escalation after its initial read-back failed in an older application.
let escalation = objects
. iter ()
. find ( | o | o . object_key . starts_with ( "escalation:" ))
. unwrap ();
f . db . forget ( f . team . id , & escalation . object_key )
. await
. unwrap ();
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
assert_eq! ( server . state . lock (). unwrap (). writes , writes );
let escalation_path = format! ( "/teams/ {remote_team} /escalations/ {} " , escalation . remote_id );
server
. state
. lock ()
. unwrap ()
. objects
. get_mut ( & escalation_path )
. unwrap ()[ "repeat" ][ "waitInterval" ] = json! ( 5 );
assert! ( matches! (
client . publish ( & f . db , & f . team , & plan , false ). await ,
Err ( Error ::Conflict ( _ ))
));
assert_eq! ( server . state . lock (). unwrap (). writes , writes );
server
. state
. lock ()
. unwrap ()
. objects
. get_mut ( & escalation_path )
. unwrap ()[ "repeat" ][ "waitInterval" ] = json! ( 0 );
2026-09-24 14:35:05 +03:00
// Simulate a crash after remote creation, before the local checkpoint.
f . db . forget ( f . team . id , & schedule . object_key ). await . unwrap ();
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
assert_eq! ( server . state . lock (). unwrap (). writes , writes );
server . state . lock (). unwrap (). objects . get_mut ( & path ). unwrap ()[ "timezone" ] = json! ( "Asia/Tokyo" );
assert! ( matches! (
client . publish ( & f . db , & f . team , & plan , false ). await ,
Err ( Error ::Conflict ( _ ))
));
assert_eq! ( server . state . lock (). unwrap (). writes , writes );
client . publish ( & f . db , & f . team , & plan , true ). await . unwrap ();
server . state . lock (). unwrap (). external_override = true ;
assert! ( matches! (
client . publish ( & f . db , & f . team , & plan , false ). await ,
Err ( Error ::Conflict ( _ ))
));
let mut empty = plan . clone ();
empty . schedules . clear ();
empty . escalations . clear ();
empty . routes . clear ();
assert! ( matches! (
client . publish ( & f . db , & f . team , & empty , false ). await ,
Err ( Error ::Conflict ( _ ))
));
assert! ( server . state . lock (). unwrap (). objects . contains_key ( & path ));
client . publish ( & f . db , & f . team , & plan , true ). await . unwrap ();
server . state . lock (). unwrap (). objects . insert (
"/schedules/unmanaged" . into (),
json! ({ "id" :"unmanaged" , "name" :"Do not touch" }),
);
client . publish ( & f . db , & f . team , & empty , false ). await . unwrap ();
2026-10-07 15:30:31 +01:00
assert_eq! ( server . state . lock (). unwrap (). objects . len (), 52 );
2026-09-24 14:35:05 +03:00
assert! (
server
. state
. lock ()
. unwrap ()
. objects
. contains_key ( "/schedules/unmanaged" )
);
}
2026-10-07 15:30:31 +01:00
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn jsm_exact_coverage_recovers_lost_responses_preserves_boundaries_and_blocks_manual_edits () {
let f = Fixture ::new (). await ;
let server = fake_jsm ::Server ::start (). await ;
let client = providers ::jsm ::Jsm ::new (
server . base . clone (),
"test@example.com" . into (),
"fake" . into (),
format! ( "og- {} " , Uuid ::new_v4 ()),
f . people
. iter ()
. map ( | p | ( p . id , p . username . clone ()))
. collect (),
)
. unwrap ();
let mut plan = providers ::compile (
& f . team ,
engine ::timeline ( & f . team . document , f . start , f . start + Duration ::days ( 7 )). unwrap (),
f . start ,
)
. unwrap ();
let mut first = plan . schedules [ 0 ]. periods [ 0 ]. clone ();
first . start = f . start + Duration ::minutes ( 1 ) + Duration ::microseconds ( 53_868 );
first . end = first . start + Duration ::hours ( 2 ) + Duration ::minutes ( 17 );
let mut second = first . clone ();
second . start = first . end ;
second . end = first . end + Duration ::hours ( 3 );
second . user_id = f . people [ 1 ]. id ;
plan . schedules [ 0 ]. periods = vec! [ first . clone (), second . clone ()];
plan . schedules [ 0 ]. recurring = None ;
server . state . lock (). unwrap (). fail_after_override_create = true ;
assert! ( client . publish ( & f . db , & f . team , & plan , false ). await . is_err ());
assert! (
f . db . objects ( f . team . id )
. await
. unwrap ()
. iter ()
. any ( | r | r . object_key . starts_with ( "coverage:" ) && r . remote_id . is_empty ())
);
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
let writes = server . state . lock (). unwrap (). writes ;
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
assert_eq! ( server . state . lock (). unwrap (). writes , writes );
let rows = f . db . objects ( f . team . id ). await . unwrap ();
let parent = rows
. iter ()
. find ( | r | r . object_key . starts_with ( "schedule:" ))
. unwrap ();
let parent_path = format! ( "/schedules/ {} " , parent . remote_id );
let mut overrides : Vec < _ > = server
. state
. lock ()
. unwrap ()
. objects
. iter ()
. filter ( | ( key , _ ) | key . contains ( "/overrides/" ))
. map ( | ( key , value ) | ( key . clone (), value . clone ()))
. collect ();
overrides . sort_by_key ( | ( _ , value ) | value [ "startDate" ]. as_str (). unwrap (). to_owned ());
assert_eq! (
overrides . len (),
2 ,
"Lost create response must not duplicate coverage"
);
assert_eq! (
overrides [ 0 ]. 1 [ "startDate" ]
. as_str ()
. unwrap ()
. parse ::< chrono ::DateTime < Utc >> ()
. unwrap ()
. timestamp_millis (),
first . start . timestamp_millis ()
);
assert_eq! (
overrides [ 0 ]. 1 [ "endDate" ], overrides [ 1 ]. 1 [ "startDate" ],
"Shared half-open boundary must remain exact"
);
assert_eq! ( overrides [ 1 ]. 1 [ "responder" ][ "id" ], f . people [ 1 ]. username );
assert_eq! (
server . state . lock (). unwrap (). objects [ & parent_path ][ "rotations" ][ 0 ][ "participants" ][ 0 ][ "type" ],
"noone"
);
server
. state
. lock ()
. unwrap ()
. objects
. get_mut ( & overrides [ 0 ]. 0 )
. unwrap ()[ "responder" ][ "id" ] = json! ( "external-person" );
assert! ( client . publish ( & f . db , & f . team , & plan , false ). await . is_err ());
assert_eq! ( server . state . lock (). unwrap (). writes , writes );
client . publish ( & f . db , & f . team , & plan , true ). await . unwrap ();
plan . schedules [ 0 ]. periods [ 0 ]. user_id = f . people [ 2 ]. id ;
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
assert_eq! (
server
. state
. lock ()
. unwrap ()
. objects
. values ()
. filter ( | v | v . get ( "alias" ). is_some ())
. count (),
2
);
// An ongoing first shift must not be recreated as a forbidden past-start override.
plan . as_of = first . start + Duration ::seconds ( 1 );
server . state . lock (). unwrap (). override_not_before = Some ( plan . as_of );
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
{
let state = server . state . lock (). unwrap ();
assert_eq! (
state . objects [ & parent_path ][ "rotations" ][ 0 ][ "participants" ][ 0 ][ "id" ],
f . people [ 2 ]. username
);
let upcoming = state
. objects
. values ()
. find ( | v | v [ "responder" ][ "id" ] == f . people [ 1 ]. username )
. unwrap ();
assert_eq! (
upcoming [ "startDate" ]
. as_str ()
. unwrap ()
. parse ::< chrono ::DateTime < Utc >> ()
. unwrap ()
. timestamp_millis (),
second . start . timestamp_millis ()
);
}
// The provider's list excludes expired overrides; that is not external drift.
plan . as_of = first . end + Duration ::seconds ( 1 );
server . state . lock (). unwrap (). override_not_before = Some ( plan . as_of );
server . state . lock (). unwrap (). objects . retain ( | _ , value | {
value
. get ( "endDate" )
. and_then ( serde_json ::Value ::as_str )
. and_then ( | v | v . parse ::< chrono ::DateTime < Utc >> (). ok ())
. is_none_or ( | end | end > plan . as_of )
});
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
assert_eq! (
f . db . objects ( f . team . id )
. await
. unwrap ()
. iter ()
. filter ( | r | r . object_key . starts_with ( "coverage:" ))
. count (),
1
);
{
let state = server . state . lock (). unwrap ();
let base = & state . objects [ & parent_path ][ "rotations" ][ 0 ];
assert_eq! ( base [ "participants" ][ 0 ][ "id" ], f . people [ 1 ]. username );
assert! (
base [ "startDate" ]
. as_str ()
. unwrap ()
. parse ::< chrono ::DateTime < Utc >> ()
. unwrap ()
<= plan . as_of
);
let tail = state
. objects
. values ()
. find ( | v | v . get ( "alias" ). is_some ())
. unwrap ();
assert_eq! ( tail [ "responder" ][ "type" ], "noone" );
assert_eq! (
tail [ "startDate" ]
. as_str ()
. unwrap ()
. parse ::< chrono ::DateTime < Utc >> ()
. unwrap ()
. timestamp_millis (),
second . end . timestamp_millis ()
);
}
let writes = server . state . lock (). unwrap (). writes ;
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
assert_eq! ( server . state . lock (). unwrap (). writes , writes );
server . state . lock (). unwrap (). external_override = true ;
assert! ( matches! (
client . publish ( & f . db , & f . team , & plan , false ). await ,
Err ( Error ::Conflict ( _ ))
));
plan . schedules . clear ();
assert! ( client . publish ( & f . db , & f . team , & plan , false ). await . is_err ());
client . publish ( & f . db , & f . team , & plan , true ). await . unwrap ();
assert! ( server . state . lock (). unwrap (). objects . is_empty ());
assert! ( f . db . objects ( f . team . id ). await . unwrap (). is_empty ());
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn jsm_readable_names_rename_in_place_and_recover_legacy_resources () {
let f = Fixture ::new (). await ;
let server = fake_jsm ::Server ::start (). await ;
let client = providers ::jsm ::Jsm ::new (
server . base . clone (),
"test@example.com" . into (),
"test-token" . into (),
format! ( "og- {} " , Uuid ::new_v4 ()),
f . people
. iter ()
. map ( | p | ( p . id , p . username . clone ()))
. collect (),
)
. unwrap ();
let mut plan = providers ::compile (
& f . team ,
engine ::timeline ( & f . team . document , f . start , f . start + Duration ::days ( 7 )). unwrap (),
f . start ,
)
. unwrap ();
plan . schedules [ 0 ]. name = "Payments · Дежурство" . into ();
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
let objects = f . db . objects ( f . team . id ). await . unwrap ();
let checkpoint = objects
. iter ()
. find ( | o | o . object_key . starts_with ( "schedule:" ))
. unwrap ();
let path = format! ( "/schedules/ {} " , checkpoint . remote_id );
let first_name = server . state . lock (). unwrap (). objects [ & path ][ "name" ]
. as_str ()
. unwrap ()
. to_owned ();
assert! ( first_name . starts_with ( "Payments · Дежурство [meguri:" ));
plan . schedules [ 0 ]. name = "Payments primary" . into ();
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
{
let state = server . state . lock (). unwrap ();
assert_eq! ( state . objects . len (), 1 );
let renamed = state . objects [ & path ][ "name" ]. as_str (). unwrap ();
assert! ( renamed . starts_with ( "Payments primary [meguri:" ));
assert_eq! (
renamed . split ( " [meguri:" ). last (),
first_name . split ( " [meguri:" ). last ()
);
}
// Simulate an old adapter write followed by a lost checkpoint.
f . db . forget ( f . team . id , & checkpoint . object_key )
. await
. unwrap ();
let legacy_name = format! (
"meguri- {} - {} " ,
f . team . id . simple (),
plan . schedules [ 0 ]. id . simple ()
);
{
let mut state = server . state . lock (). unwrap ();
state . objects . get_mut ( & path ). unwrap ()[ "name" ] = json! ( legacy_name );
state . objects . get_mut ( & path ). unwrap ()[ "timezone" ] = json! ( "Asia/Tokyo" );
}
let writes = server . state . lock (). unwrap (). writes ;
assert! ( matches! (
client . publish ( & f . db , & f . team , & plan , false ). await ,
Err ( Error ::Conflict ( _ ))
));
assert_eq! ( server . state . lock (). unwrap (). writes , writes );
server . state . lock (). unwrap (). objects . get_mut ( & path ). unwrap ()[ "timezone" ] =
json! ( plan . schedules [ 0 ]. timezone );
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
let state = server . state . lock (). unwrap ();
assert_eq! ( state . objects . len (), 1 );
assert! (
state . objects [ & path ][ "name" ]
. as_str ()
. unwrap ()
. starts_with ( "Payments primary [meguri:" )
);
}
2026-09-24 14:35:05 +03:00
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn failed_publication_is_durable_and_recoverable () {
let f = Fixture ::new (). await ;
let mut doc = f . team . document . clone ();
let day = engine ::duty_day ( & doc , Utc ::now ()). unwrap ();
let actual = engine ::timeline ( & doc , Utc ::now (), Utc ::now () + Duration ::hours ( 1 ))
. unwrap ()
. assignments [ 0 ]
. user_id ;
doc . absences . push ( Absence {
id : Uuid ::new_v4 (),
user_id : actual ,
start : day ,
end : day ,
});
let record = service ::update_document ( & f . db , f . people [ 0 ]. id , f . team . id , f . team . revision , doc )
. await
. unwrap ();
f . prioritize_publication (). await ;
worker ::tick ( & f . db ). await . unwrap ();
let status = f . db . sync_status ( f . team . id ). await . unwrap ();
assert_eq! ( status [ "state" ], "failed" );
assert! ( status [ "last_error" ]. as_str (). unwrap (). contains ( "absent" ));
let mut doc = record . document . clone ();
doc . absences . clear ();
let record = service ::update_document ( & f . db , f . people [ 0 ]. id , f . team . id , record . revision , doc )
. await
. unwrap ();
f . prioritize_publication (). await ;
worker ::tick ( & f . db ). await . unwrap ();
assert_eq! (
f . db . sync_status ( f . team . id ). await . unwrap ()[ "published_revision" ],
record . revision
);
// A successful retry also leaves a durable audit event and clears the lease.
let id = f . team . id . to_string ();
let job = query! ( SyncJobRow , $workspace_id == id )
. get ( & f . db . db )
. await
. unwrap ()
. unwrap ();
assert! ( job . lease_token . is_none ());
let id = f . team . id . to_string ();
assert! (
query! ( AuditRow , $workspace_id == id && $action == "publication.succeeded" )
. exists ( & f . db . db )
. await
. unwrap ()
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn audit_resolves_historical_names_after_rename_and_keeps_raw_ids () {
let mut f = Fixture ::new (). await ;
let leg = f . leg ();
f . propose ( vec! [ leg ], true ). await ;
let schedule = f . schedule ;
f . team . document . schedules [ 0 ]. name = "Renamed schedule" . into ();
f . db . commit (
& mut f . team ,
f . people [ 0 ]. id ,
Permission ::EditSchedules ,
"test.rename" ,
json! ({ "schedule_id" :schedule }),
None ,
)
. await
. unwrap ();
let events = f . db . audit ( f . team . id ). await . unwrap ();
let event = events
. iter ()
. find ( | e | e [ "details" ]. get ( "legs" ). is_some ())
. unwrap ();
assert_eq! (
event [ "resolved_details" ][ "legs" ][ 0 ][ "schedule_id" ][ "name" ],
"General"
);
assert_eq! (
event [ "resolved_details" ][ "legs" ][ 0 ][ "from_user" ][ "name" ],
"Alice"
);
assert_eq! (
event [ "resolved_details" ][ "legs" ][ 0 ][ "to_user" ][ "name" ],
"Bob"
);
assert_eq! ( event [ "details" ][ "legs" ][ 0 ][ "schedule_id" ], json! ( schedule ));
assert_eq! (
events [ 0 ][ "resolved_details" ][ "schedule_id" ][ "name" ],
"Renamed schedule"
);
// Another tenant's identities are not part of this team's lookup context.
let other = Fixture ::new (). await ;
let other_id = other . people [ 0 ]. id ;
f . db . commit (
& mut f . team ,
f . people [ 0 ]. id ,
Permission ::EditSchedules ,
"test.unknown" ,
json! ({ "user_id" :other_id }),
None ,
)
. await
. unwrap ();
let events = f . db . audit ( f . team . id ). await . unwrap ();
assert_eq! ( events [ 0 ][ "resolved_details" ][ "user_id" ], json! ( other_id ));
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn approved_cover_can_be_replaced_partially_with_approval_and_corrected_again () {
let mut f = Fixture ::new (). await ;
let original = f . leg ();
f . propose ( vec! [ original . clone ()], true ). await ;
let original_id = f . team . document . requests [ 0 ]. id ;
let correction = ReplacementLeg {
start : original . start + Duration ::hours ( 3 ),
end : original . end - Duration ::hours ( 3 ),
from_user : f . people [ 1 ]. id ,
to_user : f . people [ 2 ]. id ,
.. original . clone ()
};
f . team = service ::request_replacement (
& f . db ,
f . people [ 1 ]. id ,
f . team . id ,
service ::NewRequest {
remaining : false ,
revision : f . team . revision ,
reason : "Correct cover" . into (),
legs : vec ! [ correction . clone ()],
force : false ,
},
)
. await
. unwrap ();
let pending = f . team . document . requests . last (). unwrap (). id ;
assert! ( f . team . document . requests [ 0 ]. effective_legs . is_none ());
assert_eq! (
engine ::timeline ( & f . team . document , original . start , original . end )
. unwrap ()
. assignments [ 0 ]
. user_id ,
f . people [ 1 ]. id
);
// Ordinary members still cannot skip approval.
assert! (
service ::decide_request (
& f . db ,
f . people [ 1 ]. id ,
f . team . id ,
pending ,
f . decision ( "force" )
)
. await
. is_err ()
);
f . team = service ::decide_request (
& f . db ,
f . people [ 2 ]. id ,
f . team . id ,
pending ,
f . decision ( "approve" ),
)
. await
. unwrap ();
let old = & f . team . document . requests [ 0 ];
assert_eq! ( old . id , original_id );
assert_eq! ( old . legs [ 0 ]. start , original . start );
assert_eq! ( old . legs [ 0 ]. end , original . end );
assert_eq! ( old . effective_legs (). len (), 2 );
let plan = engine ::timeline ( & f . team . document , original . start , original . end ). unwrap ();
assert_eq! (
plan . assignments
. iter ()
. map ( | a | a . user_id )
. collect ::< Vec < _ >> (),
vec! [ f . people [ 1 ]. id , f . people [ 2 ]. id , f . people [ 1 ]. id ]
);
assert_eq! ( plan . assignments [ 0 ]. end , correction . start );
assert_eq! ( plan . assignments [ 2 ]. start , correction . end );
// A pending request becomes stale if another correction changes its owner.
f . team = service ::request_replacement (
& f . db ,
f . people [ 2 ]. id ,
f . team . id ,
service ::NewRequest {
remaining : false ,
revision : f . team . revision ,
reason : String ::new (),
legs : vec ! [ ReplacementLeg {
from_user : f . people [ 2 ]. id ,
to_user : f . people [ 1 ]. id ,
.. correction . clone ()
}],
force : false ,
},
)
. await
. unwrap ();
let stale = f . team . document . requests . last (). unwrap (). id ;
f . propose (
vec! [ ReplacementLeg {
from_user : f . people [ 2 ]. id ,
to_user : f . people [ 0 ]. id ,
.. correction . clone ()
}],
true ,
)
. await ;
assert! ( f . team . document . requests [ 1 ]. effective_legs (). is_empty ());
assert_eq! ( f . team . document . requests [ 1 ]. legs . len (), 1 );
assert! (
service ::decide_request (
& f . db ,
f . people [ 1 ]. id ,
f . team . id ,
stale ,
f . decision ( "approve" )
)
. await
. is_err ()
);
assert_eq! (
engine ::timeline ( & f . team . document , correction . start , correction . end )
. unwrap ()
. assignments [ 0 ]
. user_id ,
f . people [ 0 ]. id
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn local_absence_range_preserves_approval_and_target_interval () {
use meguri ::absence ::{ CoverRange , Draft , ExactCover , LocalCover };
let mut f = Fixture ::new (). await ;
let day = engine ::schedule_duty_day ( & f . team . document , f . schedule , f . start ). unwrap ();
let exact = ExactCover {
schedule_id : f . schedule ,
start : f . start + Duration ::hours ( 2 ),
end : f . start + Duration ::hours ( 5 ),
};
f . team = service ::create_absence (
& f . db ,
f . people [ 0 ]. id ,
f . team . id ,
service ::NewAbsence {
revision : f . team . revision ,
people : vec ! [],
force : false ,
reason : String ::new (),
draft : Draft {
absence : Absence {
id : Uuid ::new_v4 (),
user_id : f . people [ 0 ]. id ,
start : day ,
end : day ,
},
ranges : vec ! [ CoverRange {
local : Some ( LocalCover {
schedule_id : Some ( f . schedule ),
start : exact
. start
. with_timezone (
& f . team . document . timezone . parse ::< chrono_tz ::Tz > (). unwrap (),
)
. naive_local (),
end : exact
. end
. with_timezone (
& f . team . document . timezone . parse ::< chrono_tz ::Tz > (). unwrap (),
)
. naive_local (),
}),
start : day ,
end : day ,
replacement : f . people [ 1 ]. id ,
replace_projects : false ,
exact : None ,
}],
},
},
)
. await
. unwrap ();
let request = & f . team . document . requests [ 0 ];
assert_eq! ( request . status , RequestStatus ::Pending );
let id = request . id ;
assert_eq! (
( request . legs [ 0 ]. start , request . legs [ 0 ]. end ),
( exact . start , exact . end )
);
assert! (
engine ::timeline ( & f . team . document , exact . start , exact . end )
. unwrap ()
. assignments [ 0 ]
. unavailable
);
f . team = service ::decide_request ( & f . db , f . people [ 1 ]. id , f . team . id , id , f . decision ( "approve" ))
. await
. unwrap ();
assert_eq! (
engine ::timeline ( & f . team . document , exact . start , exact . end )
. unwrap ()
. assignments [ 0 ]
. user_id ,
f . people [ 1 ]. id
);
assert! (
engine ::timeline ( & f . team . document , f . start , exact . start )
. unwrap ()
. assignments [ 0 ]
. unavailable
);
}
2026-10-07 15:30:31 +01:00
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn jsm_recreates_deleted_resources_and_recovers_lost_replacement_responses () {
let mut f = Fixture ::new (). await ;
let server = fake_jsm ::Server ::start (). await ;
let remote_team = format! ( "og- {} " , Uuid ::new_v4 ());
// Managed resources sort after these, exercising recovery beyond the first page.
for index in 0 .. 51 {
server . state . lock (). unwrap (). objects . insert (
format! ( "/schedules/000-unmanaged- {index:02} " ),
json! ({ "id" :format ! ( "000-unmanaged-{index:02}" ), "name" :format ! ( "Unmanaged {index}" )}),
);
}
let maps = f
. people
. iter ()
. map ( | p | ( p . id , p . username . clone ()))
. collect ();
let client = providers ::jsm ::Jsm ::new (
server . base . clone (),
"test@example.com" . into (),
"test-token" . into (),
remote_team . clone (),
maps ,
)
. unwrap ();
let policy = Uuid ::new_v4 ();
f . team . document . escalations . push ( Escalation {
id : policy ,
name : "Escalation" . into (),
steps : vec ! [ EscalationStep {
delay_minutes : 0 ,
condition : EscalationCondition ::IfNotAcked ,
recipient : Recipient ::Schedule { id : f . schedule },
}],
repeat : Default ::default (),
});
f . team . document . routes . push ( RoutingRule {
id : Uuid ::new_v4 (),
name : "Catch all" . into (),
order : 9999 ,
conditions : vec ! [],
window : None ,
escalation_id : Some ( policy ),
});
let mut plan = providers ::compile (
& f . team ,
engine ::timeline ( & f . team . document , f . start , f . start + Duration ::days ( 20 )). unwrap (),
f . start ,
)
. unwrap ();
plan . schedules [ 0 ]. periods [ 0 ]. start += Duration ::minutes ( 1 );
plan . schedules [ 0 ]. recurring = None ;
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
for kind in [ "schedule" , "escalation" , "route" , "coverage" ] {
let old =
f . db . objects ( f . team . id )
. await
. unwrap ()
. into_iter ()
. find ( | r | r . object_key . starts_with ( & format! ( " {kind} :" )))
. unwrap ();
{
let mut state = server . state . lock (). unwrap ();
let path = state
. objects
. keys ()
. find ( | path | path . ends_with ( & format! ( "/ {} " , old . remote_id )))
. unwrap ()
. clone ();
state
. objects
. retain ( | key , _ | key != & path && ! key . starts_with ( & format! ( " {path} /" )));
if kind != "coverage" {
state . fail_after_resource_create = true ;
}
}
if kind != "coverage" {
assert! ( client . publish ( & f . db , & f . team , & plan , false ). await . is_err ());
}
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
let rows = f . db . objects ( f . team . id ). await . unwrap ();
let replacement = rows
. iter ()
. find ( | r | r . object_key == old . object_key )
. unwrap ();
assert_ne! ( replacement . remote_id , old . remote_id );
assert! (
rows . iter (). all ( | r | ! r . content . contains ( & old . remote_id )),
"Dependent checkpoints retain a deleted ID"
);
let writes = server . state . lock (). unwrap (). writes ;
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
let state = server . state . lock (). unwrap ();
assert_eq! (
state . writes , writes ,
"Retry must not create duplicate resources"
);
assert_eq! (
state
. objects
. values ()
. filter ( | v | v [ "name" ]
. as_str ()
. is_some_and ( | name | name . contains ( "[meguri:" )))
. count (),
3
);
}
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn publication_requires_explicit_revision_and_never_picks_up_later_edits () {
let mut f = Fixture ::new (). await ;
let id = f . team . id ;
let actor = f . people [ 0 ]. id ;
worker ::tick ( & f . db ). await . unwrap ();
assert! ( f . db . objects ( id ). await . unwrap (). is_empty ());
assert! ( f . db . sync_status ( id ). await . unwrap ()[ "published_revision" ]. is_null ());
assert! (
f . db . sync_status ( id ). await . unwrap ()[ "has_changes" ]
. as_bool ()
. unwrap ()
);
assert_eq! (
f . db . sync_status ( id ). await . unwrap ()[ "changes" ][ 0 ][ "action" ],
"create"
);
assert! (
f . db . retry ( id , f . people [ 1 ]. id , f . team . revision )
. await
. is_err ()
);
assert! ( f . db . retry ( id , actor , f . team . revision - 1 ). await . is_err ());
f . prioritize_publication (). await ;
let authorized = f . team . revision ;
let mut doc = f . team . document . clone ();
doc . schedules [ 0 ]. name = "Unpublished rename" . into ();
f . team = service ::update_document ( & f . db , actor , id , f . team . revision , doc )
. await
. unwrap ();
worker ::tick ( & f . db ). await . unwrap ();
let status = f . db . sync_status ( id ). await . unwrap ();
assert_eq! ( status [ "published_revision" ], authorized );
assert_eq! ( status [ "has_changes" ], true );
assert_ne! (
f . db . published_document ( id )
. await
. unwrap ()
. unwrap ()
. schedules [ 0 ]
. name ,
"Unpublished rename"
);
let successes =
f . db . audit ( id )
. await
. unwrap ()
. iter ()
. filter ( | e | e [ "action" ] == "publication.succeeded" )
. count ();
worker ::tick ( & f . db ). await . unwrap ();
assert_eq! (
f . db . audit ( id )
. await
. unwrap ()
. iter ()
. filter ( | e | e [ "action" ] == "publication.succeeded" )
. count (),
successes
);
f . prioritize_publication (). await ;
worker ::tick ( & f . db ). await . unwrap ();
assert_eq! ( f . db . sync_status ( id ). await . unwrap ()[ "has_changes" ], false );
assert_eq! (
f . db . sync_status ( id ). await . unwrap ()[ "changes" ][ 0 ][ "action" ],
"unchanged"
);
// A failing approved snapshot must keep failing after an unapproved correction.
let mut doc = f . team . document . clone ();
let now = Utc ::now ();
let user = engine ::timeline ( & doc , now , now + Duration ::hours ( 1 ))
. unwrap ()
. assignments [ 0 ]
. user_id ;
let day = engine ::duty_day ( & doc , now ). unwrap ();
doc . absences . push ( Absence {
id : Uuid ::new_v4 (),
user_id : user ,
start : day ,
end : day ,
});
f . team = service ::update_document ( & f . db , actor , id , f . team . revision , doc )
. await
. unwrap ();
f . prioritize_publication (). await ;
let approved = f . team . revision ;
let mut doc = f . team . document . clone ();
doc . absences . clear ();
f . team = service ::update_document ( & f . db , actor , id , f . team . revision , doc )
. await
. unwrap ();
worker ::tick ( & f . db ). await . unwrap ();
let status = f . db . sync_status ( id ). await . unwrap ();
assert_eq! ( status [ "state" ], "failed" );
assert_eq! ( status [ "authorized_revision" ], approved );
f . db . db . raw_with ( "UPDATE meguri__sync_jobs SET next_attempt='1970-01-01T00:00:00Z' WHERE workspace_id=$1" , & [ & id . to_string ()]). await . unwrap ();
worker ::tick ( & f . db ). await . unwrap ();
assert_eq! ( f . db . sync_status ( id ). await . unwrap ()[ "state" ], "failed" );
f . prioritize_publication (). await ;
worker ::tick ( & f . db ). await . unwrap ();
assert_eq! ( f . db . sync_status ( id ). await . unwrap ()[ "has_changes" ], false );
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn shared_generator_drafts_are_scoped_durable_and_revision_checked () {
let mut f = Fixture ::new (). await ;
let actor = f . people [ 0 ]. id ;
let colleague = f . people [ 1 ]. id ;
let id = f . team . id ;
2026-10-07 16:06:10 +01:00
// A second administrator may edit shared drafts; the employee role here cannot.
f . make_admin ( & f . people [ 1 ]). await ;
f . team = f . db . workspace ( id ). await . unwrap ();
2026-10-07 15:30:31 +01:00
let payload = json! ({ "revision" :f . team . revision , "variables" :[], "rules" :[], "route_position" :0 });
let saved =
f . db . save_shared_draft ( id , actor , 0 , payload . clone ())
. await
. unwrap ();
assert_eq! ( saved [ "revision" ], 1 );
let reader = meguri ::store ::Store ::connect ( & std ::env ::var ( "MEGURI_TEST_DATABASE_URL" ). unwrap ())
. await
. unwrap ();
reader
. require ( id , colleague , Permission ::View )
. await
. unwrap ();
assert_eq! ( reader . shared_draft ( id ). await . unwrap (), saved );
assert! (
f . db . save_shared_draft ( id , colleague , 0 , payload . clone ())
. await
. is_err ()
);
let saved =
f . db . save_shared_draft ( id , colleague , 1 , payload . clone ())
. await
. unwrap ();
assert_eq! ( saved [ "revision" ], 2 );
assert! (
f . db . save_shared_draft ( id , f . people [ 2 ]. id , 2 , payload . clone ())
. await
. is_err ()
);
2026-10-07 16:06:10 +01:00
let other = f . extra_workspace ( "Other shared draft" , & f . people [ 0 ]). await ;
2026-10-07 15:30:31 +01:00
assert! (
f . db . save_shared_draft (
other . id ,
colleague ,
0 ,
json! ({ "revision" :other . revision , "variables" :[], "rules" :[]})
)
. await
. is_err ()
);
worker ::tick ( & f . db ). await . unwrap ();
assert! ( f . db . objects ( id ). await . unwrap (). is_empty ());
let mut draft : meguri ::generation_service ::Draft = serde_json ::from_value ( payload ). unwrap ();
draft . shared_revision = Some ( 1 );
let preview = meguri ::generation_service ::preview ( & f . db , actor , id , draft . clone ())
. await
. unwrap ();
draft . review_id = preview [ "review_id" ]. as_str (). map ( str ::to_owned );
assert! (
meguri ::generation_service ::apply ( & f . db , actor , id , draft )
. await
. is_err (),
"A newer shared draft must block stale activation"
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn jsm_compacts_existing_weekly_shifts_without_recreating_the_schedule () {
let mut f = Fixture ::new (). await ;
f . team . document . timezone = "Europe/Bucharest" . into ();
f . team . document . schedules [ 0 ]. kind = ScheduleKind ::Rotation {
participants : vec ! [ f . people [ 0 ]. id , f . people [ 1 ]. id ],
anchor : "2026-09-21" . parse (). unwrap (),
shift_days : 7 ,
};
let from = "2026-09-30T12:00:00Z" . parse (). unwrap ();
let to = "2026-12-28T09:00:00Z" . parse (). unwrap ();
let plan = providers ::compile (
& f . team ,
engine ::timeline ( & f . team . document , from , to ). unwrap (),
from ,
)
. unwrap ();
let server = fake_jsm ::Server ::start (). await ;
let client = providers ::jsm ::Jsm ::new (
server . base . clone (),
"test@example.com" . into (),
"fake" . into (),
Uuid ::new_v4 (). to_string (),
f . people
. iter ()
. map ( | p | ( p . id , p . username . clone ()))
. collect (),
)
. unwrap ();
let mut old = plan . clone ();
old . schedules [ 0 ]. recurring = None ;
client . publish ( & f . db , & f . team , & old , false ). await . unwrap ();
let parent =
f . db . objects ( f . team . id )
. await
. unwrap ()
. into_iter ()
. find ( | r | r . object_key . starts_with ( "schedule:" ))
. unwrap ();
let path = format! ( "/schedules/ {} " , parent . remote_id );
assert! (
server . state . lock (). unwrap (). objects [ & path ][ "rotations" ]
. as_array ()
. unwrap ()
. len ()
> 1
);
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
{
let state = server . state . lock (). unwrap ();
assert_eq! ( state . objects . len (), 1 );
let rotations = state . objects [ & path ][ "rotations" ]. as_array (). unwrap ();
assert_eq! ( rotations . len (), 1 );
assert_eq! ( rotations [ 0 ][ "name" ], "General" );
assert_eq! ( rotations [ 0 ][ "type" ], "weekly" );
assert_eq! ( rotations [ 0 ][ "length" ], 1 );
assert_eq! ( rotations [ 0 ][ "participants" ][ 0 ][ "id" ], f . people [ 1 ]. username );
assert_eq! ( rotations [ 0 ][ "participants" ][ 1 ][ "id" ], f . people [ 0 ]. username );
assert_eq! ( rotations [ 0 ][ "startDate" ], "2026-09-28T08:00:00Z" );
assert_eq! ( rotations [ 0 ][ "endDate" ], "2026-12-28T09:00:00Z" );
}
let writes = server . state . lock (). unwrap (). writes ;
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
assert_eq! ( server . state . lock (). unwrap (). writes , writes );
server . state . lock (). unwrap (). objects . get_mut ( & path ). unwrap ()[ "rotations" ][ 0 ][ "participants" ]
. as_array_mut ()
. unwrap ()
. reverse ();
assert! ( matches! (
client . publish ( & f . db , & f . team , & plan , false ). await ,
Err ( Error ::Conflict ( _ ))
));
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn jsm_reorders_and_renames_rotation_in_place_with_lost_response_recovery () {
let mut f = Fixture ::new (). await ;
let server = fake_jsm ::Server ::start (). await ;
let client = providers ::jsm ::Jsm ::new (
server . base . clone (),
"test@example.com" . into (),
"fake" . into (),
Uuid ::new_v4 (). to_string (),
f . people
. iter ()
. map ( | p | ( p . id , p . username . clone ()))
. collect (),
)
. unwrap ();
let compile = | record : & meguri ::store ::WorkspaceRecord | {
providers ::compile (
record ,
engine ::timeline ( & record . document , f . start , f . start + Duration ::days ( 60 )). unwrap (),
f . start ,
)
. unwrap ()
};
client
. publish ( & f . db , & f . team , & compile ( & f . team ), false )
. await
. unwrap ();
let parent =
f . db . objects ( f . team . id )
. await
. unwrap ()
. into_iter ()
. find ( | r | r . object_key . starts_with ( "schedule:" ))
. unwrap ();
let path = format! ( "/schedules/ {} " , parent . remote_id );
let rotation_id = server . state . lock (). unwrap (). objects [ & path ][ "rotations" ][ 0 ][ "id" ]. clone ();
let ScheduleKind ::Rotation { participants , .. } = & mut f . team . document . schedules [ 0 ]. kind else {
panic! ()
};
participants . swap ( 0 , 1 );
server . state . lock (). unwrap (). fail_after_rotation_patch = true ;
let plan = compile ( & f . team );
assert! ( client . publish ( & f . db , & f . team , & plan , false ). await . is_err ());
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
{
let state = server . state . lock (). unwrap ();
assert_eq! ( state . rotation_creates , 1 );
assert_eq! ( state . rotation_patches , 1 );
assert_eq! ( state . objects [ & path ][ "rotations" ][ 0 ][ "id" ], rotation_id );
assert_eq! (
state . objects [ & path ][ "rotations" ][ 0 ][ "participants" ][ 0 ][ "id" ],
f . people [ 1 ]. username
);
}
// Metadata succeeds remotely but its response is lost before rotation rename.
f . team . document . schedules [ 0 ]. name = "Renamed on call" . into ();
let plan = compile ( & f . team );
server . state . lock (). unwrap (). fail_after_schedule_patch = true ;
assert! ( client . publish ( & f . db , & f . team , & plan , false ). await . is_err ());
let correct = server . state . lock (). unwrap (). objects [ & path ][ "rotations" ][ 0 ]. clone ();
server . state . lock (). unwrap (). objects . get_mut ( & path ). unwrap ()[ "rotations" ][ 0 ][ "participants" ]
[ 0 ][ "id" ] = json! ( "external-person" );
let writes_before_conflict = server . state . lock (). unwrap (). writes ;
assert! ( matches! (
client . publish ( & f . db , & f . team , & plan , false ). await ,
Err ( Error ::Conflict ( _ ))
));
assert_eq! ( server . state . lock (). unwrap (). writes , writes_before_conflict );
server . state . lock (). unwrap (). objects . get_mut ( & path ). unwrap ()[ "rotations" ][ 0 ] = correct ;
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
let writes = server . state . lock (). unwrap (). writes ;
client . publish ( & f . db , & f . team , & plan , false ). await . unwrap ();
{
let state = server . state . lock (). unwrap ();
assert_eq! ( state . rotation_creates , 1 );
assert_eq! ( state . rotation_patches , 2 );
assert_eq! ( state . writes , writes );
assert_eq! ( state . objects [ & path ][ "rotations" ][ 0 ][ "id" ], rotation_id );
assert_eq! (
state . objects [ & path ][ "rotations" ][ 0 ][ "name" ],
"Renamed on call"
);
}
assert! (
f . db . objects ( f . team . id )
. await
. unwrap ()
. iter ()
. all ( | r | ! r . object_key . starts_with ( "pending:" ))
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn expired_publication_lease_allows_manual_recovery () {
let f = Fixture ::new (). await ;
let id = f . team . id ;
let actor = f . people [ 0 ]. id ;
f . db . retry ( id , actor , f . team . revision ). await . unwrap ();
f . db . db . raw_with ( "UPDATE meguri__sync_jobs SET state='publishing',lease_token='interrupted-worker',lease_until=now()+interval '5 minutes' WHERE workspace_id=$1" , & [ & id . to_string ()]). await . unwrap ();
assert_eq! (
f . db . sync_status ( id ). await . unwrap ()[ "publication_busy" ],
true
);
assert! ( f . db . retry ( id , actor , f . team . revision ). await . is_err ());
f . db . db . raw_with ( "UPDATE meguri__sync_jobs SET lease_until=now()-interval '1 second' WHERE workspace_id=$1" , & [ & id . to_string ()]). await . unwrap ();
let status = f . db . sync_status ( id ). await . unwrap ();
assert_eq! ( status [ "state" ], "publishing" );
assert_eq! ( status [ "publication_busy" ], false );
f . db . retry ( id , actor , f . team . revision ). await . unwrap ();
assert_eq! ( f . db . sync_status ( id ). await . unwrap ()[ "state" ], "pending" );
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn portable_import_preview_is_scoped_revision_checked_and_only_saves_shared_draft () {
use meguri ::generation_service ::{ self , Transfer };
let f = Fixture ::new (). await ;
let other = Fixture ::new (). await ;
let text = json! ({ "format" :"meguri/v1" , "variables" :[{ "name" :"portable greeting" , "node" :{ "kind" :"text" , "value" :"Hello" }}], "generators" :[]}). to_string ();
let request = | review_id | Transfer {
revision : f . team . revision ,
shared_revision : 0 ,
text : text . clone (),
review_id ,
};
assert! (
generation_service ::import_config (
& f . db ,
other . people [ 0 ]. id ,
f . team . id ,
request ( None ),
false
)
. await
. is_err ()
);
assert! (
generation_service ::export_config ( & f . db , other . people [ 0 ]. id , f . team . id , request ( None ))
. await
. is_err ()
);
let before = f . db . shared_draft ( f . team . id ). await . unwrap ();
let preview =
generation_service ::import_config ( & f . db , f . people [ 0 ]. id , f . team . id , request ( None ), false )
. await
. unwrap ();
assert! (
preview [ "preview" ][ "issues" ]. as_array (). unwrap (). is_empty (),
"{preview}"
);
assert_eq! ( before , f . db . shared_draft ( f . team . id ). await . unwrap ());
assert! (
generation_service ::import_config (
& f . db ,
f . people [ 0 ]. id ,
f . team . id ,
request ( Some ( "wrong-review" . into ())),
true
)
. await
. is_err ()
);
let token = preview [ "review_id" ]. as_str (). unwrap (). to_owned ();
let saved = generation_service ::import_config (
& f . db ,
f . people [ 0 ]. id ,
f . team . id ,
request ( Some ( token . clone ())),
true ,
)
. await
. unwrap ();
assert_eq! (
saved [ "payload" ][ "variables" ][ 0 ][ "name" ],
"portable greeting"
);
let record = f . db . workspace ( f . team . id ). await . unwrap ();
assert_eq! ( record . revision , f . team . revision );
assert_eq! (
serde_json ::to_value ( record . document ). unwrap (),
serde_json ::to_value ( & f . team . document ). unwrap ()
);
assert! (
generation_service ::import_config (
& f . db ,
f . people [ 0 ]. id ,
f . team . id ,
request ( Some ( token )),
true
)
. await
. is_err ()
);
let exported = generation_service ::export_config (
& f . db ,
f . people [ 0 ]. id ,
f . team . id ,
Transfer {
shared_revision : 1 ,
.. request ( None )
},
)
. await
. unwrap ();
assert! (
exported [ "text" ]
. as_str ()
. unwrap ()
. contains ( "portable greeting" )
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn contact_replacements_skip_only_contact_approvals_and_preserve_permissions () {
let mut f = Fixture ::new (). await ;
let actor = f . people [ 0 ]. id ;
let contact = Uuid ::new_v4 ();
let mut doc = f . team . document . clone ();
doc . people . push ( Person {
id : contact ,
user_id : None ,
display_name : "Planning contact" . into (),
email : Some ( "planning@example.com" . into ()),
country : Some ( "CY" . into ()),
2026-10-07 16:06:10 +01:00
aliases : vec ! [],
2026-10-07 15:30:31 +01:00
});
f . team = service ::update_document ( & f . db , actor , f . team . id , f . team . revision , doc )
. await
. unwrap ();
let mut leg = f . leg ();
leg . to_user = contact ;
let input = | revision , legs | service ::NewRequest {
revision ,
legs ,
force : false ,
remaining : false ,
reason : String ::new (),
};
let preview = service ::preview_replacement (
& f . db ,
actor ,
f . team . id ,
input ( f . team . revision , vec! [ leg . clone ()]),
)
. await
. unwrap ();
assert_eq! ( preview [ "applies_immediately" ], true );
f . team = service ::request_replacement (
& f . db ,
actor ,
f . team . id ,
input ( f . team . revision , vec! [ leg . clone ()]),
)
. await
. unwrap ();
let request = f . team . document . requests . last (). unwrap ();
assert_eq! ( request . status , RequestStatus ::Applied );
assert! ( request . required_approvers . is_empty ());
assert! ( request . forced_by . is_none ());
assert! ( request . applied_at . is_some ());
assert! (
f . db . require ( f . team . id , contact , Permission ::View )
. await
. is_err ()
);
let key = contact . to_string ();
assert! (
! query! ( meguri ::models ::UserRow , $id == key )
. exists ( & f . db . db )
. await
. unwrap ()
);
leg . from_user = contact ;
leg . to_user = f . people [ 1 ]. id ;
// An unrelated ordinary member cannot plan on behalf of a contact.
assert! (
service ::request_replacement (
& f . db ,
f . people [ 2 ]. id ,
f . team . id ,
input ( f . team . revision , vec! [ leg . clone ()])
)
. await
. is_err ()
);
f . team =
service ::request_replacement ( & f . db , actor , f . team . id , input ( f . team . revision , vec! [ leg ]))
. await
. unwrap ();
let request = f . team . document . requests . last (). unwrap ();
assert_eq! ( request . status , RequestStatus ::Pending );
assert_eq! ( request . required_approvers , [ f . people [ 1 ]. id ]. into ());
let id = request . id ;
f . team = service ::decide_request ( & f . db , f . people [ 1 ]. id , f . team . id , id , f . decision ( "approve" ))
. await
. unwrap ();
assert_eq! (
f . team . document . requests . last (). unwrap (). status ,
RequestStatus ::Applied
);
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
async fn contact_absence_cover_applies_without_force_or_accounts () {
use meguri ::absence ::{ CoverRange , Draft };
let mut f = Fixture ::new (). await ;
let actor = f . people [ 0 ]. id ;
let contacts = [ Uuid ::new_v4 (), Uuid ::new_v4 ()];
let mut doc = f . team . document . clone ();
for ( index , id ) in contacts . into_iter (). enumerate () {
doc . people . push ( Person {
id ,
user_id : None ,
display_name : format ! ( "Contact {index}" ),
email : Some ( format! ( "contact {index} @example.com" )),
country : Some ( "CY" . into ()),
2026-10-07 16:06:10 +01:00
aliases : vec ! [],
2026-10-07 15:30:31 +01:00
});
}
if let ScheduleKind ::Rotation { participants , .. } = & mut doc . schedules [ 0 ]. kind {
* participants = vec! [ contacts [ 0 ]];
}
f . team = service ::update_document ( & f . db , actor , f . team . id , f . team . revision , doc )
. await
. unwrap ();
let start = f . start . date_naive ();
let input = service ::NewAbsence {
revision : f . team . revision ,
force : false ,
reason : String ::new (),
people : vec ! [],
draft : Draft {
absence : Absence {
id : Uuid ::new_v4 (),
user_id : contacts [ 0 ],
start ,
end : start ,
},
ranges : vec ! [ CoverRange {
local : None ,
exact : None ,
start ,
end : start ,
replacement : contacts [ 1 ],
replace_projects : false ,
}],
},
};
let preview = service ::preview_absence (
& f . db ,
actor ,
f . team . id ,
service ::NewAbsence {
revision : input . revision ,
draft : input . draft . clone (),
people : vec ! [],
force : false ,
reason : String ::new (),
},
)
. await
. unwrap ();
assert_eq! ( preview [ "plan" ][ "errors" ], json! ([]));
assert! ( ! preview [ "approval_groups" ]. as_array (). unwrap (). is_empty ());
assert_eq! (
preview [ "approval_groups" ][ 0 ][ "required_approvers" ],
json! ([])
);
f . team = service ::create_absence ( & f . db , actor , f . team . id , input )
. await
. unwrap ();
assert! ( ! f . team . document . requests . is_empty ());
for request in & f . team . document . requests {
assert_eq! ( request . status , RequestStatus ::Applied );
assert! ( request . forced_by . is_none ());
assert! ( request . required_approvers . is_empty ());
}
assert_eq! ( f . db . member_ids ( f . team . id ). await . unwrap (). len (), 3 );
}
#[tokio::test]
#[ignore = "requires MEGURI_TEST_DATABASE_URL" ]
2026-10-07 16:06:10 +01:00
async fn account_owns_person_email_and_workspace_cannot_override_it () {
use meguri ::admin ::{ self , NewUser };
2026-10-07 15:30:31 +01:00
let mut f = Fixture ::new (). await ;
2026-10-07 16:06:10 +01:00
let lead = f . people [ 0 ]. id ;
2026-10-07 15:30:31 +01:00
let username = format! ( "identity- {} " , Uuid ::new_v4 (). simple ());
let email = format! ( " {username} @example.com" );
2026-10-07 16:06:10 +01:00
let created = admin ::create_password_user (
2026-10-07 15:30:31 +01:00
& f . db ,
2026-10-07 16:06:10 +01:00
& f . config ,
None ,
NewUser {
username : username . clone (),
display_name : "Account" . into (),
password : "test-password-only" . into (),
email : Some ( format! ( " {} " , email . to_uppercase ())),
groups : vec ! [],
country : None ,
},
2026-10-07 15:30:31 +01:00
)
. await
. unwrap ();
2026-10-07 16:06:10 +01:00
let account : User = serde_json ::from_value ( created [ "user" ]. clone ()). unwrap ();
2026-10-07 15:30:31 +01:00
assert_eq! ( account . email . as_deref (), Some ( email . as_str ()));
assert_eq! ( account . person . user_id , Some ( account . id ));
assert! (
f . db . require ( f . team . id , account . id , Permission ::View )
. await
. is_err ()
);
2026-10-07 16:06:10 +01:00
// Access comes from the group mapping; the account's Person joins the workspace document at
// the next committed revision.
meguri ::identity ::apply_access (
2026-10-07 15:30:31 +01:00
& f . db ,
2026-10-07 16:06:10 +01:00
& f . config ,
account . id ,
Some ( f . config . workspaces [ 0 ]. member_groups . clone ()),
)
. await
. unwrap ();
f . team = service ::update_document (
& f . db ,
lead ,
2026-10-07 15:30:31 +01:00
f . team . id ,
f . team . revision ,
2026-10-07 16:06:10 +01:00
f . team . document . clone (),
2026-10-07 15:30:31 +01:00
)
. await
. unwrap ();
assert_eq! (
f . team . document . person ( account . id ). unwrap (). email ,
account . email
);
let mut forged = f . team . document . clone ();
forged
. people
. iter_mut ()
. find ( | p | p . id == account . id )
. unwrap ()
. email = Some ( "wrong@example.com" . into ());
assert! (
2026-10-07 16:06:10 +01:00
service ::update_document ( & f . db , lead , f . team . id , f . team . revision , forged )
2026-10-07 15:30:31 +01:00
. await
. is_err ()
);
let mut row = query! ( meguri ::models ::UserRow , $id == account . id . to_string ())
. get ( & f . db . db )
. await
. unwrap ()
. unwrap ();
row . display_name = "Renamed account" . into ();
service ::save_profile ( & f . db , & row ). await . unwrap ();
let saved = f . db . workspace ( f . team . id ). await . unwrap ();
assert_eq! (
saved . document . person ( account . id ). unwrap (). display_name ,
"Renamed account"
);
assert_eq! (
saved . document . person ( account . id ). unwrap (). email . as_deref (),
Some ( email . as_str ())
);
assert_eq! ( saved . revision , f . team . revision + 1 );
let base = meguri ::generation_service ::Draft {
revision : saved . revision ,
shared_revision : None ,
variables : vec ! [],
rules : vec ! [],
route_position : 0 ,
review_id : None ,
};
let config =
json! ({ "format" :"meguri/v2" , "variables" :{ "owner" :email }, "generators" :[]}). to_string ();
let ( imported , _ ) =
meguri ::portable ::import ( f . team . id , & saved . document , & base , & config ). unwrap ();
assert! (
serde_json ::to_string ( & imported . variables )
. unwrap ()
. contains ( & account . id . to_string ())
);
let exported = meguri ::portable ::export ( & saved . document , & imported ). unwrap ();
assert_eq! ( exported [ "variables" ][ "owner" ], email );
2026-10-07 16:06:10 +01:00
// An account created with a contact's email does not take over that contact.
2026-10-07 15:30:31 +01:00
let contact = Uuid ::new_v4 ();
let contact_email = format! ( " {contact} @example.com" );
let mut doc = saved . document . clone ();
doc . people . push ( Person {
id : contact ,
user_id : None ,
display_name : "Contact" . into (),
email : Some ( contact_email . clone ()),
country : Some ( "CY" . into ()),
2026-10-07 16:06:10 +01:00
aliases : vec ! [],
2026-10-07 15:30:31 +01:00
});
f . team = service ::update_document ( & f . db , f . people [ 0 ]. id , f . team . id , saved . revision , doc )
. await
. unwrap ();
2026-10-07 16:06:10 +01:00
let newcomer : User = serde_json ::from_value (
admin ::create_password_user (
& f . db ,
& f . config ,
None ,
NewUser {
username : format ! ( "new-{}" , Uuid ::new_v4 (). simple ()),
display_name : "New" . into (),
password : "test-password-only" . into (),
email : Some ( contact_email ),
groups : vec ! [],
country : None ,
},
)
. await
. unwrap ()[ "user" ]
. clone (),
2026-10-07 15:30:31 +01:00
)
. unwrap ();
assert_ne! ( newcomer . id , contact );
assert! (
f . db . require ( f . team . id , newcomer . id , Permission ::View )
. await
. is_err ()
);
assert_eq! (
f . db . workspace ( f . team . id )
. await
. unwrap ()
. document
. person ( contact )
. unwrap ()
. user_id ,
None
);
}