use chrono::{Duration, Utc}; use cot::db::{Model, query}; use meguri::{ domain::*, models::{HistoryRow, UserRow}, service, store::{Store, WorkspaceRecord}, }; use uuid::Uuid; pub struct Fixture { pub db: Store, pub people: [User; 3], pub team: WorkspaceRecord, pub schedule: Uuid, pub start: chrono::DateTime, } impl Fixture { pub async fn new() -> Self { let url = std::env::var("MEGURI_TEST_DATABASE_URL") .expect("Set MEGURI_TEST_DATABASE_URL to a disposable PostgreSQL database"); let db = Store::connect(&url).await.unwrap(); let mut people = vec![]; for name in ["Alice", "Bob", "Carol"] { let user = User { language: None, person: Person { id: Uuid::new_v4(), user_id: None, display_name: name.into(), email: Some(format!("{}@example.com", Uuid::new_v4())), country: Some("CY".into()), }, username: format!("test-{}", Uuid::new_v4().simple()), timezone: None, }; let mut user = user; user.person.user_id = Some(user.id); UserRow { language: None, demo_deleted_at: None, id: user.id.to_string(), username: user.username.clone(), display_name: user.display_name.clone(), country: user.country.clone().unwrap(), email: user.email.clone().unwrap(), timezone: None, password_hash: "test-only-unusable-password".into(), created_at: Utc::now(), } .insert(&db.db) .await .unwrap(); people.push(user); } let people: [User; 3] = people.try_into().unwrap(); let mut team = service::create_workspace(&db, people[0].id, "Test team".into()) .await .unwrap(); for person in &people[1..] { team = service::set_member( &db, people[0].id, team.id, team.revision, person.username.clone(), Permission::member(), ) .await .unwrap(); } let schedule = Uuid::new_v4(); let day = Utc::now().date_naive() + Duration::days(2); let mut doc = team.document.clone(); doc.timezone = "UTC".into(); doc.schedules.push(Schedule { handover: default_handover(), id: schedule, name: "General".into(), kind: ScheduleKind::Rotation { participants: people.iter().map(|p| p.id).collect(), anchor: day, shift_days: 7, }, }); team = service::update_document(&db, people[0].id, team.id, team.revision, doc) .await .unwrap(); let start = meguri::engine::handover(&team.document, day).unwrap(); Self { db, people, team, schedule, start, } } pub fn leg(&self) -> ReplacementLeg { ReplacementLeg { schedule_id: self.schedule, start: self.start, end: self.start + Duration::days(1), from_user: self.people[0].id, to_user: self.people[1].id, } } pub async fn propose(&mut self, legs: Vec, force: bool) { self.team = service::request_replacement( &self.db, self.people[0].id, self.team.id, service::NewRequest { remaining: false, revision: self.team.revision, reason: "Test replacement".into(), legs, force, }, ) .await .unwrap(); } pub fn decision(&self, action: &str) -> service::RequestDecision { service::RequestDecision { revision: self.team.revision, action: action.into(), reason: "Test decision".into(), } } pub async fn prioritize_publication(&self) { let revision = self.db.workspace(self.team.id).await.unwrap().revision; self.db .retry(self.team.id, self.people[0].id, revision) .await .unwrap(); // PostgreSQL and the worker can run on different host clocks. Publish only // once this test's latest committed revision has actually become effective. let workspace = self.team.id.to_string(); let effective = query!(HistoryRow, $workspace_id == workspace) .all(&self.db.db) .await .unwrap() .into_iter() .map(|row| row.effective_at) .max() .unwrap(); if let Ok(delay) = (effective - Utc::now()).to_std() { assert!( delay < std::time::Duration::from_secs(2), "Database clock is too far ahead of the test worker" ); tokio::time::sleep(delay + std::time::Duration::from_millis(1)).await; } // The worker takes a bounded batch. Repeated runs leave other test tenants // in this disposable database, so explicitly put this test's job first. self.db .db .raw_with( "UPDATE meguri__sync_jobs SET next_attempt='1970-01-01T00:00:00Z' WHERE workspace_id=$1", &[&self.team.id.to_string()], ) .await .unwrap(); } }