2026-09-24 14:35:05 +03:00
|
|
|
use chrono::{Duration, Utc};
|
2026-10-07 15:30:31 +01:00
|
|
|
use cot::db::{Model, query};
|
2026-09-24 14:35:05 +03:00
|
|
|
use meguri::{
|
|
|
|
|
domain::*,
|
2026-10-07 15:30:31 +01:00
|
|
|
models::{HistoryRow, UserRow},
|
2026-09-24 14:35:05 +03:00
|
|
|
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<Utc>,
|
|
|
|
|
}
|
|
|
|
|
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,
|
2026-10-07 15:30:31 +01:00
|
|
|
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()),
|
|
|
|
|
},
|
2026-09-24 14:35:05 +03:00
|
|
|
username: format!("test-{}", Uuid::new_v4().simple()),
|
|
|
|
|
timezone: None,
|
|
|
|
|
};
|
2026-10-07 15:30:31 +01:00
|
|
|
let mut user = user;
|
|
|
|
|
user.person.user_id = Some(user.id);
|
2026-09-24 14:35:05 +03:00
|
|
|
UserRow {
|
|
|
|
|
language: None,
|
|
|
|
|
demo_deleted_at: None,
|
|
|
|
|
id: user.id.to_string(),
|
|
|
|
|
username: user.username.clone(),
|
|
|
|
|
display_name: user.display_name.clone(),
|
2026-10-07 15:30:31 +01:00
|
|
|
country: user.country.clone().unwrap(),
|
|
|
|
|
email: user.email.clone().unwrap(),
|
2026-09-24 14:35:05 +03:00
|
|
|
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<ReplacementLeg>, 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) {
|
2026-10-07 15:30:31 +01:00
|
|
|
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;
|
|
|
|
|
}
|
2026-09-24 14:35:05 +03:00
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
}
|