diff --git a/nexus/src/app/sagas/disk_create.rs b/nexus/src/app/sagas/disk_create.rs index 57d666041b7..9a678ec5f75 100644 --- a/nexus/src/app/sagas/disk_create.rs +++ b/nexus/src/app/sagas/disk_create.rs @@ -617,21 +617,25 @@ pub(crate) mod test { project.identity.id } + pub fn new_disk_create_params() -> params::DiskCreate { + params::DiskCreate { + identity: IdentityMetadataCreateParams { + name: DISK_NAME.parse().expect("Invalid disk name"), + description: "My disk".to_string(), + }, + disk_source: params::DiskSource::Blank { + block_size: params::BlockSize(512), + }, + size: ByteCount::from_gibibytes_u32(1), + } + } + // Helper for creating disk create parameters fn new_test_params(opctx: &OpContext, project_id: Uuid) -> Params { Params { serialized_authn: Serialized::for_opctx(opctx), project_id, - create_params: params::DiskCreate { - identity: IdentityMetadataCreateParams { - name: DISK_NAME.parse().expect("Invalid disk name"), - description: "My disk".to_string(), - }, - disk_source: params::DiskSource::Blank { - block_size: params::BlockSize(512), - }, - size: ByteCount::from_gibibytes_u32(1), - }, + create_params: new_disk_create_params(), } } diff --git a/nexus/src/app/sagas/disk_delete.rs b/nexus/src/app/sagas/disk_delete.rs index c26f91f4299..e0c78e94dd5 100644 --- a/nexus/src/app/sagas/disk_delete.rs +++ b/nexus/src/app/sagas/disk_delete.rs @@ -83,3 +83,127 @@ async fn sdd_delete_volume( .map_err(ActionError::action_failed)?; Ok(()) } + +#[cfg(test)] +pub(crate) mod test { + use crate::{ + app::saga::create_saga_dag, app::sagas::disk_delete::Params, + app::sagas::disk_delete::SagaDiskDelete, context::OpContext, db, + }; + use dropshot::test_util::ClientTestContext; + use nexus_test_utils::resource_helpers::create_ip_pool; + use nexus_test_utils::resource_helpers::create_organization; + use nexus_test_utils::resource_helpers::create_project; + use nexus_test_utils::resource_helpers::DiskTest; + use nexus_test_utils_macros::nexus_test; + use omicron_common::api::external::Name; + use ref_cast::RefCast; + use std::num::NonZeroU32; + use uuid::Uuid; + + type ControlPlaneTestContext = + nexus_test_utils::ControlPlaneTestContext; + + const ORG_NAME: &str = "test-org"; + const PROJECT_NAME: &str = "springfield-squidport"; + + async fn create_org_and_project(client: &ClientTestContext) -> Uuid { + create_ip_pool(&client, "p0", None).await; + create_organization(&client, ORG_NAME).await; + let project = create_project(client, ORG_NAME, PROJECT_NAME).await; + project.identity.id + } + + pub fn test_opctx(cptestctx: &ControlPlaneTestContext) -> OpContext { + OpContext::for_tests( + cptestctx.logctx.log.new(o!()), + cptestctx.server.apictx.nexus.datastore().clone(), + ) + } + + async fn create_disk(cptestctx: &ControlPlaneTestContext) -> Uuid { + let nexus = &cptestctx.server.apictx.nexus; + let opctx = test_opctx(&cptestctx); + + nexus + .project_create_disk( + &opctx, + db::model::Name::ref_cast( + &Name::try_from(ORG_NAME.to_string()).unwrap(), + ), + db::model::Name::ref_cast( + &Name::try_from(PROJECT_NAME.to_string()).unwrap(), + ), + &crate::app::sagas::disk_create::test::new_disk_create_params(), + ) + .await + .expect("Failed to create disk") + .id() + } + + #[nexus_test(server = crate::Server)] + async fn test_saga_basic_usage_succeeds( + cptestctx: &ControlPlaneTestContext, + ) { + DiskTest::new(cptestctx).await; + + let client = &cptestctx.external_client; + let nexus = &cptestctx.server.apictx.nexus; + create_org_and_project(&client).await; + let disk_id = create_disk(&cptestctx).await; + + // Build the saga DAG with the provided test parameters + let params = Params { disk_id }; + let dag = create_saga_dag::(params).unwrap(); + let runnable_saga = nexus.create_runnable_saga(dag).await.unwrap(); + + // Actually run the saga + nexus.run_saga(runnable_saga).await.unwrap(); + } + + #[nexus_test(server = crate::Server)] + async fn test_actions_succeed_idempotently( + cptestctx: &ControlPlaneTestContext, + ) { + let test = DiskTest::new(cptestctx).await; + + let client = &cptestctx.external_client; + let nexus = &cptestctx.server.apictx.nexus; + create_org_and_project(&client).await; + let disk_id = create_disk(&cptestctx).await; + + // Build the saga DAG with the provided test parameters + let params = Params { disk_id }; + let dag = create_saga_dag::(params).unwrap(); + + let runnable_saga = + nexus.create_runnable_saga(dag.clone()).await.unwrap(); + + // Cause all actions to run twice. The saga should succeed regardless! + for node in dag.get_nodes() { + nexus + .sec() + .saga_inject_repeat( + runnable_saga.id(), + node.index(), + steno::RepeatInjected { + action: NonZeroU32::new(2).unwrap(), + undo: NonZeroU32::new(1).unwrap(), + }, + ) + .await + .unwrap(); + } + + // Verify that the saga's execution succeeded. + nexus + .run_saga(runnable_saga) + .await + .expect("Saga should have succeeded"); + + crate::app::sagas::disk_create::test::verify_clean_slate( + &cptestctx, &test, + ) + .await; + } +}