//! Demonstration lifecycle. Kept in its own test binary because installing the demonstration //! notice is process-wide and disables real provider writes. mod support; use chrono::{Duration, Utc}; use cot::db::query; use meguri::{domain::*, engine, models::SyncJobRow, service}; use uuid::Uuid; #[tokio::test] #[ignore = "requires MEGURI_TEST_DATABASE_URL and CREATEDB for isolated lifecycle tests"] async fn demo_bootstrap_protects_unmarked_and_normal_databases_and_resets_only_application_state() { use cot::db::{Database, Model}; use meguri::{ demo_runtime as runtime, models::{UserRow, WorkspaceRow}, store::Store, }; let admin = Database::new(std::env::var("MEGURI_TEST_DATABASE_URL").unwrap()) .await .unwrap(); let name = format!("meguri_demo_test_{}", Uuid::new_v4().simple()); admin.raw(&format!("CREATE DATABASE {name}")).await.unwrap(); let mut url = reqwest::Url::parse(&std::env::var("MEGURI_TEST_DATABASE_URL").unwrap()).unwrap(); url.set_path(&format!("/{name}")); let source_url = runtime::application_url(url.as_str(), false).unwrap(); let source = Store::connect(&source_url).await.unwrap(); support::install_demo_notice(); let owner = meguri::identity::new_user_row("protected-owner".into(), "Keep me".into(), None); owner.clone().insert(&source.db).await.unwrap(); let tenant = meguri::identity::ensure_workspace( &source, &support::unique_workspace("Protected workspace"), ) .await .unwrap(); assert!(!runtime::is_demo_database(&source.db).await); assert!(runtime::reset(&source.db).await.is_err()); let alternate_name = format!("meguri_demo_test_{}", Uuid::new_v4().simple()); url.set_path(&format!("/{alternate_name}")); let (selected, isolated) = runtime::select_database(&source_url, Some(url.as_str())) .await .unwrap(); assert!(isolated); assert_ne!( reqwest::Url::parse(&selected).unwrap().path(), reqwest::Url::parse(&source_url).unwrap().path() ); let demo = Store::connect(&selected).await.unwrap(); assert!(runtime::is_demo_database(&demo.db).await); meguri::demo::seed(&demo).await.unwrap(); assert_eq!(UserRow::objects().all(&demo.db).await.unwrap().len(), 6); assert_eq!( WorkspaceRow::objects().all(&demo.db).await.unwrap().len(), 4 ); let old_user = query!(UserRow,$username=="alice") .get(&demo.db) .await .unwrap() .unwrap(); demo.db.raw("DO $$ BEGIN CREATE TABLE unrelated_sentinel(value text); INSERT INTO unrelated_sentinel VALUES('keep'); END $$").await.unwrap(); runtime::reset(&demo.db).await.unwrap(); assert!(UserRow::objects().all(&demo.db).await.unwrap().is_empty()); assert!(runtime::is_demo_database(&demo.db).await); demo.db.raw("DO $$ BEGIN IF NOT EXISTS(SELECT 1 FROM unrelated_sentinel WHERE value='keep') THEN RAISE EXCEPTION 'unrelated data lost'; END IF; END $$").await.unwrap(); let rerun = Store::connect(&selected).await.unwrap(); meguri::demo::seed(&rerun).await.unwrap(); assert_ne!( query!(UserRow,$username=="alice") .get(&rerun.db) .await .unwrap() .unwrap() .id, old_user.id ); runtime::mark_normal(&source.db).await.unwrap(); assert!(!runtime::is_demo_database(&source.db).await); assert!(runtime::reset(&source.db).await.is_err()); assert!( query!(WorkspaceRow,$id==tenant.id.to_string()) .exists(&source.db) .await .unwrap() ); assert!( runtime::cleanup( &source, Utc::now() + Duration::days(100), &runtime::Retention::default() ) .await .is_err() ); assert!( query!(UserRow,$id==owner.id.clone()) .exists(&source.db) .await .unwrap() ); // Names are generated above exclusively for this test; never drop the configured DB. admin .raw(&format!("DROP DATABASE {alternate_name} WITH (FORCE)")) .await .unwrap(); admin .raw(&format!("DROP DATABASE {name} WITH (FORCE)")) .await .unwrap(); } #[tokio::test] #[ignore = "requires MEGURI_TEST_DATABASE_URL and CREATEDB for isolated lifecycle tests"] async fn demo_retention_soft_deletes_then_purges_by_creation_and_reseeds_examples() { use cot::db::{Database, Model}; use meguri::{ demo_runtime as runtime, models::{UserRow, WorkspaceRow}, store::Store, }; let admin = Database::new(std::env::var("MEGURI_TEST_DATABASE_URL").unwrap()) .await .unwrap(); let name = format!("meguri_demo_test_{}", Uuid::new_v4().simple()); admin.raw(&format!("CREATE DATABASE {name}")).await.unwrap(); let mut url = reqwest::Url::parse(&std::env::var("MEGURI_TEST_DATABASE_URL").unwrap()).unwrap(); url.set_path(&format!("/{name}")); let url = runtime::application_url(url.as_str(), true).unwrap(); let raw = Database::new(&url).await.unwrap(); runtime::claim_empty(&raw).await.unwrap(); let db = Store::connect(&url).await.unwrap(); support::install_demo_notice(); meguri::demo::seed(&db).await.unwrap(); let alice = query!(UserRow,$username=="alice") .get(&db.db) .await .unwrap() .unwrap(); let alice_id = Uuid::parse_str(&alice.id).unwrap(); // A temporary workspace and account next to the permanent examples, both granted through // the ordinary group mapping. let mut config = meguri::demo::config().unwrap(); let temporary = support::unique_workspace("Temporary team"); config.workspaces.push(temporary.clone()); meguri::identity::ensure_workspace(&db, &temporary) .await .unwrap(); let visitor = support::account(&db, &config, "Temporary visitor", &temporary.admin_groups).await; let mut alice_groups = meguri::identity::stored_groups(&alice).unwrap(); alice_groups.extend(temporary.member_groups.clone()); meguri::identity::apply_access(&db, &config, alice_id, Some(alice_groups)) .await .unwrap(); let mut team = meguri::identity::find_workspace(&db, &temporary.key) .await .unwrap() .unwrap(); let tenant = team.clone(); let original_revision = team.revision; // Editing does not extend lifetime: created_at is the expiration anchor. let mut doc = team.document.clone(); doc.name = "Edited temporary team".into(); team = service::update_document(&db, visitor.id, team.id, team.revision, doc) .await .unwrap(); assert!(team.revision > original_revision); let now = query!(WorkspaceRow,$id==team.id.to_string()) .get(&db.db) .await .unwrap() .unwrap() .created_at; let schedule = Uuid::new_v4(); let mut document = team.document.clone(); document.schedules.push(Schedule { id: schedule, name: "Temporary rotation".into(), handover: default_handover(), kind: ScheduleKind::Rotation { participants: vec![visitor.id], anchor: now.date_naive(), shift_days: 7, }, }); team = service::update_document(&db, visitor.id, team.id, team.revision, document) .await .unwrap(); let start = engine::handover(&team.document, now.date_naive() + Duration::days(2)).unwrap(); team = service::request_replacement( &db, visitor.id, team.id, service::NewRequest { remaining: false, revision: team.revision, reason: "Demo coverage".into(), force: false, legs: vec![ReplacementLeg { schedule_id: schedule, start, end: start + Duration::hours(1), from_user: visitor.id, to_user: alice_id, }], }, ) .await .unwrap(); let ttl = runtime::Retention { workspaces: 120, accounts: 300, requests: 60, events: 180, purge_grace: 30, }; runtime::cleanup(&db, now + Duration::seconds(61), &ttl) .await .unwrap(); let after = db.workspace(team.id).await.unwrap(); assert!(after.document.requests.is_empty()); assert_eq!(after.revision, team.revision + 1); assert_eq!( query!(SyncJobRow,$workspace_id==team.id.to_string()) .get(&db.db) .await .unwrap() .unwrap() .requested_revision, after.revision ); runtime::cleanup(&db, now + Duration::seconds(119), &ttl) .await .unwrap(); assert!(db.workspace(team.id).await.is_ok()); runtime::cleanup(&db, now + Duration::seconds(121), &ttl) .await .unwrap(); assert!(db.workspace(team.id).await.is_err()); assert!( query!(WorkspaceRow,$id==team.id.to_string()) .get(&db.db) .await .unwrap() .unwrap() .demo_deleted_at .is_some() ); assert!( service::list_workspaces(&db, visitor.id).await.unwrap()["workspaces"] .as_array() .unwrap() .is_empty() ); assert!( service::update_document( &db, visitor.id, team.id, team.revision, team.document.clone() ) .await .is_err() ); runtime::cleanup(&db, now + Duration::seconds(152), &ttl) .await .unwrap(); assert!( !query!(WorkspaceRow,$id==team.id.to_string()) .exists(&db.db) .await .unwrap() ); runtime::cleanup(&db, now + Duration::seconds(242), &ttl) .await .unwrap(); assert!( service::list_workspaces(&db, alice_id).await.unwrap()["workspaces"] .as_array() .unwrap() .is_empty() ); runtime::cleanup(&db, now + Duration::seconds(301), &ttl) .await .unwrap(); assert!( service::login( &db, true, visitor.username.clone(), "test-only-password".into() ) .await .is_err() ); runtime::cleanup(&db, now + Duration::seconds(332), &ttl) .await .unwrap(); assert!( !query!(UserRow,$id==visitor.id.to_string()) .exists(&db.db) .await .unwrap() ); assert!( !query!(WorkspaceRow,$id==tenant.id.to_string()) .exists(&db.db) .await .unwrap() ); assert_eq!(UserRow::objects().all(&db.db).await.unwrap().len(), 6); meguri::demo::seed(&db).await.unwrap(); assert_eq!( service::list_workspaces(&db, alice_id).await.unwrap()["workspaces"] .as_array() .unwrap() .len(), 4 ); assert_eq!(WorkspaceRow::objects().all(&db.db).await.unwrap().len(), 4); admin .raw(&format!("DROP DATABASE {name} WITH (FORCE)")) .await .unwrap(); }