Use testcontainers for Rust PostgreSQL tests
This commit is contained in:
@@ -16,20 +16,6 @@ jobs:
|
|||||||
rust:
|
rust:
|
||||||
name: Rust Checks
|
name: Rust Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
|
||||||
crank-rust-postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
env:
|
|
||||||
POSTGRES_DB: crank_test
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
options: >-
|
|
||||||
--health-cmd "pg_isready -U postgres -d crank_test"
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
env:
|
|
||||||
TEST_DATABASE_URL: postgres://postgres:postgres@crank-rust-postgres:5432/crank_test
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
Generated
+920
-16
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ members = [
|
|||||||
"crates/crank-mapping",
|
"crates/crank-mapping",
|
||||||
"crates/crank-registry",
|
"crates/crank-registry",
|
||||||
"crates/crank-runtime",
|
"crates/crank-runtime",
|
||||||
|
"crates/crank-test-support",
|
||||||
"crates/crank-adapter-rest",
|
"crates/crank-adapter-rest",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -39,3 +40,5 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
|||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
uuid = { version = "1", features = ["serde", "v7"] }
|
uuid = { version = "1", features = ["serde", "v7"] }
|
||||||
|
testcontainers = { version = "0.25.0", features = ["blocking"] }
|
||||||
|
testcontainers-modules = { version = "0.13.0", features = ["postgres", "blocking"] }
|
||||||
|
|||||||
@@ -196,8 +196,8 @@ deploy/community/
|
|||||||
|
|
||||||
- Rust toolchain из [`rust-toolchain.toml`](./rust-toolchain.toml);
|
- Rust toolchain из [`rust-toolchain.toml`](./rust-toolchain.toml);
|
||||||
- Node.js и npm;
|
- Node.js и npm;
|
||||||
- PostgreSQL;
|
- PostgreSQL, если запускаете сервисы вручную;
|
||||||
- Docker, если хотите запускать полный стенд контейнерами.
|
- Docker для полного стенда и Rust-тестов с временной PostgreSQL.
|
||||||
|
|
||||||
Быстрее всего поднять окружение так же, как для обычного запуска:
|
Быстрее всего поднять окружение так же, как для обычного запуска:
|
||||||
|
|
||||||
@@ -210,6 +210,8 @@ docker compose up -d postgres
|
|||||||
|
|
||||||
Приложения читают настройки из переменных окружения. Можно использовать `.env` через свое окружение разработки, `direnv`, IDE или любой другой привычный способ загрузки переменных.
|
Приложения читают настройки из переменных окружения. Можно использовать `.env` через свое окружение разработки, `direnv`, IDE или любой другой привычный способ загрузки переменных.
|
||||||
|
|
||||||
|
Rust-тесты сами поднимают временный PostgreSQL через Testcontainers. Отдельно запускать тестовую БД или задавать URL тестовой базы не нужно.
|
||||||
|
|
||||||
Сервер панели управления:
|
Сервер панели управления:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -35,5 +35,6 @@ uuid.workspace = true
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
|
crank-test-support = { path = "../../crates/crank-test-support" }
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serial_test = "3"
|
serial_test = "3"
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ mod tests {
|
|||||||
use crank_schema::{Schema, SchemaKind};
|
use crank_schema::{Schema, SchemaKind};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use sqlx::{Connection, Executor, PgConnection};
|
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -2229,26 +2228,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn test_registry() -> PostgresRegistry {
|
async fn test_registry() -> PostgresRegistry {
|
||||||
let database_url = env::var("TEST_DATABASE_URL")
|
let database_url = crank_test_support::postgres_schema_url("test_admin_api").await;
|
||||||
.unwrap_or_else(|_| "postgres://crank:crank@127.0.0.1:15432/crank".to_owned());
|
let registry = PostgresRegistry::connect(&database_url).await.unwrap();
|
||||||
let mut admin_connection = PgConnection::connect(&database_url).await.unwrap();
|
|
||||||
let schema = format!(
|
|
||||||
"test_admin_api_{}_{}",
|
|
||||||
std::process::id(),
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_nanos()
|
|
||||||
);
|
|
||||||
|
|
||||||
admin_connection
|
|
||||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let registry =
|
|
||||||
PostgresRegistry::connect(&format!("{database_url}?options=-csearch_path%3D{schema}"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let password_hash = hash_password(TEST_AUTH_PASSWORD, TEST_PASSWORD_PEPPER).unwrap();
|
let password_hash = hash_password(TEST_AUTH_PASSWORD, TEST_PASSWORD_PEPPER).unwrap();
|
||||||
let user_id = registry
|
let user_id = registry
|
||||||
.upsert_bootstrap_user(TEST_AUTH_EMAIL, "Test Owner", &password_hash)
|
.upsert_bootstrap_user(TEST_AUTH_EMAIL, "Test Owner", &password_hash)
|
||||||
|
|||||||
@@ -33,4 +33,5 @@ uuid.workspace = true
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
crank-mapping = { path = "../../crates/crank-mapping" }
|
crank-mapping = { path = "../../crates/crank-mapping" }
|
||||||
crank-schema = { path = "../../crates/crank-schema" }
|
crank-schema = { path = "../../crates/crank-schema" }
|
||||||
|
crank-test-support = { path = "../../crates/crank-test-support" }
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ fn mcp_api_rate_limit_config_from_env() -> Result<RequestRateLimitConfig, Box<dy
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
env, io,
|
io,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@@ -158,7 +158,6 @@ mod tests {
|
|||||||
use futures_util::stream;
|
use futures_util::stream;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use sqlx::{Executor, postgres::PgPoolOptions};
|
|
||||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
@@ -1792,23 +1791,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn test_registry() -> PostgresRegistry {
|
async fn test_registry() -> PostgresRegistry {
|
||||||
let database_url = env::var("TEST_DATABASE_URL")
|
let database_url = crank_test_support::postgres_schema_url("test_mcp_server").await;
|
||||||
.unwrap_or_else(|_| "postgres://crank:crank@127.0.0.1:15432/crank".to_owned());
|
PostgresRegistry::connect(&database_url).await.unwrap()
|
||||||
let admin_pool = PgPoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.connect(&database_url)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let schema = format!("test_mcp_server_{}", uuid::Uuid::now_v7().simple());
|
|
||||||
|
|
||||||
admin_pool
|
|
||||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
PostgresRegistry::connect(&format!("{database_url}?options=-csearch_path%3D{schema}"))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_operation(base_url: &str, name: &str) -> Operation<Schema, MappingSet> {
|
fn test_operation(base_url: &str, name: &str) -> Operation<Schema, MappingSet> {
|
||||||
|
|||||||
@@ -27,3 +27,4 @@ uuid.workspace = true
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
crank-mapping = { path = "../crank-mapping" }
|
crank-mapping = { path = "../crank-mapping" }
|
||||||
|
crank-test-support = { path = "../crank-test-support" }
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
use std::env;
|
|
||||||
|
|
||||||
use crank_community_mcp::session::{PostgresTransportSessionStore, TransportSessionStore};
|
use crank_community_mcp::session::{PostgresTransportSessionStore, TransportSessionStore};
|
||||||
use crank_registry::PostgresPoolConfig;
|
use crank_registry::PostgresPoolConfig;
|
||||||
use sqlx::{
|
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
|
||||||
Executor,
|
|
||||||
postgres::{PgConnectOptions, PgPoolOptions},
|
|
||||||
};
|
|
||||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
fn timestamp(value: &str) -> time::OffsetDateTime {
|
fn timestamp(value: &str) -> time::OffsetDateTime {
|
||||||
time::OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
time::OffsetDateTime::parse(value, &Rfc3339).unwrap()
|
||||||
@@ -21,23 +15,8 @@ fn truncate_to_micros(value: OffsetDateTime) -> OffsetDateTime {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn postgres_transport_sessions_survive_store_reconnect() {
|
async fn postgres_transport_sessions_survive_store_reconnect() {
|
||||||
let database_url = env::var("TEST_DATABASE_URL")
|
let database_url = crank_test_support::postgres_schema_url("test_mcp_transport").await;
|
||||||
.unwrap_or_else(|_| "postgres://crank:crank@127.0.0.1:15432/crank".to_owned());
|
let connect_options = database_url.parse::<PgConnectOptions>().unwrap();
|
||||||
let admin_pool = PgPoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.connect(&database_url)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let schema = format!("test_mcp_transport_{}", Uuid::now_v7().simple());
|
|
||||||
|
|
||||||
admin_pool
|
|
||||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let connect_options = format!("{database_url}?options=-csearch_path%3D{schema}")
|
|
||||||
.parse::<PgConnectOptions>()
|
|
||||||
.unwrap();
|
|
||||||
let pool_config = PostgresPoolConfig::default();
|
let pool_config = PostgresPoolConfig::default();
|
||||||
let store_a = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
let store_a = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
||||||
connect_options.clone(),
|
connect_options.clone(),
|
||||||
@@ -80,23 +59,8 @@ async fn postgres_transport_sessions_survive_store_reconnect() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn postgres_transport_sessions_evict_expired_rows_on_read() {
|
async fn postgres_transport_sessions_evict_expired_rows_on_read() {
|
||||||
let database_url = env::var("TEST_DATABASE_URL")
|
let database_url = crank_test_support::postgres_schema_url("test_mcp_transport").await;
|
||||||
.unwrap_or_else(|_| "postgres://crank:crank@127.0.0.1:15432/crank".to_owned());
|
let connect_options = database_url.parse::<PgConnectOptions>().unwrap();
|
||||||
let admin_pool = PgPoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.connect(&database_url)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let schema = format!("test_mcp_transport_{}", Uuid::now_v7().simple());
|
|
||||||
|
|
||||||
admin_pool
|
|
||||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let connect_options = format!("{database_url}?options=-csearch_path%3D{schema}")
|
|
||||||
.parse::<PgConnectOptions>()
|
|
||||||
.unwrap();
|
|
||||||
let store = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
let store = PostgresTransportSessionStore::connect_with_options_and_pool_config(
|
||||||
connect_options.clone(),
|
connect_options.clone(),
|
||||||
PostgresPoolConfig::default(),
|
PostgresPoolConfig::default(),
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ time.workspace = true
|
|||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
crank-test-support = { path = "../crank-test-support" }
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
|||||||
@@ -1009,11 +1009,7 @@ fn usage_bucket_sql(bucket: crate::model::UsageBucket) -> &'static str {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{
|
use std::collections::BTreeMap;
|
||||||
collections::BTreeMap,
|
|
||||||
env,
|
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crank_core::{
|
use crank_core::{
|
||||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig,
|
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApiKeyHeaderAuthConfig,
|
||||||
@@ -1917,21 +1913,13 @@ mod tests {
|
|||||||
|
|
||||||
impl TestDatabase {
|
impl TestDatabase {
|
||||||
async fn new() -> Self {
|
async fn new() -> Self {
|
||||||
let database_url = env::var("TEST_DATABASE_URL")
|
let database_url = crank_test_support::postgres_database_url().await.to_owned();
|
||||||
.expect("TEST_DATABASE_URL must point to a reachable PostgreSQL database");
|
|
||||||
let admin_pool = PgPoolOptions::new()
|
let admin_pool = PgPoolOptions::new()
|
||||||
.max_connections(1)
|
.max_connections(1)
|
||||||
.connect(&database_url)
|
.connect(&database_url)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let schema = format!(
|
let schema = crank_test_support::unique_schema_name("test_registry");
|
||||||
"test_registry_{}_{}",
|
|
||||||
std::process::id(),
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_nanos()
|
|
||||||
);
|
|
||||||
|
|
||||||
admin_pool
|
admin_pool
|
||||||
.execute(sqlx::query(&format!("create schema {schema}")))
|
.execute(sqlx::query(&format!("create schema {schema}")))
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "crank-test-support"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libc = "0.2"
|
||||||
|
sqlx.workspace = true
|
||||||
|
testcontainers.workspace = true
|
||||||
|
testcontainers-modules.workspace = true
|
||||||
|
tokio = { workspace = true, features = ["sync"] }
|
||||||
|
uuid.workspace = true
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
use std::{
|
||||||
|
process::{Command, Stdio},
|
||||||
|
sync::{Mutex, OnceLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use sqlx::{Connection, Executor, PgConnection};
|
||||||
|
use testcontainers::{Container, ImageExt, runners::SyncRunner};
|
||||||
|
use testcontainers_modules::postgres::Postgres;
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
|
static POSTGRES: OnceCell<TestPostgres> = OnceCell::const_new();
|
||||||
|
static CLEANUP_REGISTERED: OnceLock<()> = OnceLock::new();
|
||||||
|
static CONTAINER_IDS: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||||
|
|
||||||
|
struct TestPostgres {
|
||||||
|
_container: Container<Postgres>,
|
||||||
|
database_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn postgres_database_url() -> &'static str {
|
||||||
|
&test_postgres().await.database_url
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn postgres_schema_url(prefix: &str) -> String {
|
||||||
|
let database_url = postgres_database_url().await;
|
||||||
|
let schema = unique_schema_name(prefix);
|
||||||
|
let mut connection = PgConnection::connect(database_url)
|
||||||
|
.await
|
||||||
|
.expect("test PostgreSQL container must accept admin connections");
|
||||||
|
|
||||||
|
connection
|
||||||
|
.execute(sqlx::query(&format!("create schema {schema}")))
|
||||||
|
.await
|
||||||
|
.expect("test PostgreSQL schema must be created");
|
||||||
|
|
||||||
|
format!("{database_url}?options=-csearch_path%3D{schema}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unique_schema_name(prefix: &str) -> String {
|
||||||
|
let normalized_prefix: String = prefix
|
||||||
|
.chars()
|
||||||
|
.map(|ch| {
|
||||||
|
if ch.is_ascii_alphanumeric() || ch == '_' {
|
||||||
|
ch.to_ascii_lowercase()
|
||||||
|
} else {
|
||||||
|
'_'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
format!("{}_{}", normalized_prefix, uuid::Uuid::now_v7().simple())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_postgres() -> &'static TestPostgres {
|
||||||
|
POSTGRES
|
||||||
|
.get_or_init(|| async {
|
||||||
|
tokio::task::spawn_blocking(start_postgres)
|
||||||
|
.await
|
||||||
|
.expect("test PostgreSQL container startup task must complete")
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_postgres() -> TestPostgres {
|
||||||
|
let container = Postgres::default()
|
||||||
|
.with_db_name("crank")
|
||||||
|
.with_user("crank")
|
||||||
|
.with_password("crank")
|
||||||
|
.with_tag("16-alpine")
|
||||||
|
.start()
|
||||||
|
.expect("test PostgreSQL container must start");
|
||||||
|
register_container_cleanup(container.id().to_owned());
|
||||||
|
let port = container
|
||||||
|
.get_host_port_ipv4(5432)
|
||||||
|
.expect("test PostgreSQL port must be mapped");
|
||||||
|
|
||||||
|
TestPostgres {
|
||||||
|
_container: container,
|
||||||
|
database_url: format!("postgres://crank:crank@127.0.0.1:{port}/crank"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_container_cleanup(container_id: String) {
|
||||||
|
CLEANUP_REGISTERED.get_or_init(|| unsafe {
|
||||||
|
libc::atexit(cleanup_test_containers);
|
||||||
|
});
|
||||||
|
CONTAINER_IDS
|
||||||
|
.lock()
|
||||||
|
.expect("test container id registry must be available")
|
||||||
|
.push(container_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn cleanup_test_containers() {
|
||||||
|
let container_ids = CONTAINER_IDS
|
||||||
|
.lock()
|
||||||
|
.map(|ids| ids.clone())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if container_ids.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = Command::new("docker")
|
||||||
|
.arg("rm")
|
||||||
|
.arg("-f")
|
||||||
|
.args(container_ids)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status();
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ clippy:
|
|||||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||||
|
|
||||||
test:
|
test:
|
||||||
TEST_DATABASE_URL="${TEST_DATABASE_URL:-postgres://crank:crank@127.0.0.1:15432/crank}" cargo test --workspace --all-targets
|
cargo test --workspace --all-targets
|
||||||
|
|
||||||
sqlx-prepare database_url:
|
sqlx-prepare database_url:
|
||||||
DATABASE_URL={{database_url}} cargo sqlx prepare --workspace -- --all-targets
|
DATABASE_URL={{database_url}} cargo sqlx prepare --workspace -- --all-targets
|
||||||
|
|||||||
Reference in New Issue
Block a user