Files
farm_service/tests/users.rs
T

232 lines
7.1 KiB
Rust
Raw Normal View History

2026-10-23 19:18:50 +08:00
use axum::{
Router,
body::{Body, to_bytes},
http::{Request, StatusCode},
response::Response,
};
use farm_service::{api, db};
use serde_json::{Value, json};
use sqlx::SqlitePool;
use tempfile::TempDir;
use tower::ServiceExt;
const TOKEN: &str = "integration-test-token-not-for-production";
async fn app() -> (TempDir, SqlitePool, Router) {
let directory = tempfile::tempdir().unwrap();
let pool = db::open_database(&directory.path().join("test.sqlite3"))
.await
.unwrap();
let router = api::router(pool.clone(), TOKEN.into());
(directory, pool, router)
}
fn profile(platform: &str, id: &str, nickname: &str) -> Value {
json!({
"platform": platform,
"platform_user_id": id,
"nickname": nickname
})
}
async fn send(app: Router, method: &str, uri: &str, body: Value, authenticated: bool) -> Response {
let mut request = Request::builder()
.method(method)
.uri(uri)
.header("content-type", "application/json");
if authenticated {
request = request.header("authorization", format!("Bearer {TOKEN}"));
}
app.oneshot(request.body(Body::from(body.to_string())).unwrap())
.await
.unwrap()
}
async fn json_body(response: Response) -> Value {
let bytes = to_bytes(response.into_body(), 8192).await.unwrap();
serde_json::from_slice(&bytes).unwrap()
}
#[tokio::test]
async fn health_checks_database_readiness() {
let (_directory, pool, router) = app().await;
let response = send(router.clone(), "GET", "/health", Value::Null, false).await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(json_body(response).await, json!({"status": "ok"}));
pool.close().await;
let response = send(router, "GET", "/health", Value::Null, false).await;
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn authentication_prevents_profile_reads_and_writes() {
let (_directory, pool, router) = app().await;
for (method, uri) in [("PUT", "/v1/users"), ("GET", "/v1/users/qq/12345678")] {
let response = send(
router.clone(),
method,
uri,
profile("qq", "12345678", "测试昵称"),
false,
)
.await;
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
let response = router
.oneshot(
Request::builder()
.method("PUT")
.uri("/v1/users")
.header("authorization", "Bearer incorrect-token")
.header("content-type", "application/json")
.body(Body::from(profile("qq", "12345678", "测试").to_string()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 0);
pool.close().await;
}
#[tokio::test]
async fn profile_updates_are_persistent_and_preserve_creation_time() {
let (directory, pool, router) = app().await;
let first = send(
router.clone(),
"PUT",
"/v1/users",
profile("qq", "12345678", " 初始昵称 "),
true,
)
.await;
assert_eq!(first.status(), StatusCode::OK);
let first = json_body(first).await;
assert_eq!(first["nickname"], "初始昵称");
let updated = send(
router.clone(),
"PUT",
"/v1/users",
profile("qq", "12345678", "新昵称"),
true,
)
.await;
assert_eq!(updated.status(), StatusCode::OK);
let updated = json_body(updated).await;
assert_eq!(first["created_at"], updated["created_at"]);
assert_eq!(updated["platform_user_id"], "12345678");
drop(router);
pool.close().await;
let reopened = db::open_database(&directory.path().join("test.sqlite3"))
.await
.unwrap();
let response = send(
api::router(reopened.clone(), TOKEN.into()),
"GET",
"/v1/users/qq/12345678",
Value::Null,
true,
)
.await;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(json_body(response).await["nickname"], "新昵称");
reopened.close().await;
}
#[tokio::test]
async fn platforms_do_not_merge_equal_numeric_ids() {
let (_directory, pool, router) = app().await;
for platform in ["qq", "telegram", "discord"] {
let response = send(
router.clone(),
"PUT",
"/v1/users",
profile(platform, "12345678", platform),
true,
)
.await;
assert_eq!(response.status(), StatusCode::OK);
}
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 3);
let missing = send(router, "GET", "/v1/users/qq/99999999", Value::Null, true).await;
assert_eq!(missing.status(), StatusCode::NOT_FOUND);
pool.close().await;
}
#[tokio::test]
async fn invalid_profiles_do_not_write_any_data() {
let (_directory, pool, router) = app().await;
let mut cases = vec![
profile("qq", "0", "昵称"),
profile("qq", "00123456", "昵称"),
profile("qq", "-123", "昵称"),
profile("qq", "123", "昵称"),
profile("qq", "12345678", " "),
profile("qq", "12345678", "换行\n昵称"),
profile("qq", "12345678", &"字".repeat(65)),
profile("qq", &"1".repeat(21), "昵称"),
profile("unknown", "12345678", "昵称"),
json!({"platform": "qq", "platform_user_id": 12345678, "nickname": "昵称"}),
];
let mut unknown = profile("qq", "12345678", "昵称");
unknown["unused_field"] = json!(true);
cases.push(unknown);
for body in cases {
let response = send(router.clone(), "PUT", "/v1/users", body, true).await;
assert!(response.status().is_client_error());
}
let oversized = send(
router,
"PUT",
"/v1/users",
profile("qq", "12345678", &"a".repeat(5000)),
true,
)
.await;
assert_eq!(oversized.status(), StatusCode::PAYLOAD_TOO_LARGE);
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 0);
pool.close().await;
}
#[tokio::test]
async fn concurrent_reports_create_one_profile() {
let (_directory, pool, router) = app().await;
let mut tasks = tokio::task::JoinSet::new();
for index in 0..12 {
let router = router.clone();
tasks.spawn(async move {
send(
router,
"PUT",
"/v1/users",
profile("qq", "12345678", &format!("昵称{index}")),
true,
)
.await
.status()
});
}
while let Some(status) = tasks.join_next().await {
assert_eq!(status.unwrap(), StatusCode::OK);
}
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 1);
pool.close().await;
}