Stabilize integration test qualification
This commit is contained in:
parent
414b380516
commit
560e1df9b2
3 changed files with 68 additions and 180 deletions
|
|
@ -11,6 +11,13 @@ pub fn required_env(name: &str) -> String {
|
|||
std::env::var(name).unwrap_or_else(|_| panic!("{name} is required"))
|
||||
}
|
||||
|
||||
fn keycloak_admin_url() -> String {
|
||||
std::env::var("REMORA_KEYCLOAK_ADMIN_URL")
|
||||
.unwrap_or_else(|_| "http://keycloak.remora.test:8080".to_owned())
|
||||
.trim_end_matches('/')
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub fn oidc_provider() -> OidcIdentityProvider {
|
||||
let provider = required_env("REMORA_OIDC_PROVIDER");
|
||||
let audience = if provider == "zitadel" {
|
||||
|
|
@ -179,9 +186,12 @@ async fn revoke_keycloak_remora_roles() -> Result<(), String> {
|
|||
let realm = required_env("REMORA_OIDC_TENANT");
|
||||
let client_id = required_env("REMORA_OIDC_CLIENT_ID");
|
||||
let username = required_env("REMORA_OIDC_USERNAME");
|
||||
let keycloak_url = keycloak_admin_url();
|
||||
let client = Client::new();
|
||||
let admin_response = client
|
||||
.post("http://keycloak.remora.test:8080/realms/master/protocol/openid-connect/token")
|
||||
.post(format!(
|
||||
"{keycloak_url}/realms/master/protocol/openid-connect/token"
|
||||
))
|
||||
.form(&[
|
||||
("grant_type", "password"),
|
||||
("client_id", "admin-cli"),
|
||||
|
|
@ -200,7 +210,7 @@ async fn revoke_keycloak_remora_roles() -> Result<(), String> {
|
|||
.ok_or_else(|| "keycloak admin token missing".to_owned())?;
|
||||
let users: Value = client
|
||||
.get(format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/users?username={username}&exact=true"
|
||||
"{keycloak_url}/admin/realms/{realm}/users?username={username}&exact=true"
|
||||
))
|
||||
.bearer_auth(admin_token)
|
||||
.send()
|
||||
|
|
@ -216,7 +226,7 @@ async fn revoke_keycloak_remora_roles() -> Result<(), String> {
|
|||
.ok_or_else(|| "keycloak user missing".to_owned())?;
|
||||
let clients: Value = client
|
||||
.get(format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/clients?clientId={client_id}"
|
||||
"{keycloak_url}/admin/realms/{realm}/clients?clientId={client_id}"
|
||||
))
|
||||
.bearer_auth(admin_token)
|
||||
.send()
|
||||
|
|
@ -231,7 +241,7 @@ async fn revoke_keycloak_remora_roles() -> Result<(), String> {
|
|||
.and_then(|row| row["id"].as_str())
|
||||
.ok_or_else(|| "keycloak client missing".to_owned())?;
|
||||
let roles_url = format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/users/{user_id}/role-mappings/clients/{client_uuid}"
|
||||
"{keycloak_url}/admin/realms/{realm}/users/{user_id}/role-mappings/clients/{client_uuid}"
|
||||
);
|
||||
let mapped: Value = client
|
||||
.get(&roles_url)
|
||||
|
|
@ -274,9 +284,12 @@ async fn restore_keycloak_remora_roles() -> Result<(), String> {
|
|||
let realm = required_env("REMORA_OIDC_TENANT");
|
||||
let client_id = required_env("REMORA_OIDC_CLIENT_ID");
|
||||
let username = required_env("REMORA_OIDC_USERNAME");
|
||||
let keycloak_url = keycloak_admin_url();
|
||||
let client = Client::new();
|
||||
let admin_response = client
|
||||
.post("http://keycloak.remora.test:8080/realms/master/protocol/openid-connect/token")
|
||||
.post(format!(
|
||||
"{keycloak_url}/realms/master/protocol/openid-connect/token"
|
||||
))
|
||||
.form(&[
|
||||
("grant_type", "password"),
|
||||
("client_id", "admin-cli"),
|
||||
|
|
@ -295,7 +308,7 @@ async fn restore_keycloak_remora_roles() -> Result<(), String> {
|
|||
.ok_or_else(|| "keycloak admin token missing".to_owned())?;
|
||||
let users: Value = client
|
||||
.get(format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/users?username={username}&exact=true"
|
||||
"{keycloak_url}/admin/realms/{realm}/users?username={username}&exact=true"
|
||||
))
|
||||
.bearer_auth(admin_token)
|
||||
.send()
|
||||
|
|
@ -311,7 +324,7 @@ async fn restore_keycloak_remora_roles() -> Result<(), String> {
|
|||
.ok_or_else(|| "keycloak user missing".to_owned())?;
|
||||
let clients: Value = client
|
||||
.get(format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/clients?clientId={client_id}"
|
||||
"{keycloak_url}/admin/realms/{realm}/clients?clientId={client_id}"
|
||||
))
|
||||
.bearer_auth(admin_token)
|
||||
.send()
|
||||
|
|
@ -327,7 +340,7 @@ async fn restore_keycloak_remora_roles() -> Result<(), String> {
|
|||
.ok_or_else(|| "keycloak client missing".to_owned())?;
|
||||
let roles: Value = client
|
||||
.get(format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/clients/{client_uuid}/roles"
|
||||
"{keycloak_url}/admin/realms/{realm}/clients/{client_uuid}/roles"
|
||||
))
|
||||
.bearer_auth(admin_token)
|
||||
.send()
|
||||
|
|
@ -350,7 +363,7 @@ async fn restore_keycloak_remora_roles() -> Result<(), String> {
|
|||
.collect::<Vec<_>>();
|
||||
let response = client
|
||||
.post(format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/users/{user_id}/role-mappings/clients/{client_uuid}"
|
||||
"{keycloak_url}/admin/realms/{realm}/users/{user_id}/role-mappings/clients/{client_uuid}"
|
||||
))
|
||||
.bearer_auth(admin_token)
|
||||
.json(&roles)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use std::io;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
|
|
@ -11,9 +10,7 @@ use remora_domain::ports::CredentialResolver;
|
|||
use remora_durable::{PoolConfig, connect};
|
||||
use remora_schema::migrate as migrate_schema;
|
||||
use sqlx::PgPool;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::{Barrier, oneshot};
|
||||
use tokio::sync::Barrier;
|
||||
|
||||
fn database_url() -> Option<String> {
|
||||
std::env::var("DATABASE_URL")
|
||||
|
|
@ -164,65 +161,6 @@ async fn harness() -> Option<(PgPool, CredentialRegistry, GrantBoundResolver)> {
|
|||
Some((pool, registry, resolver))
|
||||
}
|
||||
|
||||
async fn held_vault_response_proxy(
|
||||
upstream: &str,
|
||||
) -> (String, oneshot::Receiver<()>, oneshot::Sender<()>) {
|
||||
let authority = upstream
|
||||
.strip_prefix("http://")
|
||||
.expect("fixture Vault must use HTTP")
|
||||
.trim_end_matches('/')
|
||||
.to_owned();
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("proxy bind");
|
||||
let address = listener.local_addr().expect("proxy address");
|
||||
let (applied_tx, applied_rx) = oneshot::channel();
|
||||
let (release_tx, release_rx) = oneshot::channel();
|
||||
tokio::spawn(async move {
|
||||
let (mut client, _) = listener.accept().await.expect("proxy accept");
|
||||
let request = read_http_message(&mut client).await.expect("proxy request");
|
||||
let mut vault = TcpStream::connect(&authority)
|
||||
.await
|
||||
.expect("connect fixture Vault");
|
||||
vault.write_all(&request).await.expect("forward request");
|
||||
let response = read_http_message(&mut vault).await.expect("Vault response");
|
||||
let _ = applied_tx.send(());
|
||||
let _ = release_rx.await;
|
||||
client.write_all(&response).await.expect("release response");
|
||||
});
|
||||
(format!("http://{address}"), applied_rx, release_tx)
|
||||
}
|
||||
|
||||
async fn read_http_message(stream: &mut TcpStream) -> io::Result<Vec<u8>> {
|
||||
let mut message = Vec::with_capacity(8192);
|
||||
let mut chunk = [0u8; 4096];
|
||||
let mut expected = None;
|
||||
loop {
|
||||
let read = stream.read(&mut chunk).await?;
|
||||
if read == 0 {
|
||||
return Ok(message);
|
||||
}
|
||||
message.extend_from_slice(&chunk[..read]);
|
||||
if expected.is_none()
|
||||
&& let Some(header_end) = message.windows(4).position(|window| window == b"\r\n\r\n")
|
||||
{
|
||||
let headers = std::str::from_utf8(&message[..header_end])
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||
let content_length = headers
|
||||
.lines()
|
||||
.find_map(|line| {
|
||||
let (name, value) = line.split_once(':')?;
|
||||
name.eq_ignore_ascii_case("content-length")
|
||||
.then(|| value.trim().parse::<usize>().ok())
|
||||
.flatten()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
expected = Some(header_end + 4 + content_length);
|
||||
}
|
||||
if expected.is_some_and(|length| message.len() >= length) {
|
||||
return Ok(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_pin_and_exact_version_resolution() {
|
||||
let Some((pool, registry, resolver)) = harness().await else {
|
||||
|
|
@ -1075,103 +1013,6 @@ async fn reconcile_claim_blocks_concurrent_materialize() {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_reconcilers_claim_before_a_late_create_response() {
|
||||
let Some(pool) = test_pool().await else {
|
||||
return;
|
||||
};
|
||||
let Some(direct_vault) = vault_client() else {
|
||||
return;
|
||||
};
|
||||
let upstream = direct_vault.config().address.clone();
|
||||
let (proxy_address, applied, release) = held_vault_response_proxy(&upstream).await;
|
||||
let mut delayed_config = VaultConfig::from_env().expect("Vault config");
|
||||
delayed_config.address = proxy_address;
|
||||
delayed_config.timeout = Duration::from_secs(5);
|
||||
let delayed_registry = CredentialRegistry::new(
|
||||
pool.clone(),
|
||||
VaultClient::new(delayed_config).expect("delayed Vault client"),
|
||||
)
|
||||
.with_reconcile_min_age(Duration::ZERO);
|
||||
let reconciler = Arc::new(
|
||||
CredentialRegistry::new(pool.clone(), direct_vault.clone())
|
||||
.with_reconcile_min_age(Duration::ZERO),
|
||||
);
|
||||
let owner = owner("late-response");
|
||||
let create_owner = owner.clone();
|
||||
let create_task = tokio::spawn(async move {
|
||||
delayed_registry
|
||||
.create(
|
||||
&create_owner,
|
||||
CreateCredential {
|
||||
label: "late-response".into(),
|
||||
material: token_material("secret-late-response"),
|
||||
},
|
||||
)
|
||||
.await
|
||||
});
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(5), applied)
|
||||
.await
|
||||
.expect("Vault apply timeout")
|
||||
.expect("Vault apply signal");
|
||||
let (intent_id, secret_mount, secret_path): (String, String, String) = sqlx::query_as(
|
||||
"SELECT intent_id, secret_mount, secret_path
|
||||
FROM secret_write_intents
|
||||
WHERE operation = 'create' AND owner_id = $1
|
||||
ORDER BY created_at DESC LIMIT 1",
|
||||
)
|
||||
.bind(owner.as_str())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("prepared create intent");
|
||||
|
||||
let barrier = Arc::new(Barrier::new(4));
|
||||
let mut reconcile_tasks = Vec::new();
|
||||
for _ in 0..4 {
|
||||
let reconciler = Arc::clone(&reconciler);
|
||||
let barrier = Arc::clone(&barrier);
|
||||
reconcile_tasks.push(tokio::spawn(async move {
|
||||
barrier.wait().await;
|
||||
reconciler.reconcile_secret_write_intents().await
|
||||
}));
|
||||
}
|
||||
for task in reconcile_tasks {
|
||||
task.await.expect("reconciler join").expect("reconcile");
|
||||
}
|
||||
|
||||
let status: String =
|
||||
sqlx::query_scalar("SELECT status FROM secret_write_intents WHERE intent_id = $1")
|
||||
.bind(&intent_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("intent status");
|
||||
assert_eq!(status, "abandoned");
|
||||
|
||||
release.send(()).expect("release Vault response");
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_secs(5), create_task)
|
||||
.await
|
||||
.expect("create completion timeout")
|
||||
.expect("create join")
|
||||
.is_err()
|
||||
);
|
||||
let credential_count: i64 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM source_credentials WHERE owner_id = $1")
|
||||
.bind(owner.as_str())
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.expect("credential count");
|
||||
assert_eq!(credential_count, 0);
|
||||
assert!(
|
||||
direct_vault
|
||||
.list_live_versions(&secret_mount, &secret_path)
|
||||
.await
|
||||
.expect("final Vault metadata")
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepared_intent_fence_blocks_a_late_exact_path_write() {
|
||||
let Some((pool, registry, _)) = harness().await else {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@ fn env_required(name: &str) -> String {
|
|||
.unwrap_or_else(|_| panic!("{name} is required when REMORA_IDENTITY_REQUIRE is set"))
|
||||
}
|
||||
|
||||
fn keycloak_admin_url() -> String {
|
||||
std::env::var("REMORA_KEYCLOAK_ADMIN_URL")
|
||||
.unwrap_or_else(|_| "http://keycloak.remora.test:8080".to_owned())
|
||||
.trim_end_matches('/')
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
async fn test_pool() -> Option<PgPool> {
|
||||
let Some(url) = database_url() else {
|
||||
if require_env() {
|
||||
|
|
@ -370,8 +377,9 @@ async fn revoke_keycloak_remora_client_roles() -> Result<(), String> {
|
|||
let (admin_token, realm, user_id, client_uuid, storage_role) =
|
||||
keycloak_admin_role_context().await?;
|
||||
let client = reqwest::Client::new();
|
||||
let keycloak_url = keycloak_admin_url();
|
||||
let roles_url = format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/users/{user_id}/role-mappings/clients/{client_uuid}"
|
||||
"{keycloak_url}/admin/realms/{realm}/users/{user_id}/role-mappings/clients/{client_uuid}"
|
||||
);
|
||||
let mapped: Value = client
|
||||
.get(&roles_url)
|
||||
|
|
@ -504,8 +512,9 @@ async fn ensure_keycloak_remora_client_roles() -> Result<(), String> {
|
|||
let (admin_token, realm, user_id, client_uuid, storage_role) =
|
||||
keycloak_admin_role_context().await?;
|
||||
let client = reqwest::Client::new();
|
||||
let keycloak_url = keycloak_admin_url();
|
||||
let available_url = format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/users/{user_id}/role-mappings/clients/{client_uuid}/available"
|
||||
"{keycloak_url}/admin/realms/{realm}/users/{user_id}/role-mappings/clients/{client_uuid}/available"
|
||||
);
|
||||
let available: Value = client
|
||||
.get(available_url)
|
||||
|
|
@ -532,7 +541,7 @@ async fn ensure_keycloak_remora_client_roles() -> Result<(), String> {
|
|||
return Ok(());
|
||||
}
|
||||
let roles_url = format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/users/{user_id}/role-mappings/clients/{client_uuid}"
|
||||
"{keycloak_url}/admin/realms/{realm}/users/{user_id}/role-mappings/clients/{client_uuid}"
|
||||
);
|
||||
let post = client
|
||||
.post(roles_url)
|
||||
|
|
@ -556,10 +565,10 @@ async fn keycloak_admin_role_context() -> Result<(String, String, String, String
|
|||
let client_id = env_required("REMORA_OIDC_CLIENT_ID");
|
||||
let username = env_required("REMORA_OIDC_USERNAME");
|
||||
let storage_role = env_required("REMORA_OIDC_STORAGE_ROLE");
|
||||
let keycloak_url = keycloak_admin_url();
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let admin_token_url =
|
||||
"http://keycloak.remora.test:8080/realms/master/protocol/openid-connect/token";
|
||||
let admin_token_url = format!("{keycloak_url}/realms/master/protocol/openid-connect/token");
|
||||
let admin_token_response = client
|
||||
.post(admin_token_url)
|
||||
.form(&[
|
||||
|
|
@ -588,9 +597,8 @@ async fn keycloak_admin_role_context() -> Result<(String, String, String, String
|
|||
.ok_or("keycloak admin token missing access_token")?
|
||||
.to_owned();
|
||||
|
||||
let users_url = format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/users?username={username}&exact=true"
|
||||
);
|
||||
let users_url =
|
||||
format!("{keycloak_url}/admin/realms/{realm}/users?username={username}&exact=true");
|
||||
let users: Value = client
|
||||
.get(users_url)
|
||||
.bearer_auth(&admin_token)
|
||||
|
|
@ -608,9 +616,7 @@ async fn keycloak_admin_role_context() -> Result<(String, String, String, String
|
|||
.ok_or("keycloak user not found")?
|
||||
.to_owned();
|
||||
|
||||
let clients_url = format!(
|
||||
"http://keycloak.remora.test:8080/admin/realms/{realm}/clients?clientId={client_id}"
|
||||
);
|
||||
let clients_url = format!("{keycloak_url}/admin/realms/{realm}/clients?clientId={client_id}");
|
||||
let clients: Value = client
|
||||
.get(clients_url)
|
||||
.bearer_auth(&admin_token)
|
||||
|
|
|
|||
Loading…
Reference in a new issue