mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 10:27:46 +08:00
feat(referrals): 添加邀请返利和注册确认功能
This commit is contained in:
@@ -567,6 +567,121 @@ async fn gateway_allows_default_user_group_access_policy_updates() {
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_allows_removing_default_group_members_when_other_group_remains() {
|
||||
let upstream = Router::new().fallback(any(|_request: Request| async {
|
||||
(StatusCode::OK, Body::from("unexpected upstream hit"))
|
||||
}));
|
||||
|
||||
let user_repository = Arc::new(
|
||||
InMemoryUserReadRepository::seed_auth_users(vec![
|
||||
sample_admin_user_with_role("admin-1", "admin", "[email protected]", "admin"),
|
||||
sample_admin_user_with_role("user-2", "user", "[email protected]", "bob"),
|
||||
sample_admin_user_with_role("user-3", "user", "[email protected]", "carol"),
|
||||
])
|
||||
.with_export_users(vec![
|
||||
sample_admin_export_user_with("admin", true, "admin-1", "[email protected]", "admin"),
|
||||
sample_admin_export_user_with("user", true, "user-2", "[email protected]", "bob"),
|
||||
sample_admin_export_user_with("user", true, "user-3", "[email protected]", "carol"),
|
||||
]),
|
||||
);
|
||||
let default_group = user_repository
|
||||
.create_user_group(UpsertUserGroupRecord {
|
||||
name: "Default".to_string(),
|
||||
description: None,
|
||||
priority: 0,
|
||||
allowed_providers: None,
|
||||
allowed_providers_mode: "unrestricted".to_string(),
|
||||
allowed_api_formats: None,
|
||||
allowed_api_formats_mode: "unrestricted".to_string(),
|
||||
allowed_models: None,
|
||||
allowed_models_mode: "unrestricted".to_string(),
|
||||
rate_limit: None,
|
||||
rate_limit_mode: "system".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("default group should create")
|
||||
.expect("default group should exist");
|
||||
let team_group = user_repository
|
||||
.create_user_group(UpsertUserGroupRecord {
|
||||
name: "Team".to_string(),
|
||||
description: None,
|
||||
priority: 0,
|
||||
allowed_providers: None,
|
||||
allowed_providers_mode: "unrestricted".to_string(),
|
||||
allowed_api_formats: None,
|
||||
allowed_api_formats_mode: "unrestricted".to_string(),
|
||||
allowed_models: None,
|
||||
allowed_models_mode: "unrestricted".to_string(),
|
||||
rate_limit: None,
|
||||
rate_limit_mode: "system".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("team group should create")
|
||||
.expect("team group should exist");
|
||||
user_repository
|
||||
.add_user_to_group(&team_group.id, "user-2")
|
||||
.await
|
||||
.expect("team membership should create");
|
||||
|
||||
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(
|
||||
GatewayDataState::with_user_reader_for_tests(user_repository.clone())
|
||||
.with_system_config_values_for_tests(vec![(
|
||||
crate::constants::DEFAULT_USER_GROUP_CONFIG_KEY.to_string(),
|
||||
json!(default_group.id),
|
||||
)]),
|
||||
),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
user_repository
|
||||
.add_user_to_group(&default_group.id, "user-2")
|
||||
.await
|
||||
.expect("default membership should create");
|
||||
user_repository
|
||||
.add_user_to_group(&default_group.id, "user-3")
|
||||
.await
|
||||
.expect("default membership should create");
|
||||
|
||||
let remove_user_with_other_group = client
|
||||
.put(format!(
|
||||
"{gateway_url}/api/admin/user-groups/{}/members",
|
||||
default_group.id
|
||||
))
|
||||
.header(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!({ "user_ids": ["user-3"] }))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
assert_eq!(remove_user_with_other_group.status(), StatusCode::OK);
|
||||
|
||||
let reject_groupless_user = client
|
||||
.put(format!(
|
||||
"{gateway_url}/api/admin/user-groups/{}/members",
|
||||
default_group.id
|
||||
))
|
||||
.header(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!({ "user_ids": [] }))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
assert_eq!(reject_groupless_user.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
gateway_handle.abort();
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_resolves_admin_user_batch_selection_locally() {
|
||||
let upstream_hits = Arc::new(Mutex::new(0usize));
|
||||
|
||||
@@ -69,6 +69,7 @@ async fn gateway_handles_public_announcements_list_without_proxying_upstream() {
|
||||
5,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -85,6 +86,7 @@ async fn gateway_handles_public_announcements_list_without_proxying_upstream() {
|
||||
3,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some("admin-2".to_string()),
|
||||
Some("ops".to_string()),
|
||||
None,
|
||||
@@ -101,6 +103,7 @@ async fn gateway_handles_public_announcements_list_without_proxying_upstream() {
|
||||
100,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Some("admin-3".to_string()),
|
||||
Some("root".to_string()),
|
||||
None,
|
||||
@@ -170,6 +173,7 @@ async fn gateway_handles_public_active_announcements_without_proxying_upstream()
|
||||
50,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
Some((now.saturating_sub(60)) as i64),
|
||||
@@ -186,6 +190,7 @@ async fn gateway_handles_public_active_announcements_without_proxying_upstream()
|
||||
10,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some("admin-2".to_string()),
|
||||
Some("ops".to_string()),
|
||||
Some((now.saturating_add(3600)) as i64),
|
||||
@@ -251,6 +256,7 @@ async fn gateway_handles_public_announcement_detail_without_proxying_upstream()
|
||||
10,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
Some(1_711_000_000),
|
||||
@@ -390,6 +396,7 @@ async fn gateway_updates_announcement_locally_with_trusted_admin_principal() {
|
||||
10,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -479,6 +486,7 @@ async fn gateway_deletes_announcement_locally_with_trusted_admin_principal() {
|
||||
10,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -561,6 +569,7 @@ async fn gateway_returns_service_unavailable_for_admin_announcement_writes_witho
|
||||
10,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -1436,6 +1445,22 @@ async fn gateway_handles_auth_registration_settings_without_proxying_upstream()
|
||||
"turnstile_secret_key".to_string(),
|
||||
json!("secret-private-key"),
|
||||
),
|
||||
(
|
||||
"registration_privacy_policy_enabled".to_string(),
|
||||
json!(true),
|
||||
),
|
||||
(
|
||||
"registration_privacy_policy_format".to_string(),
|
||||
json!("html"),
|
||||
),
|
||||
(
|
||||
"registration_privacy_policy_content".to_string(),
|
||||
json!("<p>Policy</p>"),
|
||||
),
|
||||
(
|
||||
"registration_privacy_policy_version".to_string(),
|
||||
json!("2026-05-16"),
|
||||
),
|
||||
]);
|
||||
|
||||
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
||||
@@ -1464,6 +1489,12 @@ async fn gateway_handles_auth_registration_settings_without_proxying_upstream()
|
||||
"turnstile_enabled": true,
|
||||
"turnstile_site_key": "site-public-key",
|
||||
"turnstile_required_actions": ["send_verification_code", "register"],
|
||||
"privacy_policy": {
|
||||
"enabled": true,
|
||||
"format": "html",
|
||||
"content": "<p>Policy</p>",
|
||||
"version": "2026-05-16",
|
||||
},
|
||||
})
|
||||
);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
@@ -2839,6 +2870,7 @@ async fn gateway_reads_announcement_unread_count_locally_without_proxying_upstre
|
||||
10,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -2855,6 +2887,7 @@ async fn gateway_reads_announcement_unread_count_locally_without_proxying_upstre
|
||||
8,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -2871,6 +2904,7 @@ async fn gateway_reads_announcement_unread_count_locally_without_proxying_upstre
|
||||
6,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -2917,6 +2951,122 @@ async fn gateway_reads_announcement_unread_count_locally_without_proxying_upstre
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_lists_required_unread_announcements_locally_without_proxying_upstream() {
|
||||
let now = Utc::now();
|
||||
let user = sample_auth_user(now);
|
||||
let access_token = build_test_auth_token(
|
||||
"access",
|
||||
serde_json::Map::from_iter([
|
||||
("user_id".to_string(), json!(user.id)),
|
||||
("role".to_string(), json!(user.role)),
|
||||
(
|
||||
"created_at".to_string(),
|
||||
json!(user.created_at.map(|value| value.to_rfc3339())),
|
||||
),
|
||||
(
|
||||
"session_id".to_string(),
|
||||
json!("session-announcement-required-1"),
|
||||
),
|
||||
]),
|
||||
now + chrono::Duration::hours(1),
|
||||
);
|
||||
let announcement_repository = Arc::new(InMemoryAnnouncementReadRepository::seed_with_reads(
|
||||
vec![
|
||||
StoredAnnouncement::new(
|
||||
"announcement-required".to_string(),
|
||||
"必读公告".to_string(),
|
||||
"需要确认".to_string(),
|
||||
"important".to_string(),
|
||||
20,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
None,
|
||||
now.timestamp(),
|
||||
now.timestamp(),
|
||||
)
|
||||
.expect("announcement should build"),
|
||||
StoredAnnouncement::new(
|
||||
"announcement-normal".to_string(),
|
||||
"普通公告".to_string(),
|
||||
"不需要弹窗".to_string(),
|
||||
"info".to_string(),
|
||||
10,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
None,
|
||||
now.timestamp(),
|
||||
now.timestamp(),
|
||||
)
|
||||
.expect("announcement should build"),
|
||||
StoredAnnouncement::new(
|
||||
"announcement-read-required".to_string(),
|
||||
"已读必读公告".to_string(),
|
||||
"已经确认".to_string(),
|
||||
"warning".to_string(),
|
||||
8,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
None,
|
||||
now.timestamp(),
|
||||
now.timestamp(),
|
||||
)
|
||||
.expect("announcement should build"),
|
||||
],
|
||||
[(
|
||||
"user-auth-1".to_string(),
|
||||
"announcement-read-required".to_string(),
|
||||
)],
|
||||
));
|
||||
let (gateway_url, upstream_hits, gateway_handle, upstream_handle) =
|
||||
start_auth_announcement_gateway_with_state(
|
||||
user,
|
||||
sample_auth_wallet("user-auth-1", now),
|
||||
[sample_auth_session(
|
||||
"user-auth-1",
|
||||
"session-announcement-required-1",
|
||||
"device-announcement-required-1",
|
||||
"refresh-token-placeholder",
|
||||
now,
|
||||
)],
|
||||
announcement_repository,
|
||||
)
|
||||
.await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(format!(
|
||||
"{gateway_url}/api/announcements/users/me/required-unread"
|
||||
))
|
||||
.header("authorization", format!("Bearer {access_token}"))
|
||||
.header("x-client-device-id", "device-announcement-required-1")
|
||||
.header("user-agent", "AetherTest/1.0")
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["total"], 1);
|
||||
assert_eq!(payload["items"][0]["id"], "announcement-required");
|
||||
assert_eq!(payload["items"][0]["requires_ack"], true);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_marks_announcement_read_status_locally_without_proxying_upstream() {
|
||||
let now = Utc::now();
|
||||
@@ -2946,6 +3096,7 @@ async fn gateway_marks_announcement_read_status_locally_without_proxying_upstrea
|
||||
20,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -3040,6 +3191,7 @@ async fn gateway_marks_all_announcements_read_locally_without_proxying_upstream(
|
||||
10,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -3056,6 +3208,7 @@ async fn gateway_marks_all_announcements_read_locally_without_proxying_upstream(
|
||||
8,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -3072,6 +3225,7 @@ async fn gateway_marks_all_announcements_read_locally_without_proxying_upstream(
|
||||
6,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -3163,6 +3317,7 @@ async fn gateway_handles_announcement_user_routes_with_trailing_slash_locally()
|
||||
10,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -3323,6 +3478,7 @@ async fn gateway_rejects_invalid_nested_announcement_paths_as_local_not_found_wi
|
||||
10,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Some("admin-1".to_string()),
|
||||
Some("admin".to_string()),
|
||||
None,
|
||||
@@ -7918,6 +8074,54 @@ async fn gateway_handles_auth_register_locally_without_proxying_upstream() {
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_rejects_auth_register_without_current_privacy_policy_acceptance() {
|
||||
let (gateway_url, upstream_hits, gateway_handle, upstream_handle) =
|
||||
start_auth_gateway_with_builder(|| {
|
||||
let data_state = crate::data::GatewayDataState::disabled()
|
||||
.with_system_config_values_for_tests(vec![
|
||||
("enable_registration".to_string(), json!(true)),
|
||||
("require_email_verification".to_string(), json!(true)),
|
||||
("smtp_host".to_string(), json!("smtp.example.com")),
|
||||
("smtp_from_email".to_string(), json!("[email protected]")),
|
||||
(
|
||||
"registration_privacy_policy_enabled".to_string(),
|
||||
json!(true),
|
||||
),
|
||||
(
|
||||
"registration_privacy_policy_version".to_string(),
|
||||
json!("2026-05-16"),
|
||||
),
|
||||
]);
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_auth_email_verified_for_tests("[email protected]")
|
||||
})
|
||||
.await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.post(format!("{gateway_url}/api/auth/register"))
|
||||
.json(&json!({
|
||||
"email": "[email protected]",
|
||||
"username": "alice",
|
||||
"password": "secret123",
|
||||
"privacy_policy_accepted": true,
|
||||
"privacy_policy_version": "old-version",
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("register request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["detail"], "请先阅读并同意当前版本的隐私政策");
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
async fn start_turnstile_siteverify_server(
|
||||
response_payload: serde_json::Value,
|
||||
status: StatusCode,
|
||||
|
||||
Reference in New Issue
Block a user