326 lines
11 KiB
Rust
326 lines
11 KiB
Rust
//! 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();
|
||
|
|
}
|