mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(users): harden batch role and quota updates
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use super::{
|
use super::{
|
||||||
build_admin_users_bad_request_response, build_admin_users_read_only_response,
|
build_admin_users_bad_request_response, build_admin_users_read_only_response,
|
||||||
normalize_admin_user_api_formats, normalize_admin_user_string_list,
|
normalize_admin_user_api_formats, normalize_admin_user_role, normalize_admin_user_string_list,
|
||||||
};
|
};
|
||||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||||
@@ -70,6 +70,7 @@ struct ResolvedAdminUserSelection {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
struct AdminUserBatchMutation {
|
struct AdminUserBatchMutation {
|
||||||
|
role: Option<String>,
|
||||||
allowed_providers_present: bool,
|
allowed_providers_present: bool,
|
||||||
allowed_providers: Option<Vec<String>>,
|
allowed_providers: Option<Vec<String>>,
|
||||||
allowed_api_formats_present: bool,
|
allowed_api_formats_present: bool,
|
||||||
@@ -79,9 +80,21 @@ struct AdminUserBatchMutation {
|
|||||||
rate_limit_present: bool,
|
rate_limit_present: bool,
|
||||||
rate_limit: Option<i32>,
|
rate_limit: Option<i32>,
|
||||||
is_active: Option<bool>,
|
is_active: Option<bool>,
|
||||||
|
unlimited: Option<bool>,
|
||||||
modified_fields: Vec<&'static str>,
|
modified_fields: Vec<&'static str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AdminUserBatchMutation {
|
||||||
|
fn has_auth_user_fields(&self) -> bool {
|
||||||
|
self.role.is_some()
|
||||||
|
|| self.allowed_providers_present
|
||||||
|
|| self.allowed_api_formats_present
|
||||||
|
|| self.allowed_models_present
|
||||||
|
|| self.rate_limit_present
|
||||||
|
|| self.is_active.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(in super::super) async fn build_admin_resolve_user_selection_response(
|
pub(in super::super) async fn build_admin_resolve_user_selection_response(
|
||||||
state: &AdminAppState<'_>,
|
state: &AdminAppState<'_>,
|
||||||
_request_context: &AdminRequestContext<'_>,
|
_request_context: &AdminRequestContext<'_>,
|
||||||
@@ -105,15 +118,9 @@ pub(in super::super) async fn build_admin_resolve_user_selection_response(
|
|||||||
|
|
||||||
pub(in super::super) async fn build_admin_user_batch_action_response(
|
pub(in super::super) async fn build_admin_user_batch_action_response(
|
||||||
state: &AdminAppState<'_>,
|
state: &AdminAppState<'_>,
|
||||||
_request_context: &AdminRequestContext<'_>,
|
request_context: &AdminRequestContext<'_>,
|
||||||
request_body: Option<&Bytes>,
|
request_body: Option<&Bytes>,
|
||||||
) -> Result<Response<Body>, GatewayError> {
|
) -> Result<Response<Body>, GatewayError> {
|
||||||
if !state.has_auth_user_write_capability() {
|
|
||||||
return Ok(build_admin_users_read_only_response(
|
|
||||||
"当前为只读模式,无法批量更新用户",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = match parse_batch_action_request(request_body) {
|
let request = match parse_batch_action_request(request_body) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(detail) => return Ok(build_admin_user_batch_bad_request_response(detail)),
|
Err(detail) => return Ok(build_admin_user_batch_bad_request_response(detail)),
|
||||||
@@ -122,10 +129,30 @@ pub(in super::super) async fn build_admin_user_batch_action_response(
|
|||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(detail) => return Ok(build_admin_user_batch_bad_request_response(detail)),
|
Err(detail) => return Ok(build_admin_user_batch_bad_request_response(detail)),
|
||||||
};
|
};
|
||||||
|
if mutation.has_auth_user_fields() && !state.has_auth_user_write_capability() {
|
||||||
|
return Ok(build_admin_users_read_only_response(
|
||||||
|
"当前为只读模式,无法批量更新用户",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if mutation.unlimited.is_some() && !state.has_auth_wallet_write_capability() {
|
||||||
|
return Ok(build_admin_users_read_only_response(
|
||||||
|
"当前为只读模式,无法批量更新用户钱包",
|
||||||
|
));
|
||||||
|
}
|
||||||
let resolved = match resolve_admin_user_selection(state, request.selection).await {
|
let resolved = match resolve_admin_user_selection(state, request.selection).await {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(detail) => return Ok(build_admin_user_batch_bad_request_response(detail)),
|
Err(detail) => return Ok(build_admin_user_batch_bad_request_response(detail)),
|
||||||
};
|
};
|
||||||
|
let active_admin_demotions = count_active_admin_demotions(&mutation, &resolved.items);
|
||||||
|
let active_admin_count = if active_admin_demotions > 0 {
|
||||||
|
state.count_active_admin_users().await?
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
let current_admin_user_id = request_context
|
||||||
|
.decision()
|
||||||
|
.and_then(|decision| decision.admin_principal.as_ref())
|
||||||
|
.map(|principal| principal.user_id.as_str());
|
||||||
|
|
||||||
let mut success = 0usize;
|
let mut success = 0usize;
|
||||||
let mut failures = resolved
|
let mut failures = resolved
|
||||||
@@ -135,29 +162,64 @@ pub(in super::super) async fn build_admin_user_batch_action_response(
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for item in &resolved.items {
|
for item in &resolved.items {
|
||||||
let updated = state
|
if state.find_user_auth_by_id(&item.user_id).await?.is_none() {
|
||||||
.update_local_auth_user_admin_fields(
|
|
||||||
&item.user_id,
|
|
||||||
None,
|
|
||||||
mutation.allowed_providers_present,
|
|
||||||
mutation.allowed_providers.clone(),
|
|
||||||
mutation.allowed_api_formats_present,
|
|
||||||
mutation.allowed_api_formats.clone(),
|
|
||||||
mutation.allowed_models_present,
|
|
||||||
mutation.allowed_models.clone(),
|
|
||||||
mutation.rate_limit_present,
|
|
||||||
mutation.rate_limit,
|
|
||||||
mutation.is_active,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
if updated.is_some() {
|
|
||||||
success += 1;
|
|
||||||
} else {
|
|
||||||
failures.push(json!({
|
failures.push(json!({
|
||||||
"user_id": item.user_id,
|
"user_id": item.user_id,
|
||||||
"reason": "用户不存在或已删除",
|
"reason": "用户不存在或已删除",
|
||||||
}));
|
}));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(reason) = batch_role_demotion_failure_reason(
|
||||||
|
&mutation,
|
||||||
|
item,
|
||||||
|
active_admin_count,
|
||||||
|
active_admin_demotions,
|
||||||
|
current_admin_user_id,
|
||||||
|
) {
|
||||||
|
failures.push(json!({
|
||||||
|
"user_id": item.user_id,
|
||||||
|
"reason": reason,
|
||||||
|
}));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(unlimited) = mutation.unlimited {
|
||||||
|
if !apply_batch_user_wallet_limit_mode(state, &item.user_id, unlimited).await? {
|
||||||
|
failures.push(json!({
|
||||||
|
"user_id": item.user_id,
|
||||||
|
"reason": "用户钱包不可用",
|
||||||
|
}));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mutation.has_auth_user_fields()
|
||||||
|
&& state
|
||||||
|
.update_local_auth_user_admin_fields(
|
||||||
|
&item.user_id,
|
||||||
|
mutation.role.clone(),
|
||||||
|
mutation.allowed_providers_present,
|
||||||
|
mutation.allowed_providers.clone(),
|
||||||
|
mutation.allowed_api_formats_present,
|
||||||
|
mutation.allowed_api_formats.clone(),
|
||||||
|
mutation.allowed_models_present,
|
||||||
|
mutation.allowed_models.clone(),
|
||||||
|
mutation.rate_limit_present,
|
||||||
|
mutation.rate_limit,
|
||||||
|
mutation.is_active,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
failures.push(json!({
|
||||||
|
"user_id": item.user_id,
|
||||||
|
"reason": "用户不存在或已删除",
|
||||||
|
}));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
success += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
let failed = failures.len();
|
let failed = failures.len();
|
||||||
@@ -410,10 +472,32 @@ fn parse_batch_mutation(
|
|||||||
..AdminUserBatchMutation::default()
|
..AdminUserBatchMutation::default()
|
||||||
}),
|
}),
|
||||||
"update_access_control" => parse_access_control_mutation(payload),
|
"update_access_control" => parse_access_control_mutation(payload),
|
||||||
|
"update_role" => parse_role_mutation(payload),
|
||||||
_ => Err("不支持的批量操作".to_string()),
|
_ => Err("不支持的批量操作".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_role_mutation(payload: Option<Value>) -> Result<AdminUserBatchMutation, String> {
|
||||||
|
let Some(Value::Object(payload)) = payload else {
|
||||||
|
return Err("payload 必须是对象".to_string());
|
||||||
|
};
|
||||||
|
let Some(value) = payload.get("role") else {
|
||||||
|
return Err("role 参数不能为空".to_string());
|
||||||
|
};
|
||||||
|
let Some(role) = value.as_str() else {
|
||||||
|
return Err("role 参数不合法".to_string());
|
||||||
|
};
|
||||||
|
let role = role.trim();
|
||||||
|
if role.is_empty() {
|
||||||
|
return Err("role 参数不能为空".to_string());
|
||||||
|
}
|
||||||
|
Ok(AdminUserBatchMutation {
|
||||||
|
role: Some(normalize_admin_user_role(Some(role))?),
|
||||||
|
modified_fields: vec!["role"],
|
||||||
|
..AdminUserBatchMutation::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_access_control_mutation(payload: Option<Value>) -> Result<AdminUserBatchMutation, String> {
|
fn parse_access_control_mutation(payload: Option<Value>) -> Result<AdminUserBatchMutation, String> {
|
||||||
let Some(Value::Object(payload)) = payload else {
|
let Some(Value::Object(payload)) = payload else {
|
||||||
return Err("payload 必须是对象".to_string());
|
return Err("payload 必须是对象".to_string());
|
||||||
@@ -440,6 +524,10 @@ fn parse_access_control_mutation(payload: Option<Value>) -> Result<AdminUserBatc
|
|||||||
mutation.rate_limit = parse_optional_rate_limit(value)?;
|
mutation.rate_limit = parse_optional_rate_limit(value)?;
|
||||||
mutation.modified_fields.push("rate_limit");
|
mutation.modified_fields.push("rate_limit");
|
||||||
}
|
}
|
||||||
|
if let Some(value) = payload.get("unlimited") {
|
||||||
|
mutation.unlimited = Some(parse_unlimited(value)?);
|
||||||
|
mutation.modified_fields.push("unlimited");
|
||||||
|
}
|
||||||
|
|
||||||
if mutation.modified_fields.is_empty() {
|
if mutation.modified_fields.is_empty() {
|
||||||
return Err("至少需要选择一个要修改的访问控制字段".to_string());
|
return Err("至少需要选择一个要修改的访问控制字段".to_string());
|
||||||
@@ -481,6 +569,73 @@ fn parse_optional_rate_limit(value: &Value) -> Result<Option<i32>, String> {
|
|||||||
Ok(Some(rate_limit))
|
Ok(Some(rate_limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_unlimited(value: &Value) -> Result<bool, String> {
|
||||||
|
serde_json::from_value::<bool>(value.clone()).map_err(|_| "unlimited 必须是布尔值".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_active_admin_demotions(
|
||||||
|
mutation: &AdminUserBatchMutation,
|
||||||
|
items: &[AdminUserSelectionItem],
|
||||||
|
) -> usize {
|
||||||
|
if mutation.role.as_deref() != Some("user") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.filter(|item| item.is_active && item.role.eq_ignore_ascii_case("admin"))
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn batch_role_demotion_failure_reason(
|
||||||
|
mutation: &AdminUserBatchMutation,
|
||||||
|
item: &AdminUserSelectionItem,
|
||||||
|
active_admin_count: u64,
|
||||||
|
active_admin_demotions: usize,
|
||||||
|
current_admin_user_id: Option<&str>,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
if mutation.role.as_deref() != Some("user")
|
||||||
|
|| !item.is_active
|
||||||
|
|| !item.role.eq_ignore_ascii_case("admin")
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if current_admin_user_id.is_some_and(|user_id| user_id == item.user_id) {
|
||||||
|
return Some("不能降级当前管理员账户");
|
||||||
|
}
|
||||||
|
if active_admin_count <= active_admin_demotions as u64 {
|
||||||
|
return Some("不能降级最后一个管理员账户");
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_batch_user_wallet_limit_mode(
|
||||||
|
state: &AdminAppState<'_>,
|
||||||
|
user_id: &str,
|
||||||
|
unlimited: bool,
|
||||||
|
) -> Result<bool, GatewayError> {
|
||||||
|
let desired_limit_mode = if unlimited { "unlimited" } else { "finite" };
|
||||||
|
match state
|
||||||
|
.find_wallet(aether_data::repository::wallet::WalletLookupKey::UserId(
|
||||||
|
user_id,
|
||||||
|
))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some(wallet) => {
|
||||||
|
if wallet.limit_mode.eq_ignore_ascii_case(desired_limit_mode) {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Ok(state
|
||||||
|
.update_auth_user_wallet_limit_mode(user_id, desired_limit_mode)
|
||||||
|
.await?
|
||||||
|
.is_some())
|
||||||
|
}
|
||||||
|
None => Ok(state
|
||||||
|
.initialize_auth_user_wallet(user_id, 0.0, unlimited)
|
||||||
|
.await?
|
||||||
|
.is_some()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn build_admin_user_batch_bad_request_response(detail: String) -> Response<Body> {
|
fn build_admin_user_batch_bad_request_response(detail: String) -> Response<Body> {
|
||||||
if detail.as_str() == "缺少 user_id" {
|
if detail.as_str() == "缺少 user_id" {
|
||||||
return build_admin_users_bad_request_response("缺少 user_id");
|
return build_admin_users_bad_request_response("缺少 user_id");
|
||||||
|
|||||||
@@ -474,7 +474,8 @@ async fn gateway_handles_admin_user_batch_actions_locally() {
|
|||||||
let gateway = build_router_with_state(
|
let gateway = build_router_with_state(
|
||||||
AppState::new()
|
AppState::new()
|
||||||
.expect("gateway should build")
|
.expect("gateway should build")
|
||||||
.with_auth_users_for_tests([sample_admin_user("user-1")]),
|
.with_auth_users_for_tests([sample_admin_user("user-1")])
|
||||||
|
.with_auth_wallets_for_tests([sample_admin_wallet("user-1", "unlimited")]),
|
||||||
);
|
);
|
||||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
@@ -534,7 +535,8 @@ async fn gateway_handles_admin_user_batch_actions_locally() {
|
|||||||
"allowed_providers": null,
|
"allowed_providers": null,
|
||||||
"allowed_api_formats": ["OPENAI:RESPONSES"],
|
"allowed_api_formats": ["OPENAI:RESPONSES"],
|
||||||
"allowed_models": [],
|
"allowed_models": [],
|
||||||
"rate_limit": 0
|
"rate_limit": 0,
|
||||||
|
"unlimited": false
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.send()
|
.send()
|
||||||
@@ -554,10 +556,103 @@ async fn gateway_handles_admin_user_batch_actions_locally() {
|
|||||||
"allowed_providers",
|
"allowed_providers",
|
||||||
"allowed_api_formats",
|
"allowed_api_formats",
|
||||||
"allowed_models",
|
"allowed_models",
|
||||||
"rate_limit"
|
"rate_limit",
|
||||||
|
"unlimited"
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let role_response = client
|
||||||
|
.post(format!("{gateway_url}/api/admin/users/batch-action"))
|
||||||
|
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||||
|
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||||
|
.json(&json!({
|
||||||
|
"selection": {
|
||||||
|
"user_ids": ["user-1"]
|
||||||
|
},
|
||||||
|
"action": "update_role",
|
||||||
|
"payload": {
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("request should succeed");
|
||||||
|
assert_eq!(role_response.status(), StatusCode::OK);
|
||||||
|
let role_payload: serde_json::Value =
|
||||||
|
role_response.json().await.expect("json body should parse");
|
||||||
|
assert_eq!(role_payload["total"], 1);
|
||||||
|
assert_eq!(role_payload["success"], 1);
|
||||||
|
assert_eq!(role_payload["modified_fields"], json!(["role"]));
|
||||||
|
|
||||||
|
let blank_role_response = client
|
||||||
|
.post(format!("{gateway_url}/api/admin/users/batch-action"))
|
||||||
|
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||||
|
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||||
|
.json(&json!({
|
||||||
|
"selection": {
|
||||||
|
"user_ids": ["user-1"]
|
||||||
|
},
|
||||||
|
"action": "update_role",
|
||||||
|
"payload": {
|
||||||
|
"role": ""
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("request should succeed");
|
||||||
|
assert_eq!(blank_role_response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
let enable_admin_response = client
|
||||||
|
.post(format!("{gateway_url}/api/admin/users/batch-action"))
|
||||||
|
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||||
|
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||||
|
.json(&json!({
|
||||||
|
"selection": {
|
||||||
|
"user_ids": ["user-1"]
|
||||||
|
},
|
||||||
|
"action": "enable"
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("request should succeed");
|
||||||
|
assert_eq!(enable_admin_response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let last_admin_demotion_response = client
|
||||||
|
.post(format!("{gateway_url}/api/admin/users/batch-action"))
|
||||||
|
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
|
||||||
|
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||||
|
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||||
|
.json(&json!({
|
||||||
|
"selection": {
|
||||||
|
"user_ids": ["user-1"]
|
||||||
|
},
|
||||||
|
"action": "update_role",
|
||||||
|
"payload": {
|
||||||
|
"role": "user"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("request should succeed");
|
||||||
|
assert_eq!(last_admin_demotion_response.status(), StatusCode::OK);
|
||||||
|
let last_admin_demotion_payload: serde_json::Value = last_admin_demotion_response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.expect("json body should parse");
|
||||||
|
assert_eq!(last_admin_demotion_payload["success"], 0);
|
||||||
|
assert_eq!(last_admin_demotion_payload["failed"], 1);
|
||||||
|
assert_eq!(
|
||||||
|
last_admin_demotion_payload["failures"][0]["reason"],
|
||||||
|
"不能降级最后一个管理员账户"
|
||||||
|
);
|
||||||
|
|
||||||
let detail_response = client
|
let detail_response = client
|
||||||
.get(format!("{gateway_url}/api/admin/users/user-1"))
|
.get(format!("{gateway_url}/api/admin/users/user-1"))
|
||||||
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
|
||||||
@@ -572,7 +667,9 @@ async fn gateway_handles_admin_user_batch_actions_locally() {
|
|||||||
.json()
|
.json()
|
||||||
.await
|
.await
|
||||||
.expect("json body should parse");
|
.expect("json body should parse");
|
||||||
assert_eq!(detail_payload["is_active"], false);
|
assert_eq!(detail_payload["role"], "admin");
|
||||||
|
assert_eq!(detail_payload["is_active"], true);
|
||||||
|
assert_eq!(detail_payload["unlimited"], false);
|
||||||
assert_eq!(detail_payload["allowed_providers"], serde_json::Value::Null);
|
assert_eq!(detail_payload["allowed_providers"], serde_json::Value::Null);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
detail_payload["allowed_api_formats"],
|
detail_payload["allowed_api_formats"],
|
||||||
|
|||||||
Reference in New Issue
Block a user