Stabilize integration test qualification

This commit is contained in:
ms2k 2026-08-19 22:33:56 +09:00
commit 560e1df9b2
4 changed files with 68 additions and 180 deletions

View file

@ -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)

View file

@ -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 {

View file

@ -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)

View file

@ -0,0 +1,28 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { loadConfig } from './config.mjs'
const environment = {
REMORA_WEB_PUBLIC_ORIGIN: 'https://remora.example.test',
REMORA_WEB_API_UPSTREAM: 'https://api.example.test/v1',
REMORA_WEB_OIDC_AUTHORITY: 'https://identity.example.test',
REMORA_WEB_OIDC_CLIENT_ID: 'remora-web',
REMORA_WEB_OIDC_CLIENT_SECRET: 'client-secret',
REMORA_WEB_SESSION_URL: 'redis://session.example.test',
}
test('derives the event endpoint from the API upstream', () => {
const config = loadConfig(environment)
assert.equal(config.eventsUpstream.href, 'wss://api.example.test/v1/events')
})
test('uses the explicit event endpoint', () => {
const config = loadConfig({
...environment,
REMORA_WEB_EVENTS_UPSTREAM: 'http://events.example.test/stream',
})
assert.equal(config.eventsUpstream.href, 'ws://events.example.test/stream')
})