#![cfg(any(test, feature = "test-support"))]
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use chrono::Duration;
use ed25519_dalek::SigningKey;
use ed25519_dalek_bip32::{DerivationPath, ExtendedSigningKey};
use serde_json::Value;
use tokio::sync::RwLock;
use affinidi_did_resolver_cache_sdk::{DIDCacheClient, config::DIDCacheConfigBuilder};
use crate::acl::Role;
use crate::auth::AuthClaims;
use crate::config::{AppConfig, StoreConfig};
use crate::didcomm_bridge::DIDCommBridge;
use crate::keys::seed_store::PlaintextSeedStore;
use crate::keys::{KeyType as SdkKeyType, save_key_record};
use crate::operations::provision_integration::ProvisionIntegrationDeps;
use crate::store::{KeyspaceHandle, Store};
use vta_sdk::did_key::ed25519_multibase_pubkey;
use vta_sdk::provision_integration::{
BootstrapAsk, BootstrapRequest, DidTemplateRef, TemplateBootstrapAsk, VerifiedBootstrapRequest,
};
pub struct TestStore {
_dir: tempfile::TempDir,
_store: Store,
pub contexts_ks: KeyspaceHandle,
pub did_templates_ks: KeyspaceHandle,
pub keys_ks: KeyspaceHandle,
pub acl_ks: KeyspaceHandle,
pub audit_ks: KeyspaceHandle,
pub imported_ks: KeyspaceHandle,
pub webvh_ks: KeyspaceHandle,
pub sealed_nonces_ks: KeyspaceHandle,
pub drains_ks: KeyspaceHandle,
pub snapshot_ks: KeyspaceHandle,
pub service_state_ks: KeyspaceHandle,
pub data_dir: PathBuf,
}
pub async fn open_test_store() -> TestStore {
let dir = tempfile::tempdir().expect("temp dir");
let data_dir = dir.path().to_path_buf();
let store = Store::open(&StoreConfig {
data_dir: data_dir.clone(),
})
.expect("open store");
TestStore {
contexts_ks: store
.keyspace(crate::keyspaces::CONTEXTS)
.expect("contexts ks"),
did_templates_ks: store
.keyspace(crate::keyspaces::DID_TEMPLATES)
.expect("did_templates ks"),
keys_ks: store.keyspace(crate::keyspaces::KEYS).expect("keys ks"),
acl_ks: store.keyspace(crate::keyspaces::ACL).expect("acl ks"),
audit_ks: store.keyspace(crate::keyspaces::AUDIT).expect("audit ks"),
imported_ks: store
.keyspace(crate::keyspaces::IMPORTED_SECRETS)
.expect("imported ks"),
webvh_ks: store.keyspace(crate::keyspaces::WEBVH).expect("webvh ks"),
sealed_nonces_ks: store
.keyspace(crate::keyspaces::SEALED_NONCES)
.expect("nonces ks"),
drains_ks: store.keyspace(crate::keyspaces::DRAINS).expect("drains ks"),
snapshot_ks: store
.keyspace(crate::operations::protocol::snapshot::KEYSPACE_NAME)
.expect("snapshot ks"),
service_state_ks: store
.keyspace(crate::keyspaces::SERVICE_STATE)
.expect("service_state ks"),
_dir: dir,
_store: store,
data_dir,
}
}
pub fn test_app_config(data_dir: PathBuf) -> AppConfig {
AppConfig {
trusted_presentation_verifiers: Vec::new(),
credential_holder_did: None,
vta_did: None,
vta_name: None,
public_url: None,
resolver_url: None,
server: Default::default(),
log: Default::default(),
store: StoreConfig { data_dir },
messaging: None,
services: Default::default(),
auth: Default::default(),
audit: Default::default(),
vault: Default::default(),
secrets: Default::default(),
#[cfg(feature = "tee")]
tee: Default::default(),
config_path: PathBuf::new(),
unknown_keys: Vec::new(),
}
}
pub fn test_deps(ts: &TestStore) -> ProvisionIntegrationDeps {
ProvisionIntegrationDeps {
keys_ks: ts.keys_ks.clone(),
acl_ks: ts.acl_ks.clone(),
audit_ks: ts.audit_ks.clone(),
contexts_ks: ts.contexts_ks.clone(),
did_templates_ks: ts.did_templates_ks.clone(),
imported_ks: ts.imported_ks.clone(),
webvh_ks: ts.webvh_ks.clone(),
sealed_nonces_ks: ts.sealed_nonces_ks.clone(),
seed_store: Arc::new(PlaintextSeedStore::new(&ts.data_dir)),
config: Arc::new(RwLock::new(test_app_config(ts.data_dir.clone()))),
did_resolver: None,
didcomm_bridge: Arc::new(DIDCommBridge::placeholder()),
}
}
pub fn super_admin_claims() -> AuthClaims {
AuthClaims {
did: "did:key:zTestAdmin".into(),
role: Role::Admin,
allowed_contexts: Vec::new(),
session_id: "test-session".into(),
access_expires_at: 0,
amr: Vec::new(),
acr: String::new(),
}
}
pub async fn signed_request(template_name: &str, context_hint: &str) -> VerifiedBootstrapRequest {
signed_request_with_vars(template_name, context_hint, BTreeMap::new()).await
}
pub async fn signed_request_with_vars(
template_name: &str,
context_hint: &str,
vars: BTreeMap<String, Value>,
) -> VerifiedBootstrapRequest {
let seed = [7u8; 32];
let signing = SigningKey::from_bytes(&seed);
let pub_bytes: [u8; 32] = signing.verifying_key().to_bytes();
let client_did = affinidi_crypto::did_key::ed25519_pub_to_did_key(&pub_bytes);
let ask = BootstrapAsk::TemplateBootstrap(TemplateBootstrapAsk {
context_hint: Some(context_hint.into()),
template: DidTemplateRef {
name: template_name.into(),
vars,
},
admin_template: None,
note: None,
});
let req = BootstrapRequest::sign(
&seed,
&client_did,
[0u8; 16],
Duration::minutes(10),
None,
ask,
)
.await
.expect("sign bootstrap request");
req.verify().expect("verify bootstrap request")
}
pub async fn signed_admin_rotation_request(
admin_template_name: &str,
context_hint: &str,
) -> VerifiedBootstrapRequest {
use vta_sdk::provision_integration::AdminRotationAsk;
let seed = [7u8; 32];
let signing = SigningKey::from_bytes(&seed);
let pub_bytes: [u8; 32] = signing.verifying_key().to_bytes();
let client_did = affinidi_crypto::did_key::ed25519_pub_to_did_key(&pub_bytes);
let ask = BootstrapAsk::AdminRotation(AdminRotationAsk {
context_hint: Some(context_hint.into()),
admin_template: DidTemplateRef {
name: admin_template_name.into(),
vars: BTreeMap::new(),
},
note: None,
});
let req = BootstrapRequest::sign(
&seed,
&client_did,
[0u8; 16],
Duration::minutes(10),
None,
ask,
)
.await
.expect("sign bootstrap request");
req.verify().expect("verify bootstrap request")
}
async fn provision_vta_signing_identity(
keys_ks: &KeyspaceHandle,
data_dir: &std::path::Path,
) -> (String, Arc<PlaintextSeedStore>) {
use crate::keys::seeds::{SeedRecord, save_seed_record, set_active_seed_id};
let raw_seed = [0xC5u8; 64];
let seed_store = PlaintextSeedStore::new(data_dir);
crate::keys::seed_store::SeedStore::set(&seed_store, &raw_seed)
.await
.expect("write test seed to plaintext store");
let now = chrono::Utc::now();
save_seed_record(
keys_ks,
&SeedRecord {
id: 0,
seed_hex: None,
seed_enc: None,
created_at: now,
retired_at: None,
},
)
.await
.expect("save seed record");
set_active_seed_id(keys_ks, 0)
.await
.expect("set active seed id");
let vta_base_path = "m/26'/1'/0'";
let root = ExtendedSigningKey::from_seed(&raw_seed).expect("bip-32 root");
let dp: DerivationPath = vta_base_path.parse().expect("derivation path");
let derived = root.derive(&dp).expect("derive VTA key");
let signing = ed25519_dalek::SigningKey::from_bytes(derived.signing_key.as_bytes());
let pub_bytes = signing.verifying_key().to_bytes();
let multibase = ed25519_multibase_pubkey(&pub_bytes);
let vta_did = format!("did:key:{multibase}");
let key_id = format!("{vta_did}#key-0");
save_key_record(
keys_ks,
&key_id,
vta_base_path,
SdkKeyType::Ed25519,
&multibase,
"VTA signing key",
None,
Some(0),
)
.await
.expect("save VTA key record");
let st_base_path = "m/26'/1'/1'";
let st_dp: DerivationPath = st_base_path.parse().expect("st derivation path");
let st_derived = root.derive(&st_dp).expect("derive VTA sealed-transfer key");
let st_signing = ed25519_dalek::SigningKey::from_bytes(st_derived.signing_key.as_bytes());
let st_pub_bytes = st_signing.verifying_key().to_bytes();
let st_multibase = ed25519_multibase_pubkey(&st_pub_bytes);
save_key_record(
keys_ks,
&format!("{vta_did}#sealed-transfer-0"),
st_base_path,
SdkKeyType::Ed25519,
&st_multibase,
"VTA sealed-transfer producer-assertion key",
None,
Some(0),
)
.await
.expect("save VTA sealed-transfer key record");
(vta_did, Arc::new(PlaintextSeedStore::new(data_dir)))
}
pub async fn bootstrap_test_vta(ts: &TestStore) -> (String, ProvisionIntegrationDeps) {
let (vta_did, _seed_store) = provision_vta_signing_identity(&ts.keys_ks, &ts.data_dir).await;
let mut config = test_app_config(ts.data_dir.clone());
config.vta_did = Some(vta_did.clone());
config.public_url = Some("https://vta.test".into());
let resolver = DIDCacheClient::new(DIDCacheConfigBuilder::default().build())
.await
.expect("DID resolver");
let deps = ProvisionIntegrationDeps {
keys_ks: ts.keys_ks.clone(),
acl_ks: ts.acl_ks.clone(),
audit_ks: ts.audit_ks.clone(),
contexts_ks: ts.contexts_ks.clone(),
did_templates_ks: ts.did_templates_ks.clone(),
imported_ks: ts.imported_ks.clone(),
webvh_ks: ts.webvh_ks.clone(),
sealed_nonces_ks: ts.sealed_nonces_ks.clone(),
seed_store: Arc::new(PlaintextSeedStore::new(&ts.data_dir)),
config: Arc::new(RwLock::new(config)),
did_resolver: Some(resolver),
didcomm_bridge: Arc::new(DIDCommBridge::placeholder()),
};
(vta_did, deps)
}
pub const PROVISIONABLE_CONTEXT: &str = "provisionable-ctx";
pub async fn bootstrap_provisionable_test_vta(
ts: &TestStore,
) -> (String, ProvisionIntegrationDeps) {
let (vta_did, deps) = bootstrap_test_vta(ts).await;
crate::contexts::create_context(
&ts.contexts_ks,
PROVISIONABLE_CONTEXT,
"Provisionable Context",
)
.await
.expect("create provisionable context");
(vta_did, deps)
}
pub fn provisionable_mediator_vars() -> BTreeMap<String, Value> {
let mut vars = BTreeMap::new();
vars.insert("URL".into(), Value::String("https://mediator.test".into()));
vars.insert(
"WS_URL".into(),
Value::String("wss://mediator.test/ws".into()),
);
vars.insert("ROUTING_KEYS".into(), Value::Array(vec![]));
vars
}
pub fn init_jwt_provider() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
let _ = jsonwebtoken::crypto::aws_lc::DEFAULT_PROVIDER.install_default();
});
}
pub struct TestSeedStore(pub Vec<u8>);
impl crate::keys::seed_store::SeedStore for TestSeedStore {
fn get(
&self,
) -> std::pin::Pin<
Box<
dyn std::future::Future<Output = Result<Option<Vec<u8>>, crate::error::AppError>>
+ Send
+ '_,
>,
> {
let v = self.0.clone();
Box::pin(async move { Ok(Some(v)) })
}
fn set(
&self,
_seed: &[u8],
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<(), crate::error::AppError>> + Send + '_>,
> {
Box::pin(async { Ok(()) })
}
}
pub struct TestAppContext {
pub jwt_keys: Arc<vti_common::auth::jwt::JwtKeys>,
pub sessions_ks: KeyspaceHandle,
pub acl_ks: KeyspaceHandle,
pub keys_ks: KeyspaceHandle,
pub vault_ks: KeyspaceHandle,
pub backup_bundles_ks: KeyspaceHandle,
pub backup_blob_dir: std::path::PathBuf,
#[cfg(feature = "webvh")]
pub webvh_ks: KeyspaceHandle,
pub vta_did: String,
pub config: Arc<RwLock<AppConfig>>,
pub _dir: tempfile::TempDir,
}
impl TestAppContext {
pub async fn mint_token(&self, did: &str, role: &str, contexts: Vec<String>) -> String {
use vti_common::auth::session::{Session, SessionState, store_session};
let session_id = format!("sess-{}", uuid::Uuid::new_v4());
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let session = Session {
session_id: session_id.clone(),
did: did.to_string(),
challenge: String::new(),
state: SessionState::Authenticated,
created_at: now,
refresh_token: None,
refresh_expires_at: None,
tee_attested: false,
amr: Vec::new(),
acr: String::new(),
token_id: None,
session_pubkey_b58btc: None,
};
store_session(&self.sessions_ks, &session)
.await
.expect("store session");
let claims = self.jwt_keys.new_claims(
did.to_string(),
session_id,
role.to_string(),
contexts,
900,
false,
);
self.jwt_keys.encode(&claims).expect("encode jwt")
}
}
#[derive(Default)]
pub struct TestAppOptions {
pub provisionable_vta: bool,
pub preseed_did_docs: Vec<(String, serde_json::Value)>,
#[cfg(feature = "webvh")]
pub webvh_servers: Vec<(String, String)>,
}
pub async fn build_test_app() -> (axum::Router, TestAppContext) {
build_test_app_with(TestAppOptions::default()).await
}
pub async fn build_provisionable_test_app() -> (axum::Router, TestAppContext) {
build_test_app_with(TestAppOptions {
provisionable_vta: true,
..Default::default()
})
.await
}
pub async fn build_test_app_with(opts: TestAppOptions) -> (axum::Router, TestAppContext) {
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD as BASE64;
use tokio::sync::watch;
init_jwt_provider();
let dir = tempfile::tempdir().expect("temp dir");
let store_config = StoreConfig {
data_dir: dir.path().to_path_buf(),
};
let store = Store::open(&store_config).expect("open store");
let keys_ks = store.keyspace(crate::keyspaces::KEYS).unwrap();
let sessions_ks = store.keyspace(crate::keyspaces::SESSIONS).unwrap();
let acl_ks = store.keyspace(crate::keyspaces::ACL).unwrap();
let contexts_ks = store.keyspace(crate::keyspaces::CONTEXTS).unwrap();
{
use chrono::Utc;
let now = Utc::now();
crate::contexts::store_context(
&contexts_ks,
&crate::contexts::ContextRecord {
id: "ctx1".into(),
name: "ctx1".into(),
did: None,
description: None,
parent: None,
base_path: "m/26'/2'/0'".into(),
index: 0,
created_at: now,
updated_at: now,
},
)
.await
.expect("seed ctx1");
}
let audit_ks = store.keyspace(crate::keyspaces::AUDIT).unwrap();
let cache_ks = store.keyspace(crate::keyspaces::CACHE).unwrap();
let vault_ks = store.keyspace(crate::keyspaces::VAULT).unwrap();
let vault_ks_ctx = vault_ks.clone();
let service_state_ks = store.keyspace(crate::keyspaces::SERVICE_STATE).unwrap();
let imported_ks = store.keyspace(crate::keyspaces::IMPORTED_SECRETS).unwrap();
let sealed_nonces_ks = store.keyspace(crate::keyspaces::SEALED_NONCES).unwrap();
let backup_bundles_ks = store.keyspace(crate::keyspaces::BACKUP_BUNDLES).unwrap();
let backup_blob_dir = dir.path().join("backups");
let did_templates_ks = store.keyspace(crate::keyspaces::DID_TEMPLATES).unwrap();
#[cfg(feature = "webvh")]
let webvh_ks = store.keyspace(crate::keyspaces::WEBVH).unwrap();
#[cfg(feature = "webvh")]
for (id, did) in &opts.webvh_servers {
seed_webvh_server(&webvh_ks, id, did).await;
}
#[cfg(feature = "webvh")]
let passkey_vms_ks = store.keyspace(crate::keyspaces::PASSKEY_VMS).unwrap();
#[cfg(feature = "webvh")]
let drains_ks = store.keyspace(crate::keyspaces::DRAINS).unwrap();
#[cfg(feature = "webvh")]
let snapshot_ks = store
.keyspace(crate::operations::protocol::snapshot::KEYSPACE_NAME)
.unwrap();
let jwt_seed = [0x42u8; 32];
let jwt_keys = Arc::new(
vti_common::auth::jwt::JwtKeys::from_ed25519_bytes(&jwt_seed, "VTA").expect("jwt keys"),
);
let (vta_did, seed_store): (String, Arc<dyn crate::keys::seed_store::SeedStore>) =
if opts.provisionable_vta {
let (did, ps) = provision_vta_signing_identity(&keys_ks, dir.path()).await;
let store: Arc<dyn crate::keys::seed_store::SeedStore> = ps;
(did, store)
} else {
let store: Arc<dyn crate::keys::seed_store::SeedStore> =
Arc::new(TestSeedStore(vec![0xABu8; 32]));
("did:key:z6MkTestVTA".to_string(), store)
};
let mut config: AppConfig = toml::from_str(&format!(
r#"
vta_did = "{vta_did}"
[store]
data_dir = "{}"
[auth]
jwt_signing_key = "{}"
"#,
dir.path().display(),
BASE64.encode(jwt_seed),
))
.expect("parse config");
config.config_path = dir.path().join("config.toml");
let (restart_tx, _rx) = watch::channel(false);
let telemetry: vti_common::telemetry::SharedTelemetrySink =
Arc::new(vti_common::telemetry::RingBufferTelemetry::new());
#[cfg(feature = "webvh")]
let mediator_registry = Arc::new(crate::messaging::registry::MediatorListenerRegistry::new(
Arc::clone(&telemetry),
));
#[cfg(feature = "webvh")]
let drain_sweeper = {
let (tx, _rx) = crate::messaging::drain_sweeper::teardown_channel(8);
Arc::new(crate::messaging::drain_sweeper::DrainSweeper::new(
Arc::clone(&mediator_registry),
drains_ks.clone(),
tx,
))
};
let config = Arc::new(RwLock::new(config));
let did_resolver = {
let mut resolver = DIDCacheClient::new(DIDCacheConfigBuilder::default().build())
.await
.ok();
if let Some(client) = resolver.as_mut() {
for (did, doc_json) in &opts.preseed_did_docs {
let doc = serde_json::from_value(doc_json.clone())
.expect("preseed DID document must deserialize into a resolver Document");
client.add_did_document(did, doc).await;
}
}
resolver
};
let state = crate::server::AppState {
keys_ks: keys_ks.clone(),
sessions_ks: sessions_ks.clone(),
acl_ks: acl_ks.clone(),
contexts_ks,
did_templates_ks,
audit_ks,
imported_ks,
cache_ks,
vault_ks,
consent_ks: store.keyspace(crate::keyspaces::CONSENT).unwrap(),
consent_approvers_ks: store.keyspace(crate::keyspaces::CONSENT_APPROVERS).unwrap(),
service_state_ks,
sealed_nonces_ks,
backup_bundles_ks: backup_bundles_ks.clone(),
backup_blob_dir: backup_blob_dir.clone(),
#[cfg(feature = "webvh")]
webvh_ks: webvh_ks.clone(),
#[cfg(feature = "webvh")]
passkey_vms_ks,
#[cfg(feature = "webvh")]
drains_ks,
#[cfg(feature = "webvh")]
snapshot_ks,
#[cfg(feature = "webvh")]
mediator_registry,
#[cfg(feature = "webvh")]
drain_sweeper,
#[cfg(feature = "webvh")]
webvh_auth_locks: crate::operations::did_webvh::WebvhAuthLocks::new(),
telemetry,
wrapping_cache: crate::keys::wrapping::WrappingKeyCache::new(),
config: config.clone(),
seed_store,
did_resolver,
status_list_resolver: None,
secrets_resolver: None,
#[cfg(feature = "didcomm")]
signing_vm_id: None,
#[cfg(feature = "didcomm")]
ka_vm_id: None,
#[cfg(feature = "didcomm")]
didcomm_bridge: Arc::new(DIDCommBridge::placeholder()),
#[cfg(feature = "didcomm")]
didcomm_websocket_status: Arc::new(tokio::sync::RwLock::new(
crate::server::DidcommWebsocketStatus::Disconnected,
)),
jwt_keys: Some(jwt_keys.clone()),
atm: None,
tee: None,
restart_tx,
metrics_handle: None,
};
let router = crate::routes::router_with_cors(&[], true)
.with_state(state.clone())
.merge(crate::routes::health_router().with_state(state));
let ctx = TestAppContext {
jwt_keys,
sessions_ks,
acl_ks,
keys_ks,
vault_ks: vault_ks_ctx,
backup_bundles_ks,
backup_blob_dir,
#[cfg(feature = "webvh")]
webvh_ks,
vta_did,
config,
_dir: dir,
};
(router, ctx)
}
#[cfg(feature = "webvh")]
pub async fn seed_webvh_server(webvh_ks: &KeyspaceHandle, id: &str, server_did: &str) {
use chrono::Utc;
let now = Utc::now();
let record = vta_sdk::webvh::WebvhServerRecord {
id: id.to_string(),
did: server_did.to_string(),
label: Some(format!("test server {id}")),
created_at: now,
updated_at: now,
};
crate::webvh_store::store_server(webvh_ks, &record)
.await
.expect("seed webvh server");
}
pub async fn seed_acl_entry(
acl_ks: &KeyspaceHandle,
did: &str,
role: crate::acl::Role,
contexts: Vec<String>,
) {
let entry = crate::acl::AclEntry::new(did, role, "test-support").with_contexts(contexts);
crate::acl::store_acl_entry(acl_ks, &entry)
.await
.expect("seed acl entry");
}
#[cfg(feature = "webvh")]
pub const STUB_WEBVH_DID_URL: &str = "https://webvh-host.test/dids/persona/did.jsonl";
#[cfg(feature = "webvh")]
pub struct StubWebvhHost {
base_url: String,
shutdown: Option<tokio::sync::oneshot::Sender<()>>,
handle: Option<tokio::task::JoinHandle<()>>,
}
#[cfg(feature = "webvh")]
impl StubWebvhHost {
pub async fn start() -> StubWebvhHost {
use axum::routing::{post, put};
use serde_json::json;
async fn tokens() -> axum::Json<serde_json::Value> {
axum::Json(json!({
"sessionId": "stub-session",
"data": {
"accessToken": "stub-access-token",
"accessExpiresAt": 9_999_999_999u64,
"refreshToken": "stub-refresh-token",
"refreshExpiresAt": 9_999_999_999u64,
}
}))
}
let router = axum::Router::new()
.route(
"/api/auth/challenge",
post(|| async {
axum::Json(json!({
"sessionId": "stub-session",
"data": { "challenge": "stub-challenge-0000000000000000" }
}))
}),
)
.route("/api/auth/", post(tokens))
.route("/api/auth/refresh", post(tokens))
.route(
"/api/dids",
post(|| async {
axum::Json(
json!({ "did_url": STUB_WEBVH_DID_URL, "mnemonic": "stub-mnemonic" }),
)
}),
)
.route(
"/api/dids/register",
post(|| async {
axum::Json(
json!({ "did_url": STUB_WEBVH_DID_URL, "mnemonic": "stub-mnemonic" }),
)
}),
)
.route(
"/api/dids/check",
post(|| async { axum::Json(json!({ "available": true })) }),
)
.route(
"/api/dids/{mnemonic}",
put(|| async { axum::http::StatusCode::OK })
.delete(|| async { axum::http::StatusCode::OK }),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind stub webvh host port");
let addr = listener.local_addr().expect("stub host local addr");
let base_url = format!("http://{addr}");
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
let handle = tokio::spawn(async move {
let _ = axum::serve(listener, router)
.with_graceful_shutdown(async move {
let _ = rx.await;
})
.await;
});
StubWebvhHost {
base_url,
shutdown: Some(tx),
handle: Some(handle),
}
}
pub fn base_url(&self) -> &str {
&self.base_url
}
}
#[cfg(feature = "webvh")]
impl Drop for StubWebvhHost {
fn drop(&mut self) {
if let Some(tx) = self.shutdown.take() {
let _ = tx.send(());
}
if let Some(handle) = self.handle.take() {
handle.abort();
}
}
}
pub struct MockVta {
base_url: String,
pub ctx: TestAppContext,
shutdown: Option<tokio::sync::oneshot::Sender<()>>,
handle: Option<tokio::task::JoinHandle<()>>,
#[cfg(feature = "webvh")]
webvh_host: Option<StubWebvhHost>,
}
impl MockVta {
pub async fn start() -> MockVta {
Self::serve(build_test_app().await).await
}
pub async fn start_provisionable() -> MockVta {
Self::serve(build_provisionable_test_app().await).await
}
#[cfg(feature = "webvh")]
pub const WEBVH_SERVER_ID: &'static str = "stub-webvh";
#[cfg(feature = "webvh")]
pub async fn start_with_webvh_host() -> MockVta {
use serde_json::json;
let host = StubWebvhHost::start().await;
let server_did = "did:webvh:stubscid0000000000000000:webvh-host.test".to_string();
let server_doc = json!({
"@context": ["https://www.w3.org/ns/did/v1"],
"id": server_did,
"service": [{
"id": format!("{server_did}#webvh"),
"type": "WebVHHosting",
"serviceEndpoint": host.base_url(),
}]
});
let opts = TestAppOptions {
provisionable_vta: true,
preseed_did_docs: vec![(server_did.clone(), server_doc)],
webvh_servers: vec![(Self::WEBVH_SERVER_ID.to_string(), server_did)],
};
let mut mock = Self::serve(build_test_app_with(opts).await).await;
mock.webvh_host = Some(host);
mock
}
async fn serve((router, ctx): (axum::Router, TestAppContext)) -> MockVta {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral loopback port");
let addr = listener.local_addr().expect("resolve local addr");
let base_url = format!("http://{addr}");
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
let handle = tokio::spawn(async move {
let _ = axum::serve(
listener,
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.with_graceful_shutdown(async move {
let _ = rx.await;
})
.await;
});
MockVta {
base_url,
ctx,
shutdown: Some(tx),
handle: Some(handle),
#[cfg(feature = "webvh")]
webvh_host: None,
}
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn vta_did(&self) -> &str {
&self.ctx.vta_did
}
#[cfg(feature = "webvh")]
pub async fn seed_webvh_server(&self, id: &str, server_did: &str) {
seed_webvh_server(&self.ctx.webvh_ks, id, server_did).await;
}
pub async fn authorize_did(&self, did: &str, role: crate::acl::Role, contexts: Vec<String>) {
seed_acl_entry(&self.ctx.acl_ks, did, role, contexts).await;
}
pub async fn grant_super_admin(&self, did: &str) {
self.authorize_did(did, crate::acl::Role::Admin, Vec::new())
.await;
}
pub async fn shutdown(mut self) {
if let Some(tx) = self.shutdown.take() {
let _ = tx.send(());
}
if let Some(handle) = self.handle.take() {
let _ = handle.await;
}
}
}
impl Drop for MockVta {
fn drop(&mut self) {
if let Some(tx) = self.shutdown.take() {
let _ = tx.send(());
}
if let Some(handle) = self.handle.take() {
handle.abort();
}
}
}