mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-10-12 08:02:47 +00:00
improve testing code
This commit is contained in:
@@ -127,3 +127,98 @@ where
|
||||
let _ = cache.set(key, &value, Some(ttl)).await;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_and_get() {
|
||||
let cache = MemoryCache::new();
|
||||
cache.set("k", "v", None).await.unwrap();
|
||||
assert_eq!(cache.get("k").await.unwrap(), "v");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_missing_returns_none() {
|
||||
let cache = MemoryCache::new();
|
||||
assert!(cache.get("missing").await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_removes_entry() {
|
||||
let cache = MemoryCache::new();
|
||||
cache.set("k", "v", None).await.unwrap();
|
||||
cache.delete("k").await.unwrap();
|
||||
assert!(cache.get("k").await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_overwrites() {
|
||||
let cache = MemoryCache::new();
|
||||
cache.set("k", "v1", None).await.unwrap();
|
||||
cache.set("k", "v2", None).await.unwrap();
|
||||
assert_eq!(cache.get("k").await.unwrap(), "v2");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_prefix_removes_matching() {
|
||||
let cache = MemoryCache::new();
|
||||
cache.set("posts:1", "a", None).await.unwrap();
|
||||
cache.set("posts:2", "b", None).await.unwrap();
|
||||
cache.set("tags:1", "c", None).await.unwrap();
|
||||
let count = cache.delete_prefix("posts:").await.unwrap();
|
||||
assert_eq!(count, 2);
|
||||
assert!(cache.get("posts:1").await.is_none());
|
||||
assert!(cache.get("posts:2").await.is_none());
|
||||
assert_eq!(cache.get("tags:1").await.unwrap(), "c");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn expiry_with_ttl() {
|
||||
let cache = MemoryCache::new();
|
||||
cache
|
||||
.set("k", "v", Some(Duration::from_millis(1)))
|
||||
.await
|
||||
.unwrap();
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
assert!(cache.get("k").await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_or_cache_miss() {
|
||||
let cache: Arc<dyn CacheStore> = Arc::new(MemoryCache::new());
|
||||
let call_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let count_clone = call_count.clone();
|
||||
let result = get_or(&cache, "key", Duration::from_secs(60), || {
|
||||
let c = count_clone.clone();
|
||||
async move {
|
||||
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok("computed".to_string())
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result, "computed");
|
||||
assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_or_cache_hit() {
|
||||
let cache: Arc<dyn CacheStore> = Arc::new(MemoryCache::new());
|
||||
cache.set("key", "cached", None).await.unwrap();
|
||||
let call_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let count_clone = call_count.clone();
|
||||
let result = get_or(&cache, "key", Duration::from_secs(60), || {
|
||||
let c = count_clone.clone();
|
||||
async move {
|
||||
c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok("computed".to_string())
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result, "cached");
|
||||
assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 0);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-15
@@ -13,7 +13,9 @@ use serde_json::{Value, json};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::repository::{ContentQuery, ContentRepository, SaveContext, resolve_document_id_to_int_id};
|
||||
use super::repository::{
|
||||
ContentQuery, ContentRepository, SaveContext, resolve_document_id_to_int_id,
|
||||
};
|
||||
use super::rule_engine::compile_rule_sql;
|
||||
use super::schema::{ContentKind, ContentTypeSchema, FieldType, RelationType, check_api_access};
|
||||
use crate::AppState;
|
||||
@@ -410,13 +412,11 @@ pub async fn do_list(
|
||||
.foreign_key
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{}_id", field.name));
|
||||
let int_id = resolve_document_id_to_int_id(
|
||||
&state.pool, &rel.target, v,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(-1);
|
||||
let int_id = resolve_document_id_to_int_id(&state.pool, &rel.target, v)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(-1);
|
||||
filters.insert(fk_col, json!(int_id));
|
||||
}
|
||||
_ => {}
|
||||
@@ -876,13 +876,11 @@ async fn do_admin_list(
|
||||
.foreign_key
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{}_id", field.name));
|
||||
let int_id = resolve_document_id_to_int_id(
|
||||
&state.pool, &rel.target, v,
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(-1);
|
||||
let int_id = resolve_document_id_to_int_id(&state.pool, &rel.target, v)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(-1);
|
||||
filters.insert(fk_col, json!(int_id));
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -109,7 +109,10 @@ pub fn generate_junction_tables(ct: &ContentTypeSchema) -> Vec<String> {
|
||||
let is_safe = crate::db::dialect::is_safe_identifier;
|
||||
for field in &ct.fields {
|
||||
if let Some(ref rel) = field.relation
|
||||
&& matches!(rel.relation_type, RelationType::ManyToMany | RelationType::ManyWay)
|
||||
&& matches!(
|
||||
rel.relation_type,
|
||||
RelationType::ManyToMany | RelationType::ManyWay
|
||||
)
|
||||
{
|
||||
let through = rel
|
||||
.through
|
||||
|
||||
@@ -790,9 +790,7 @@ impl ContentRepository {
|
||||
.bind(source_int_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AppError::Internal(anyhow::anyhow!("junction delete failed: {e}"))
|
||||
})?;
|
||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("junction delete failed: {e}")))?;
|
||||
|
||||
let doc_ids = extract_document_ids(val);
|
||||
if doc_ids.is_empty() {
|
||||
|
||||
@@ -47,9 +47,7 @@ pub async fn resolve_relations(
|
||||
};
|
||||
|
||||
match rel.relation_type {
|
||||
RelationType::ManyToOne
|
||||
| RelationType::OneToOne
|
||||
| RelationType::OneWay => {
|
||||
RelationType::ManyToOne | RelationType::OneToOne | RelationType::OneWay => {
|
||||
resolve_many_to_one_batch(pool, ct, field, rel, items).await?;
|
||||
}
|
||||
RelationType::OneToMany => {
|
||||
|
||||
+75
@@ -65,3 +65,78 @@ fn validate_uuid_vec(ids: &[String]) -> Result<(), validator::ValidationError> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validate_password_valid() {
|
||||
assert!(validate_password("abc123").is_ok());
|
||||
assert!(validate_password("Password1").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_password_no_digit() {
|
||||
assert!(validate_password("abcdef").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_password_no_letter() {
|
||||
assert!(validate_password("123456").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_post_status_valid() {
|
||||
assert!(validate_post_status("draft").is_ok());
|
||||
assert!(validate_post_status("published").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_post_status_invalid() {
|
||||
assert!(validate_post_status("archived").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_comment_status_valid() {
|
||||
assert!(validate_comment_status("approved").is_ok());
|
||||
assert!(validate_comment_status("pending").is_ok());
|
||||
assert!(validate_comment_status("spam").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_comment_status_invalid() {
|
||||
assert!(validate_comment_status("deleted").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_optional_uuid_valid_uuid() {
|
||||
assert!(validate_optional_uuid("01901234-5678-7000-8000-000000000000").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_optional_uuid_valid_i64() {
|
||||
assert!(validate_optional_uuid("42").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_optional_uuid_invalid() {
|
||||
assert!(validate_optional_uuid("not-a-uuid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_uuid_vec_valid() {
|
||||
assert!(
|
||||
validate_uuid_vec(&[
|
||||
"01901234-5678-7000-8000-000000000000".to_string(),
|
||||
"1".to_string()
|
||||
])
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_uuid_vec_invalid() {
|
||||
assert!(validate_uuid_vec(&["bad-id".to_string()]).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,3 +25,41 @@ pub struct UpdateCategoryRequest {
|
||||
pub parent_id: Option<String>,
|
||||
pub sort_order: Option<i64>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn create_category_valid() {
|
||||
let req = CreateCategoryRequest {
|
||||
name: "Tech".to_string(),
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_category_empty_name_fails() {
|
||||
let req = CreateCategoryRequest {
|
||||
name: "".to_string(),
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_category_valid() {
|
||||
let req = UpdateCategoryRequest {
|
||||
name: Some("New".to_string()),
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,3 +23,46 @@ pub struct UpdateCommentStatusRequest {
|
||||
#[validate(custom(function = "validate_comment_status"))]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn create_comment_valid() {
|
||||
let req = CreateCommentRequest {
|
||||
content: "Nice post!".to_string(),
|
||||
parent_id: None,
|
||||
nickname: Some("Guest".to_string()),
|
||||
email: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_comment_empty_content_fails() {
|
||||
let req = CreateCommentRequest {
|
||||
content: "".to_string(),
|
||||
parent_id: None,
|
||||
nickname: None,
|
||||
email: None,
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_status_valid() {
|
||||
let req = UpdateCommentStatusRequest {
|
||||
status: "approved".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_status_invalid() {
|
||||
let req = UpdateCommentStatusRequest {
|
||||
status: "deleted".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,3 +15,32 @@ pub struct UpdateTagRequest {
|
||||
#[validate(length(min = 1, max = 50))]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn create_tag_valid() {
|
||||
let req = CreateTagRequest {
|
||||
name: "rust".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_tag_empty_name_fails() {
|
||||
let req = CreateTagRequest {
|
||||
name: "".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_tag_valid() {
|
||||
let req = UpdateTagRequest {
|
||||
name: "updated".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
+120
@@ -181,3 +181,123 @@ pub struct LoginResponse {
|
||||
pub expires_in: u64,
|
||||
pub user: UserResponse,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn register_request_valid() {
|
||||
let req = RegisterRequest {
|
||||
email: "test@example.com".to_string(),
|
||||
username: "testuser".to_string(),
|
||||
password: "Password1".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_request_bad_email() {
|
||||
let req = RegisterRequest {
|
||||
email: "not-an-email".to_string(),
|
||||
username: "testuser".to_string(),
|
||||
password: "Password1".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_request_short_username() {
|
||||
let req = RegisterRequest {
|
||||
email: "test@example.com".to_string(),
|
||||
username: "a".to_string(),
|
||||
password: "Password1".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_request_short_password() {
|
||||
let req = RegisterRequest {
|
||||
email: "test@example.com".to_string(),
|
||||
username: "testuser".to_string(),
|
||||
password: "short".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_request_password_no_digit() {
|
||||
let req = RegisterRequest {
|
||||
email: "test@example.com".to_string(),
|
||||
username: "testuser".to_string(),
|
||||
password: "passwordonly".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_request_valid() {
|
||||
let req = LoginRequest {
|
||||
email: "test@example.com".to_string(),
|
||||
password: "anypassword".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_request_empty_password() {
|
||||
let req = LoginRequest {
|
||||
email: "test@example.com".to_string(),
|
||||
password: "".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_user_request_valid() {
|
||||
let req = UpdateUserRequest {
|
||||
username: Some("newname".to_string()),
|
||||
bio: None,
|
||||
website: None,
|
||||
avatar: None,
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_password_request_valid() {
|
||||
let req = UpdatePasswordRequest {
|
||||
old_password: "OldPass1".to_string(),
|
||||
new_password: "NewPass2".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_password_request_weak_new_password() {
|
||||
let req = UpdatePasswordRequest {
|
||||
old_password: "OldPass1".to_string(),
|
||||
new_password: "abcdefgh".to_string(),
|
||||
};
|
||||
assert!(req.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_response_from_user_serializes() {
|
||||
let resp = UserResponse {
|
||||
id: "doc-123".to_string(),
|
||||
email: "a@b.com".to_string(),
|
||||
username: "test".to_string(),
|
||||
role: "reader".to_string(),
|
||||
phone: None,
|
||||
avatar: None,
|
||||
bio: None,
|
||||
website: None,
|
||||
created_at: "2025-01-01T00:00:00Z".to_string(),
|
||||
updated_at: "2025-01-01T00:00:00Z".to_string(),
|
||||
};
|
||||
let json = serde_json::to_string(&resp).unwrap();
|
||||
assert!(json.contains("\"id\":\"doc-123\""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,3 +267,46 @@ impl From<sqlx::Error> for AppError {
|
||||
/// 所有 handler 和 service 函数的返回类型统一使用 `AppResult<T>`,
|
||||
/// 等价于 `Result<T, AppError>`,简化函数签名并保证错误处理的一致性。
|
||||
pub type AppResult<T> = Result<T, AppError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn not_found_convenience() {
|
||||
let err = AppError::not_found("post");
|
||||
match err {
|
||||
AppError::NotFound(r) => assert_eq!(r, "post"),
|
||||
_ => panic!("expected NotFound"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expect_affected_zero_rows() {
|
||||
let result = sqlx::sqlite::SqliteQueryResult::default();
|
||||
let outcome = AppError::expect_affected(&result, "user");
|
||||
assert!(outcome.is_err());
|
||||
match outcome.unwrap_err() {
|
||||
AppError::NotFound(r) => assert_eq!(r, "user"),
|
||||
_ => panic!("expected NotFound"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_sqlx_row_not_found() {
|
||||
let err: AppError = sqlx::Error::RowNotFound.into();
|
||||
match err {
|
||||
AppError::NotFound(_) => {}
|
||||
_ => panic!("expected NotFound"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_anyhow() {
|
||||
let err = AppError::Internal(anyhow::anyhow!("boom"));
|
||||
match err {
|
||||
AppError::Internal(e) => assert_eq!(e.to_string(), "boom"),
|
||||
_ => panic!("expected Internal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,9 +28,9 @@ pub mod oauth;
|
||||
pub mod options;
|
||||
pub mod page;
|
||||
pub mod plugin;
|
||||
pub mod reusable_block;
|
||||
pub mod post;
|
||||
pub mod rbac;
|
||||
pub mod reusable_block;
|
||||
pub mod rss;
|
||||
pub mod sse;
|
||||
pub mod stats;
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
|
||||
use crate::errors::app_error::AppResult;
|
||||
use crate::errors::response::ApiResponse;
|
||||
use crate::errors::validation;
|
||||
use crate::dto::{
|
||||
AuthConfigResponse, BindPhoneRequest, ForgotPasswordRequest, LoginRequest, RefreshRequest,
|
||||
RegisterRequest, ResendVerificationRequest, ResetPasswordRequest, SendSmsCodeRequest,
|
||||
SetPasswordRequest, VerifyEmailRequest, VerifySmsRequest,
|
||||
};
|
||||
use crate::errors::app_error::AppResult;
|
||||
use crate::errors::response::ApiResponse;
|
||||
use crate::errors::validation;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::{auth, email_verification, password_reset, sms};
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, Query, State};
|
||||
|
||||
use crate::dto::{CreateCategoryRequest, UpdateCategoryRequest};
|
||||
use crate::errors::app_error::AppResult;
|
||||
use crate::errors::response::ApiResponse;
|
||||
use crate::errors::validation;
|
||||
use crate::dto::{CreateCategoryRequest, UpdateCategoryRequest};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::category;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
|
||||
use crate::dto::{CreateCommentRequest, UpdateCommentStatusRequest};
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::errors::response::{ApiResponse, PaginatedData};
|
||||
use crate::errors::validation;
|
||||
use crate::dto::{CreateCommentRequest, UpdateCommentStatusRequest};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::comment as comment_service;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
@@ -101,7 +101,5 @@ pub async fn stats(
|
||||
State(state): State<crate::AppState>,
|
||||
) -> AppResult<ApiResponse<crate::dto::MediaStatsResponse>> {
|
||||
let s = media_service::stats(state.media_repo.as_ref(), &state.pool, &auth).await?;
|
||||
Ok(ApiResponse::success(
|
||||
crate::dto::stats_to_response(&s),
|
||||
))
|
||||
Ok(ApiResponse::success(crate::dto::stats_to_response(&s)))
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ async fn resolve_page_parent_id(
|
||||
pool: &crate::db::Pool,
|
||||
parent_id: Option<String>,
|
||||
) -> AppResult<Option<i64>> {
|
||||
let Some(doc_id) = parent_id else { return Ok(None) };
|
||||
let Some(doc_id) = parent_id else {
|
||||
return Ok(None);
|
||||
};
|
||||
resolve_doc_id_to_int(pool, "pages", &doc_id, None).await
|
||||
}
|
||||
|
||||
@@ -180,8 +182,7 @@ pub async fn create(
|
||||
let template = req.template.unwrap_or_else(|| "default".to_string());
|
||||
let status = req.status.unwrap_or_else(|| "draft".to_string());
|
||||
|
||||
let resolved_parent_id =
|
||||
resolve_page_parent_id(&state.pool, req.parent_id).await?;
|
||||
let resolved_parent_id = resolve_page_parent_id(&state.pool, req.parent_id).await?;
|
||||
let cmd = CreatePageCmd {
|
||||
title: req.title,
|
||||
slug,
|
||||
@@ -212,11 +213,7 @@ pub async fn update(
|
||||
auth.ensure_author()?;
|
||||
validation::validate(&req)?;
|
||||
|
||||
let resolved_parent_id = resolve_page_parent_id(
|
||||
&state.pool,
|
||||
req.parent_id.flatten(),
|
||||
)
|
||||
.await?;
|
||||
let resolved_parent_id = resolve_page_parent_id(&state.pool, req.parent_id.flatten()).await?;
|
||||
let cmd = UpdatePageCmd {
|
||||
id: 0,
|
||||
title: req.title,
|
||||
|
||||
@@ -7,10 +7,10 @@ use axum::Json;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::dto::{CreatePostRequest, PostResponse, UpdatePostRequest};
|
||||
use crate::errors::app_error::AppResult;
|
||||
use crate::errors::response::{ApiResponse, PaginatedData};
|
||||
use crate::errors::validation;
|
||||
use crate::dto::{CreatePostRequest, PostResponse, UpdatePostRequest};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::post as post_service;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
+1
-1
@@ -3,10 +3,10 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, Query, State};
|
||||
|
||||
use crate::dto::{CreateTagRequest, UpdateTagRequest};
|
||||
use crate::errors::app_error::AppResult;
|
||||
use crate::errors::response::ApiResponse;
|
||||
use crate::errors::validation;
|
||||
use crate::dto::{CreateTagRequest, UpdateTagRequest};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::tag;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
@@ -5,12 +5,10 @@
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, Query, State};
|
||||
|
||||
use crate::dto::{UpdatePasswordRequest, UpdateRoleRequest, UpdateUserRequest, UserResponse};
|
||||
use crate::errors::app_error::AppResult;
|
||||
use crate::errors::response::{ApiResponse, PaginatedData};
|
||||
use crate::errors::validation;
|
||||
use crate::dto::{
|
||||
UpdatePasswordRequest, UpdateRoleRequest, UpdateUserRequest, UserResponse,
|
||||
};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::{auth, user};
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
+1
-1
@@ -27,11 +27,11 @@ pub mod oauth;
|
||||
pub mod options;
|
||||
pub mod page;
|
||||
pub mod password_reset;
|
||||
pub mod reusable_block;
|
||||
pub mod plugin_storage;
|
||||
pub mod post;
|
||||
pub mod rbac;
|
||||
pub mod refresh_token;
|
||||
pub mod reusable_block;
|
||||
pub mod sms_code;
|
||||
pub mod tag;
|
||||
pub mod tenant;
|
||||
|
||||
@@ -248,3 +248,110 @@ pub async fn delete(pool: &crate::db::Pool, id: i64, tenant_id: Option<&str>) ->
|
||||
|
||||
AppError::expect_affected(&result, "category")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::commands::category::CreateCategoryCmd;
|
||||
use crate::commands::category::UpdateCategoryCmd;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
fn make_cmd(name: &str) -> CreateCategoryCmd {
|
||||
CreateCategoryCmd {
|
||||
name: name.to_string(),
|
||||
slug: name.to_lowercase(),
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_id() {
|
||||
let pool = setup_pool().await;
|
||||
let cat = create(&pool, &make_cmd("Tech"), None, None).await.unwrap();
|
||||
let found = find_by_id(&pool, cat.id, None).await.unwrap();
|
||||
assert_eq!(found.id, cat.id);
|
||||
assert_eq!(found.name, "Tech");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_document_id() {
|
||||
let pool = setup_pool().await;
|
||||
let cat = create(&pool, &make_cmd("Science"), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let found = super::find_by_document_id(&pool, &cat.document_id, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(found.id, cat.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_all_returns_all() {
|
||||
let pool = setup_pool().await;
|
||||
create(&pool, &make_cmd("A"), None, None).await.unwrap();
|
||||
create(&pool, &make_cmd("B"), None, None).await.unwrap();
|
||||
create(&pool, &make_cmd("C"), None, None).await.unwrap();
|
||||
let all = find_all(&pool, None).await.unwrap();
|
||||
assert_eq!(all.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_paginated() {
|
||||
let pool = setup_pool().await;
|
||||
for name in ["A", "B", "C", "D", "E"] {
|
||||
create(&pool, &make_cmd(name), None, None).await.unwrap();
|
||||
}
|
||||
let (items, total) = super::find_paginated(&pool, None, 1, 3).await.unwrap();
|
||||
assert_eq!(total, 5);
|
||||
assert_eq!(items.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_changes_name() {
|
||||
let pool = setup_pool().await;
|
||||
let cat = create(&pool, &make_cmd("Old"), None, None).await.unwrap();
|
||||
let updated = update(
|
||||
&pool,
|
||||
&UpdateCategoryCmd {
|
||||
id: cat.id,
|
||||
name: Some("New".to_string()),
|
||||
slug: None,
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.name, "New");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_removes_category() {
|
||||
let pool = setup_pool().await;
|
||||
let cat = create(&pool, &make_cmd("Gone"), None, None).await.unwrap();
|
||||
delete(&pool, cat.id, None).await.unwrap();
|
||||
let result = find_by_id(&pool, cat.id, None).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_id_not_found() {
|
||||
let pool = setup_pool().await;
|
||||
let result = find_by_id(&pool, 99999, None).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,4 +458,180 @@ mod tests {
|
||||
let comments = vec![make_comment(1, 10, None)];
|
||||
assert!(validate_depth(&comments, 999).is_err());
|
||||
}
|
||||
|
||||
mod integration {
|
||||
use super::*;
|
||||
use crate::commands::CreateCommentCmd;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
async fn insert_user(pool: &crate::db::Pool) -> i64 {
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
sqlx::query(
|
||||
"INSERT INTO users (document_id, username, email, password_hash, role) VALUES (?, 'testuser', 'test@test.com', 'hash', 'author')",
|
||||
)
|
||||
.bind(&doc_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?")
|
||||
.bind(&doc_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
async fn insert_post(pool: &crate::db::Pool, user_id: i64) -> i64 {
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let slug = format!("slug-{doc_id}");
|
||||
sqlx::query(
|
||||
"INSERT INTO posts (document_id, title, slug, content, status, created_by, updated_by) VALUES (?, 'Test', ?, 'content', 'published', ?, ?)",
|
||||
)
|
||||
.bind(&doc_id)
|
||||
.bind(&slug)
|
||||
.bind(user_id)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (id,): (i64,) = sqlx::query_as("SELECT id FROM posts WHERE document_id = ?")
|
||||
.bind(&doc_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
fn make_cmd(post_id: i64) -> CreateCommentCmd {
|
||||
CreateCommentCmd {
|
||||
post_id,
|
||||
created_by: None,
|
||||
nickname: Some("Alice".into()),
|
||||
email: Some("alice@test.com".into()),
|
||||
content: "hello".into(),
|
||||
parent_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_id() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let pid = insert_post(&pool, uid).await;
|
||||
let c = create(&pool, &make_cmd(pid), None).await.unwrap();
|
||||
assert_eq!(c.post_id, pid);
|
||||
assert_eq!(c.content, "hello");
|
||||
let found = super::find_by_id(&pool, c.id, None).await.unwrap().unwrap();
|
||||
assert_eq!(found.id, c.id);
|
||||
assert_eq!(found.document_id, c.document_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_document_id_test() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let pid = insert_post(&pool, uid).await;
|
||||
let c = create(&pool, &make_cmd(pid), None).await.unwrap();
|
||||
let found = super::find_by_document_id(&pool, &c.document_id, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(found.id, c.id);
|
||||
assert_eq!(found.content, "hello");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_approved_by_post_returns_only_approved() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let pid = insert_post(&pool, uid).await;
|
||||
let c1 = create(&pool, &make_cmd(pid), None).await.unwrap();
|
||||
let _c2 = create(&pool, &make_cmd(pid), None).await.unwrap();
|
||||
update_status(&pool, c1.id, "approved", None).await.unwrap();
|
||||
|
||||
let approved = super::find_approved_by_post(&pool, pid, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(approved.len(), 1);
|
||||
assert_eq!(approved[0].id, c1.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_approved_by_post_paginated_test() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let pid = insert_post(&pool, uid).await;
|
||||
let mut ids = Vec::new();
|
||||
for i in 0..5 {
|
||||
let mut cmd = make_cmd(pid);
|
||||
cmd.content = format!("comment {i}");
|
||||
let c = create(&pool, &cmd, None).await.unwrap();
|
||||
update_status(&pool, c.id, "approved", None).await.unwrap();
|
||||
ids.push(c.id);
|
||||
}
|
||||
|
||||
let (page1, total) = super::find_approved_by_post_paginated(&pool, pid, 1, 2, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(total, 5);
|
||||
assert_eq!(page1.len(), 2);
|
||||
|
||||
let (page3, _) = super::find_approved_by_post_paginated(&pool, pid, 3, 2, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(page3.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_status_changes_status() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let pid = insert_post(&pool, uid).await;
|
||||
let c = create(&pool, &make_cmd(pid), None).await.unwrap();
|
||||
assert_eq!(c.status, "pending");
|
||||
update_status(&pool, c.id, "approved", None).await.unwrap();
|
||||
let found = super::find_by_id(&pool, c.id, None).await.unwrap().unwrap();
|
||||
assert_eq!(found.status, "approved");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_removes_comment() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let pid = insert_post(&pool, uid).await;
|
||||
let c = create(&pool, &make_cmd(pid), None).await.unwrap();
|
||||
super::delete(&pool, c.id, None).await.unwrap();
|
||||
let found = super::find_by_id(&pool, c.id, None).await.unwrap();
|
||||
assert!(found.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_all_paginated_test() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let pid = insert_post(&pool, uid).await;
|
||||
for i in 0..5 {
|
||||
let mut cmd = make_cmd(pid);
|
||||
cmd.content = format!("comment {i}");
|
||||
create(&pool, &cmd, None).await.unwrap();
|
||||
}
|
||||
|
||||
let (page1, total) = super::find_all_paginated(&pool, 1, 2, None).await.unwrap();
|
||||
assert_eq!(total, 5);
|
||||
assert_eq!(page1.len(), 2);
|
||||
|
||||
let (page3, _) = super::find_all_paginated(&pool, 3, 2, None).await.unwrap();
|
||||
assert_eq!(page3.len(), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,3 +114,88 @@ pub async fn cleanup_expired(pool: &crate::db::Pool) -> AppResult<u64> {
|
||||
let result = sqlx::query(&sql).bind(now).execute(pool).await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
async fn insert_user(pool: &crate::db::Pool, document_id: &str) -> i64 {
|
||||
let hash = "$argon2id$v=19$m=19456,t=2,p=1$test$test".to_string();
|
||||
let sql = format!(
|
||||
"INSERT INTO users (document_id, email, username, password_hash, role) VALUES ({}, {}, {}, {}, 'admin') RETURNING id",
|
||||
ph(1),
|
||||
ph(2),
|
||||
ph(3),
|
||||
ph(4)
|
||||
);
|
||||
let (id,): (i64,) = sqlx::query_as(&sql)
|
||||
.bind(document_id)
|
||||
.bind("ev-test@test.com")
|
||||
.bind("evtestuser")
|
||||
.bind(&hash)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_token() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool, "ev-user-1-doc").await;
|
||||
let row = create(&pool, user_id, "ev1@test.com", 3600).await.unwrap();
|
||||
let found = find_by_token(&pool, &row.token).await.unwrap().unwrap();
|
||||
assert_eq!(found.id, row.id);
|
||||
assert_eq!(found.token, row.token);
|
||||
assert_eq!(found.email, "ev1@test.com");
|
||||
assert!(found.verified_at.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mark_verified() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool, "ev-user-2-doc").await;
|
||||
let row = create(&pool, user_id, "ev2@test.com", 3600).await.unwrap();
|
||||
assert!(row.verified_at.is_none());
|
||||
super::mark_verified(&pool, row.id).await.unwrap();
|
||||
let found = find_by_token(&pool, &row.token).await.unwrap();
|
||||
assert!(found.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_unused_by_user() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool, "ev-user-3-doc").await;
|
||||
create(&pool, user_id, "ev3a@test.com", 3600).await.unwrap();
|
||||
create(&pool, user_id, "ev3b@test.com", 3600).await.unwrap();
|
||||
super::delete_unused_by_user(&pool, user_id).await.unwrap();
|
||||
let sql = format!(
|
||||
"SELECT COUNT(*) FROM email_verification_tokens WHERE user_id = {}",
|
||||
ph(1),
|
||||
);
|
||||
let (count,): (i64,) = sqlx::query_as(&sql)
|
||||
.bind(user_id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cleanup_expired() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool, "ev-user-4-doc").await;
|
||||
create(&pool, user_id, "ev4@test.com", -1).await.unwrap();
|
||||
let removed = super::cleanup_expired(&pool).await.unwrap();
|
||||
assert_eq!(removed, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,3 +235,113 @@ pub async fn delete(pool: &crate::db::Pool, id: i64, tenant_id: Option<&str>) ->
|
||||
|
||||
AppError::expect_affected(&result, "media")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::commands::media::CreateMediaCmd;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
async fn insert_user(pool: &crate::db::Pool) -> i64 {
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let sql = format!(
|
||||
"INSERT INTO users (document_id, email, username, password_hash, role) VALUES ({}, {}, {}, {}, 'admin') RETURNING id",
|
||||
crate::db::dialect::ph(1),
|
||||
crate::db::dialect::ph(2),
|
||||
crate::db::dialect::ph(3),
|
||||
crate::db::dialect::ph(4)
|
||||
);
|
||||
let (id,): (i64,) = sqlx::query_as(&sql)
|
||||
.bind(&doc_id)
|
||||
.bind("media-test@test.com")
|
||||
.bind("mediauser")
|
||||
.bind("$argon2id$v=19$m=19456,t=2,p=1$test$test")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
fn make_cmd(user_id: i64, filename: &str) -> CreateMediaCmd {
|
||||
CreateMediaCmd {
|
||||
user_id,
|
||||
filename: filename.to_string(),
|
||||
filepath: format!("/uploads/{filename}"),
|
||||
mimetype: "image/png".to_string(),
|
||||
size: 1024,
|
||||
width: Some(100),
|
||||
height: Some(100),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_id() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let media = create(&pool, &make_cmd(uid, "photo.png"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let found = find_by_id(&pool, media.id, None).await.unwrap().unwrap();
|
||||
assert_eq!(found.id, media.id);
|
||||
assert_eq!(found.filename, "photo.png");
|
||||
assert_eq!(found.user_id, uid);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_all_paginated() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
for i in 0..5 {
|
||||
create(&pool, &make_cmd(uid, &format!("file{i}.png")), None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let (items, total) = find_all(&pool, uid, 1, 3, None).await.unwrap();
|
||||
assert_eq!(total, 5);
|
||||
assert_eq!(items.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stats_returns_counts() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
for i in 0..3 {
|
||||
create(&pool, &make_cmd(uid, &format!("img{i}.png")), None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let s = stats(&pool, uid, None).await.unwrap();
|
||||
assert_eq!(s.total_files, 3);
|
||||
assert_eq!(s.total_size, 3 * 1024);
|
||||
assert_eq!(s.by_type.len(), 1);
|
||||
assert_eq!(s.by_type[0].mimetype, "image/png");
|
||||
assert_eq!(s.by_type[0].count, 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_removes_media() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let media = create(&pool, &make_cmd(uid, "gone.png"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
delete(&pool, media.id, None).await.unwrap();
|
||||
let found = find_by_id(&pool, media.id, None).await.unwrap();
|
||||
assert!(found.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_id_not_found() {
|
||||
let pool = setup_pool().await;
|
||||
let found = find_by_id(&pool, 99999, None).await.unwrap();
|
||||
assert!(found.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,3 +278,213 @@ pub async fn count_by_user(pool: &crate::db::Pool, user_id: i64) -> AppResult<i6
|
||||
.await?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
async fn insert_user(pool: &crate::db::Pool) -> i64 {
|
||||
let cmd = crate::commands::user::CreateUserCmd {
|
||||
email: format!("{}@test.com", crate::utils::id::new_document_id()),
|
||||
username: crate::utils::id::new_document_id(),
|
||||
password_hash: "$argon2id$v=19$m=19456,t=2,p=1$test$test".to_string(),
|
||||
};
|
||||
let user = crate::models::user::create(pool, &cmd, None).await.unwrap();
|
||||
user.id
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_consume_state() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
let doc_id = create_state(
|
||||
&pool,
|
||||
"github",
|
||||
"verifier123",
|
||||
Some(user_id),
|
||||
"2099-12-31T00:00:00Z",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let state = consume_state(&pool, &doc_id).await.unwrap().unwrap();
|
||||
assert_eq!(state.document_id, doc_id);
|
||||
assert_eq!(state.provider, "github");
|
||||
assert_eq!(state.code_verifier, "verifier123");
|
||||
assert_eq!(state.user_id, Some(user_id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn consume_state_twice_returns_none() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
let doc_id = create_state(
|
||||
&pool,
|
||||
"github",
|
||||
"verifier123",
|
||||
Some(user_id),
|
||||
"2099-12-31T00:00:00Z",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let first = consume_state(&pool, &doc_id).await.unwrap();
|
||||
assert!(first.is_some());
|
||||
let second = consume_state(&pool, &doc_id).await.unwrap();
|
||||
assert!(second.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_account() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
let account = create_account(
|
||||
&pool,
|
||||
CreateOAuthAccountParams {
|
||||
user_id,
|
||||
provider: "github",
|
||||
provider_user_id: "github-123",
|
||||
email: Some("user@example.com"),
|
||||
display_name: Some("Test User"),
|
||||
avatar_url: None,
|
||||
access_token: None,
|
||||
refresh_token: None,
|
||||
token_expires_at: None,
|
||||
profile: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let found = find_by_provider_user(&pool, "github", "github-123")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(found.id, account.id);
|
||||
assert_eq!(found.provider, "github");
|
||||
assert_eq!(found.provider_user_id, "github-123");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_user_id() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
create_account(
|
||||
&pool,
|
||||
CreateOAuthAccountParams {
|
||||
user_id,
|
||||
provider: "github",
|
||||
provider_user_id: "github-123",
|
||||
email: None,
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
access_token: None,
|
||||
refresh_token: None,
|
||||
token_expires_at: None,
|
||||
profile: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
create_account(
|
||||
&pool,
|
||||
CreateOAuthAccountParams {
|
||||
user_id,
|
||||
provider: "google",
|
||||
provider_user_id: "google-456",
|
||||
email: None,
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
access_token: None,
|
||||
refresh_token: None,
|
||||
token_expires_at: None,
|
||||
profile: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let accounts = super::find_by_user_id(&pool, user_id).await.unwrap();
|
||||
assert_eq!(accounts.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_account() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
create_account(
|
||||
&pool,
|
||||
CreateOAuthAccountParams {
|
||||
user_id,
|
||||
provider: "github",
|
||||
provider_user_id: "github-123",
|
||||
email: None,
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
access_token: None,
|
||||
refresh_token: None,
|
||||
token_expires_at: None,
|
||||
profile: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let deleted = super::delete_account(&pool, user_id, "github")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(deleted);
|
||||
assert!(
|
||||
find_by_provider_user(&pool, "github", "github-123")
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn count_by_user() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
create_account(
|
||||
&pool,
|
||||
CreateOAuthAccountParams {
|
||||
user_id,
|
||||
provider: "github",
|
||||
provider_user_id: "github-123",
|
||||
email: None,
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
access_token: None,
|
||||
refresh_token: None,
|
||||
token_expires_at: None,
|
||||
profile: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
create_account(
|
||||
&pool,
|
||||
CreateOAuthAccountParams {
|
||||
user_id,
|
||||
provider: "google",
|
||||
provider_user_id: "google-456",
|
||||
email: None,
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
access_token: None,
|
||||
refresh_token: None,
|
||||
token_expires_at: None,
|
||||
profile: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let count = super::count_by_user(&pool, user_id).await.unwrap();
|
||||
assert_eq!(count, 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,3 +221,116 @@ pub async fn delete_by_key(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
async fn insert_test_option(
|
||||
pool: &crate::db::Pool,
|
||||
key: &str,
|
||||
value: &str,
|
||||
autoload: bool,
|
||||
updated_at: &str,
|
||||
) {
|
||||
sqlx::query(
|
||||
"INSERT INTO options (document_id, option_key, value, type, group_name, label, autoload, sort_order, updated_at) \
|
||||
VALUES (?, ?, ?, 'text', 'test', 'test', ?, 0, ?)",
|
||||
)
|
||||
.bind(crate::utils::id::new_document_id())
|
||||
.bind(key)
|
||||
.bind(value)
|
||||
.bind(autoload)
|
||||
.bind(updated_at)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upsert_and_find_by_key() {
|
||||
let pool = setup_pool().await;
|
||||
let key = format!("test.{}", crate::utils::id::new_document_id());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
insert_test_option(&pool, &key, "initial", true, &now).await;
|
||||
upsert_value(&pool, &key, "updated", None, &now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let found = find_by_key(&pool, &key, None).await.unwrap();
|
||||
assert!(found.is_some());
|
||||
let row = found.unwrap();
|
||||
assert_eq!(row.option_key, key);
|
||||
assert_eq!(row.value, "updated");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_all_returns_all() {
|
||||
let pool = setup_pool().await;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let k1 = format!("test.{}", crate::utils::id::new_document_id());
|
||||
let k2 = format!("test.{}", crate::utils::id::new_document_id());
|
||||
let k3 = format!("test.{}", crate::utils::id::new_document_id());
|
||||
insert_test_option(&pool, &k1, "v1", true, &now).await;
|
||||
insert_test_option(&pool, &k2, "v2", true, &now).await;
|
||||
insert_test_option(&pool, &k3, "v3", true, &now).await;
|
||||
|
||||
let all = find_all(&pool, None).await.unwrap();
|
||||
assert!(all.len() >= 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upsert_overwrites() {
|
||||
let pool = setup_pool().await;
|
||||
let key = format!("test.{}", crate::utils::id::new_document_id());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
insert_test_option(&pool, &key, "v1", true, &now).await;
|
||||
upsert_value(&pool, &key, "v2", None, &now).await.unwrap();
|
||||
|
||||
let found = find_by_key(&pool, &key, None).await.unwrap().unwrap();
|
||||
assert_eq!(found.value, "v2");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_by_key_removes() {
|
||||
let pool = setup_pool().await;
|
||||
let key = format!("test.{}", crate::utils::id::new_document_id());
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
insert_test_option(&pool, &key, "val", true, &now).await;
|
||||
delete_by_key(&pool, &key, None).await.unwrap();
|
||||
|
||||
let found = find_by_key(&pool, &key, None).await.unwrap();
|
||||
assert!(found.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_autoload_test() {
|
||||
let pool = setup_pool().await;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
let k1 = format!("test.{}", crate::utils::id::new_document_id());
|
||||
let k2 = format!("test.{}", crate::utils::id::new_document_id());
|
||||
let k3 = format!("test.{}", crate::utils::id::new_document_id());
|
||||
insert_test_option(&pool, &k1, "v1", true, &now).await;
|
||||
insert_test_option(&pool, &k2, "v2", true, &now).await;
|
||||
insert_test_option(&pool, &k3, "v3", false, &now).await;
|
||||
|
||||
let autoloaded = find_autoload(&pool).await.unwrap();
|
||||
assert!(autoloaded.iter().any(|r| r.option_key == k1));
|
||||
assert!(autoloaded.iter().any(|r| r.option_key == k2));
|
||||
assert!(!autoloaded.iter().any(|r| r.option_key == k3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,3 +753,143 @@ pub async fn list_sitemap(
|
||||
}
|
||||
Ok(q.fetch_all(pool).await?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
async fn create_user(pool: &crate::db::Pool) -> i64 {
|
||||
let uid = uuid::Uuid::now_v7().to_string();
|
||||
sqlx::query(
|
||||
"INSERT INTO users (document_id, username, email, password_hash, role) VALUES (?, 'testuser', 'test@test.com', 'hash', 'author')",
|
||||
)
|
||||
.bind(&uid)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (id,): (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?")
|
||||
.bind(&uid)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
async fn create_test_page(
|
||||
pool: &crate::db::Pool,
|
||||
title: &str,
|
||||
slug: &str,
|
||||
status: &str,
|
||||
created_by: i64,
|
||||
) -> Page {
|
||||
create(
|
||||
pool, title, slug, None, None, None, None, None, "default", None, 0, status,
|
||||
created_by, None, None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_slug() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = create_user(&pool).await;
|
||||
let page = create_test_page(&pool, "About Us", "about", "published", uid).await;
|
||||
|
||||
let found = find_by_slug(&pool, "about", None).await.unwrap();
|
||||
assert!(found.is_some());
|
||||
assert_eq!(found.unwrap().id, page.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_document_id_test() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = create_user(&pool).await;
|
||||
let page = create_test_page(&pool, "Contact", "contact", "draft", uid).await;
|
||||
|
||||
let found = super::find_by_document_id(&pool, &page.document_id, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(found.is_some());
|
||||
assert_eq!(found.unwrap().slug, "contact");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_published_excludes_drafts() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = create_user(&pool).await;
|
||||
create_test_page(&pool, "Published Page", "pub", "published", uid).await;
|
||||
create_test_page(&pool, "Draft Page", "draft", "draft", uid).await;
|
||||
|
||||
let (items, total) = list_published(&pool, 1, 10, None).await.unwrap();
|
||||
assert_eq!(total, 1);
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].slug, "pub");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_changes_title() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = create_user(&pool).await;
|
||||
let page = create_test_page(&pool, "Old Title", "old", "published", uid).await;
|
||||
|
||||
let updated = update(
|
||||
&pool,
|
||||
page.id,
|
||||
Some("New Title"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated.title, "New Title");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_removes_page() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = create_user(&pool).await;
|
||||
let page = create_test_page(&pool, "To Delete", "delete-me", "published", uid).await;
|
||||
|
||||
delete(&pool, page.id, None).await.unwrap();
|
||||
let found = find_by_slug(&pool, "delete-me", None).await.unwrap();
|
||||
assert!(found.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_status_changes() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = create_user(&pool).await;
|
||||
let page = create_test_page(&pool, "Status Test", "status-test", "draft", uid).await;
|
||||
|
||||
assert_eq!(page.status, "draft");
|
||||
|
||||
let updated = update_status(&pool, page.id, "published", None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.status, "published");
|
||||
assert!(updated.published_at.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,3 +115,106 @@ pub async fn cleanup_expired(pool: &crate::db::Pool) -> AppResult<u64> {
|
||||
let result = sqlx::query(&sql).bind(now).execute(pool).await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
async fn insert_user(pool: &crate::db::Pool) -> i64 {
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let sql = format!(
|
||||
"INSERT INTO users (document_id, email, username, password_hash, role) VALUES ({}, {}, {}, {}, 'admin') RETURNING id",
|
||||
crate::db::dialect::ph(1),
|
||||
crate::db::dialect::ph(2),
|
||||
crate::db::dialect::ph(3),
|
||||
crate::db::dialect::ph(4)
|
||||
);
|
||||
let (id,): (i64,) = sqlx::query_as(&sql)
|
||||
.bind(&doc_id)
|
||||
.bind("pr-test@test.com")
|
||||
.bind("pruser")
|
||||
.bind("$argon2id$v=19$m=19456,t=2,p=1$test$test")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_token() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
let row = create(&pool, user_id, 3600).await.unwrap();
|
||||
assert!(row.id > 0);
|
||||
assert_eq!(row.user_id, user_id);
|
||||
assert!(!row.token.is_empty());
|
||||
assert!(row.used_at.is_none());
|
||||
|
||||
let found = find_by_token(&pool, &row.token).await.unwrap().unwrap();
|
||||
assert_eq!(found.id, row.id);
|
||||
assert_eq!(found.token, row.token);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mark_used() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
let row = create(&pool, user_id, 3600).await.unwrap();
|
||||
assert!(row.used_at.is_none());
|
||||
|
||||
super::mark_used(&pool, row.id).await.unwrap();
|
||||
|
||||
let found = find_by_token(&pool, &row.token).await.unwrap();
|
||||
assert!(
|
||||
found.is_none(),
|
||||
"used token should not be found by find_by_token"
|
||||
);
|
||||
|
||||
let sql = format!(
|
||||
"SELECT used_at FROM password_reset_tokens WHERE id = {}",
|
||||
crate::db::dialect::ph(1),
|
||||
);
|
||||
let (used_at,): (Option<String>,) = sqlx::query_as(&sql)
|
||||
.bind(row.id)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(used_at.is_some(), "used_at should be set after mark_used");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_unused_by_user() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
let row1 = create(&pool, user_id, 3600).await.unwrap();
|
||||
let row2 = create(&pool, user_id, 3600).await.unwrap();
|
||||
|
||||
super::delete_unused_by_user(&pool, user_id).await.unwrap();
|
||||
|
||||
let found1 = find_by_token(&pool, &row1.token).await.unwrap();
|
||||
let found2 = find_by_token(&pool, &row2.token).await.unwrap();
|
||||
assert!(found1.is_none());
|
||||
assert!(found2.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_cleanup_expired() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
let _row = create(&pool, user_id, 1).await.unwrap();
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
|
||||
let removed = super::cleanup_expired(&pool).await.unwrap();
|
||||
assert_eq!(removed, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,3 +120,82 @@ pub async fn delete_all(pool: &Pool, plugin_id: &str) -> AppResult<()> {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_and_get() {
|
||||
let pool = setup_pool().await;
|
||||
set(&pool, "plugin_set_get", "key1", "hello", None)
|
||||
.await
|
||||
.unwrap();
|
||||
let val = get(&pool, "plugin_set_get", "key1").await.unwrap();
|
||||
assert_eq!(val, Some("hello".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_missing_returns_none() {
|
||||
let pool = setup_pool().await;
|
||||
let val = get(&pool, "plugin_missing", "nope").await.unwrap();
|
||||
assert!(val.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_overwrites() {
|
||||
let pool = setup_pool().await;
|
||||
set(&pool, "plugin_overwrite", "key1", "v1", None)
|
||||
.await
|
||||
.unwrap();
|
||||
set(&pool, "plugin_overwrite", "key1", "v2", None)
|
||||
.await
|
||||
.unwrap();
|
||||
let val = get(&pool, "plugin_overwrite", "key1").await.unwrap();
|
||||
assert_eq!(val, Some("v2".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_key() {
|
||||
let pool = setup_pool().await;
|
||||
set(&pool, "plugin_delete", "key1", "val", None)
|
||||
.await
|
||||
.unwrap();
|
||||
delete(&pool, "plugin_delete", "key1").await.unwrap();
|
||||
let val = get(&pool, "plugin_delete", "key1").await.unwrap();
|
||||
assert!(val.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_all_removes_all() {
|
||||
let pool = setup_pool().await;
|
||||
let pid = "plugin_delete_all";
|
||||
set(&pool, pid, "k1", "v1", None).await.unwrap();
|
||||
set(&pool, pid, "k2", "v2", None).await.unwrap();
|
||||
set(&pool, pid, "k3", "v3", None).await.unwrap();
|
||||
delete_all(&pool, pid).await.unwrap();
|
||||
assert!(get(&pool, pid, "k1").await.unwrap().is_none());
|
||||
assert!(get(&pool, pid, "k2").await.unwrap().is_none());
|
||||
assert!(get(&pool, pid, "k3").await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_with_ttl() {
|
||||
let pool = setup_pool().await;
|
||||
set(&pool, "plugin_ttl", "key1", "expires", Some(1))
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
let val = get(&pool, "plugin_ttl", "key1").await.unwrap();
|
||||
assert!(val.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,3 +206,132 @@ pub async fn insert_permission(
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
fn now() -> String {
|
||||
chrono::Utc::now().to_rfc3339()
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn create_and_find_role_by_id() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let role = create_role(&pool, &doc_id, "admin_test", Some("desc"), &now())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(role.document_id, doc_id);
|
||||
assert_eq!(role.name, "admin_test");
|
||||
|
||||
let found = find_role_by_id(&pool, &doc_id).await.unwrap().unwrap();
|
||||
assert_eq!(found.id, role.id);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn list_roles_test() {
|
||||
let pool = setup_pool().await;
|
||||
for i in 0..3 {
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
create_role(&pool, &doc_id, &format!("role_{i}"), None, &now())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let roles = super::list_roles(&pool).await.unwrap();
|
||||
assert!(roles.len() >= 3);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn update_role_changes_name() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
create_role(&pool, &doc_id, "original", None, &now())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let updated = update_role(&pool, &doc_id, Some("new_name"), None, &now())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.name, "new_name");
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn delete_role_test() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
create_role(&pool, &doc_id, "to_delete", None, &now())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
super::delete_role(&pool, &doc_id).await.unwrap();
|
||||
let found = find_role_by_id(&pool, &doc_id).await.unwrap();
|
||||
assert!(found.is_none());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn permissions_crud() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let role = create_role(&pool, &doc_id, "perm_role", None, &now())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let t = now();
|
||||
insert_permission(
|
||||
&pool,
|
||||
&crate::utils::id::new_document_id(),
|
||||
role.id,
|
||||
"read",
|
||||
"posts",
|
||||
None,
|
||||
None,
|
||||
&t,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
insert_permission(
|
||||
&pool,
|
||||
&crate::utils::id::new_document_id(),
|
||||
role.id,
|
||||
"write",
|
||||
"posts",
|
||||
None,
|
||||
None,
|
||||
&t,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let perms = find_permissions_by_role_id(&pool, role.id).await.unwrap();
|
||||
assert_eq!(perms.len(), 2);
|
||||
|
||||
delete_permissions_by_role_id(&pool, role.id).await.unwrap();
|
||||
let perms = find_permissions_by_role_id(&pool, role.id).await.unwrap();
|
||||
assert!(perms.is_empty());
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn find_role_id_by_name_test() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let role = create_role(&pool, &doc_id, "lookup_name", None, &now())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id = super::find_role_id_by_name(&pool, "lookup_name")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(id, role.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,3 +91,70 @@ pub async fn delete_by_user(pool: &crate::db::Pool, user_id: i64) -> AppResult<(
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
async fn insert_user(pool: &crate::db::Pool) -> i64 {
|
||||
let cmd = crate::commands::user::CreateUserCmd {
|
||||
email: format!("{}@test.com", crate::utils::id::new_document_id()),
|
||||
username: crate::utils::id::new_document_id(),
|
||||
password_hash: "$argon2id$v=19$m=19456,t=2,p=1$test$test".to_string(),
|
||||
};
|
||||
let user = crate::models::user::create(pool, &cmd, None).await.unwrap();
|
||||
user.id
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_token() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
let token = crate::utils::id::new_document_id();
|
||||
create_token(&pool, user_id, &token, "2099-12-31T00:00:00Z")
|
||||
.await
|
||||
.unwrap();
|
||||
let found = find_by_token(&pool, &token).await.unwrap().unwrap();
|
||||
assert_eq!(found.token, token);
|
||||
assert_eq!(found.user_id, user_id);
|
||||
assert_eq!(found.expires_at, "2099-12-31T00:00:00Z");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_by_token() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
let token = crate::utils::id::new_document_id();
|
||||
create_token(&pool, user_id, &token, "2099-12-31T00:00:00Z")
|
||||
.await
|
||||
.unwrap();
|
||||
super::delete_by_token(&pool, &token).await.unwrap();
|
||||
assert!(find_by_token(&pool, &token).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_by_user() {
|
||||
let pool = setup_pool().await;
|
||||
let user_id = insert_user(&pool).await;
|
||||
let token1 = crate::utils::id::new_document_id();
|
||||
let token2 = crate::utils::id::new_document_id();
|
||||
create_token(&pool, user_id, &token1, "2099-12-31T00:00:00Z")
|
||||
.await
|
||||
.unwrap();
|
||||
create_token(&pool, user_id, &token2, "2099-12-31T00:00:00Z")
|
||||
.await
|
||||
.unwrap();
|
||||
super::delete_by_user(&pool, user_id).await.unwrap();
|
||||
assert!(find_by_token(&pool, &token1).await.unwrap().is_none());
|
||||
assert!(find_by_token(&pool, &token2).await.unwrap().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,3 +217,118 @@ pub async fn delete_reusable(
|
||||
let result = q.execute(pool).await?;
|
||||
AppError::expect_affected(&result, "reusable_block")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
async fn insert_user(pool: &crate::db::Pool) -> i64 {
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let sql = format!(
|
||||
"INSERT INTO users (document_id, email, username, password_hash, role) VALUES ({}, {}, {}, {}, 'admin') RETURNING id",
|
||||
crate::db::dialect::ph(1),
|
||||
crate::db::dialect::ph(2),
|
||||
crate::db::dialect::ph(3),
|
||||
crate::db::dialect::ph(4)
|
||||
);
|
||||
let (id,): (i64,) = sqlx::query_as(&sql)
|
||||
.bind(&doc_id)
|
||||
.bind("block-test@test.com")
|
||||
.bind("blockuser")
|
||||
.bind("$argon2id$v=19$m=19456,t=2,p=1$test$test")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn create_and_find_by_id() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let block = create_reusable(&pool, "Block", "text", "[]", None, Some(uid), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let found = find_reusable_by_id(&pool, block.id, None).await.unwrap();
|
||||
assert!(found.is_some());
|
||||
assert_eq!(found.unwrap().name, "Block");
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn find_by_document_id_test() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let block = create_reusable(&pool, "Block", "text", "[]", None, Some(uid), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let found = super::find_reusable_by_document_id(&pool, &block.document_id, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(found.is_some());
|
||||
assert_eq!(found.unwrap().id, block.id);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn list_reusable_test() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
for i in 0..3 {
|
||||
create_reusable(
|
||||
&pool,
|
||||
&format!("Block{i}"),
|
||||
"text",
|
||||
"[]",
|
||||
None,
|
||||
Some(uid),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let list = super::list_reusable(&pool, None).await.unwrap();
|
||||
assert!(list.len() >= 3);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn update_changes_name() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let block = create_reusable(&pool, "Block", "text", "[]", None, Some(uid), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let updated = update_reusable(
|
||||
&pool,
|
||||
block.id,
|
||||
Some("Updated"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(uid),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.name, "Updated");
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn delete_removes_block() {
|
||||
let pool = setup_pool().await;
|
||||
let uid = insert_user(&pool).await;
|
||||
let block = create_reusable(&pool, "Block", "text", "[]", None, Some(uid), None)
|
||||
.await
|
||||
.unwrap();
|
||||
delete_reusable(&pool, block.id, None).await.unwrap();
|
||||
let found = find_reusable_by_id(&pool, block.id, None).await.unwrap();
|
||||
assert!(found.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,3 +200,125 @@ pub async fn cleanup_expired(pool: &crate::db::Pool) -> AppResult<u64> {
|
||||
let result = sqlx::query(&sql).bind(now).execute(pool).await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
fn unique_phone() -> String {
|
||||
let id = crate::utils::id::new_document_id();
|
||||
let hash = id
|
||||
.bytes()
|
||||
.fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
|
||||
format!("1380000{:04}", hash % 10000)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_code_length() {
|
||||
let code = super::generate_code(6);
|
||||
assert_eq!(code.len(), 6);
|
||||
assert!(code.chars().all(|c| c.is_ascii_digit()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_id() {
|
||||
let pool = setup_pool().await;
|
||||
let phone = unique_phone();
|
||||
let code = super::generate_code(6);
|
||||
|
||||
let sms = super::create(&pool, &phone, &code, "login", 300, Some("127.0.0.1"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let found = super::find_by_id(&pool, sms.id).await.unwrap();
|
||||
assert!(found.is_some());
|
||||
let row = found.unwrap();
|
||||
assert_eq!(row.phone, phone);
|
||||
assert_eq!(row.code, code);
|
||||
assert_eq!(row.purpose, "login");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_latest_unverified() {
|
||||
let pool = setup_pool().await;
|
||||
let phone = unique_phone();
|
||||
|
||||
let _first = super::create(&pool, &phone, "111111", "login", 300, None)
|
||||
.await
|
||||
.unwrap();
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
let second = super::create(&pool, &phone, "222222", "login", 300, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let latest = super::find_latest_unverified(&pool, &phone, "login")
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(latest.is_some());
|
||||
assert_eq!(latest.unwrap().id, second.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn verify_code_correct() {
|
||||
let pool = setup_pool().await;
|
||||
let phone = unique_phone();
|
||||
let code = "654321";
|
||||
|
||||
let sms = super::create(&pool, &phone, code, "login", 300, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = super::verify_code(&pool, sms.id, code).await.unwrap();
|
||||
assert_eq!(result, super::VerifyResult::Verified);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn verify_code_wrong() {
|
||||
let pool = setup_pool().await;
|
||||
let phone = unique_phone();
|
||||
|
||||
let sms = super::create(&pool, &phone, "123456", "login", 300, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = super::verify_code(&pool, sms.id, "000000").await.unwrap();
|
||||
assert_eq!(result, super::VerifyResult::WrongCode);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn is_rate_limited() {
|
||||
let pool = setup_pool().await;
|
||||
let phone = unique_phone();
|
||||
|
||||
super::create(&pool, &phone, "111111", "login", 300, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let limited = super::is_rate_limited(&pool, &phone, "login", 60)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(limited);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cleanup_expired() {
|
||||
let pool = setup_pool().await;
|
||||
let phone = unique_phone();
|
||||
|
||||
super::create(&pool, &phone, "111111", "login", 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
let removed = super::cleanup_expired(&pool).await.unwrap();
|
||||
assert!(removed > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,3 +207,89 @@ pub async fn update(
|
||||
AppError::expect_affected(&result, "tag")?;
|
||||
find_by_id(pool, id, tenant_id).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_id() {
|
||||
let pool = setup_pool().await;
|
||||
let tag = create(&pool, "rust", "rust", None, None).await.unwrap();
|
||||
assert_eq!(tag.name, "rust");
|
||||
assert_eq!(tag.slug, "rust");
|
||||
|
||||
let found = find_by_id(&pool, tag.id, None).await.unwrap();
|
||||
assert_eq!(found.id, tag.id);
|
||||
assert_eq!(found.name, "rust");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_document_id() {
|
||||
let pool = setup_pool().await;
|
||||
let tag = create(&pool, "rust", "rust", None, None).await.unwrap();
|
||||
|
||||
let found = super::find_by_document_id(&pool, &tag.document_id, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(found.id, tag.id);
|
||||
assert_eq!(found.document_id, tag.document_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_all_returns_all() {
|
||||
let pool = setup_pool().await;
|
||||
create(&pool, "rust", "rust", None, None).await.unwrap();
|
||||
create(&pool, "axum", "axum", None, None).await.unwrap();
|
||||
create(&pool, "tokio", "tokio", None, None).await.unwrap();
|
||||
|
||||
let tags = find_all(&pool, None).await.unwrap();
|
||||
assert_eq!(tags.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_paginated() {
|
||||
let pool = setup_pool().await;
|
||||
create(&pool, "rust", "rust", None, None).await.unwrap();
|
||||
create(&pool, "axum", "axum", None, None).await.unwrap();
|
||||
create(&pool, "tokio", "tokio", None, None).await.unwrap();
|
||||
create(&pool, "serde", "serde", None, None).await.unwrap();
|
||||
create(&pool, "clap", "clap", None, None).await.unwrap();
|
||||
|
||||
let (items, total) = super::find_paginated(&pool, None, 1, 3).await.unwrap();
|
||||
assert_eq!(total, 5);
|
||||
assert_eq!(items.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_changes_name() {
|
||||
let pool = setup_pool().await;
|
||||
let tag = create(&pool, "rust", "rust", None, None).await.unwrap();
|
||||
|
||||
let updated = update(&pool, tag.id, "Rust Lang", "rust-lang", None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.name, "Rust Lang");
|
||||
assert_eq!(updated.slug, "rust-lang");
|
||||
assert_eq!(updated.id, tag.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_removes_tag() {
|
||||
let pool = setup_pool().await;
|
||||
let tag = create(&pool, "rust", "rust", None, None).await.unwrap();
|
||||
|
||||
delete(&pool, tag.id, None).await.unwrap();
|
||||
let result = find_by_id(&pool, tag.id, None).await;
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,3 +149,131 @@ pub async fn delete(pool: &crate::db::Pool, document_id: &str) -> AppResult<()>
|
||||
sqlx::query(&sql).bind(document_id).execute(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_id() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = "tenant-001";
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let row = create(
|
||||
&pool,
|
||||
doc_id,
|
||||
"Test Tenant",
|
||||
Some("test.example.com"),
|
||||
"{}",
|
||||
&now,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(row.document_id, doc_id);
|
||||
assert_eq!(row.name, "Test Tenant");
|
||||
assert_eq!(row.domain.unwrap(), "test.example.com");
|
||||
|
||||
let found = find_by_id(&pool, doc_id).await.unwrap().unwrap();
|
||||
assert_eq!(found.id, row.id);
|
||||
assert_eq!(found.document_id, doc_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_domain_returns_match() {
|
||||
let pool = setup_pool().await;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
create(
|
||||
&pool,
|
||||
"tenant-002",
|
||||
"Dom Tenant",
|
||||
Some("dom.example.com"),
|
||||
"{}",
|
||||
&now,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let found = find_by_domain(&pool, "dom.example.com")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(found.document_id, "tenant-002");
|
||||
assert_eq!(found.name, "Dom Tenant");
|
||||
|
||||
let missing = find_by_domain(&pool, "no.such.domain").await.unwrap();
|
||||
assert!(missing.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_all_returns_all() {
|
||||
let pool = setup_pool().await;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
create(&pool, "tenant-a", "Alpha", None, "{}", &now)
|
||||
.await
|
||||
.unwrap();
|
||||
create(&pool, "tenant-b", "Bravo", None, "{}", &now)
|
||||
.await
|
||||
.unwrap();
|
||||
create(&pool, "tenant-c", "Charlie", None, "{}", &now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let all = find_all(&pool).await.unwrap();
|
||||
assert!(all.len() >= 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_changes_name() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = "tenant-003";
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
create(
|
||||
&pool,
|
||||
doc_id,
|
||||
"Original",
|
||||
Some("orig.example.com"),
|
||||
"{}",
|
||||
&now,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let later = chrono::Utc::now().to_rfc3339();
|
||||
let updated = update(
|
||||
&pool,
|
||||
doc_id,
|
||||
Some("Updated Name"),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
&later,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.name, "Updated Name");
|
||||
assert_eq!(updated.domain.unwrap(), "orig.example.com");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_removes_tenant() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = "tenant-004";
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
create(&pool, doc_id, "ToDelete", None, "{}", &now)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
delete(&pool, doc_id).await.unwrap();
|
||||
let found = find_by_id(&pool, doc_id).await.unwrap();
|
||||
assert!(found.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,3 +342,138 @@ pub async fn update_role(
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("user"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
fn new_cmd(email: &str, username: &str) -> crate::commands::user::CreateUserCmd {
|
||||
crate::commands::user::CreateUserCmd {
|
||||
email: email.to_string(),
|
||||
username: username.to_string(),
|
||||
password_hash: "$argon2id$v=19$m=19456,t=2,p=1$test$test".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_find_by_email() {
|
||||
let pool = setup_pool().await;
|
||||
let user = create(&pool, &new_cmd("a@b.com", "alice"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let found = find_by_email(&pool, "a@b.com", None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(found.id, user.id);
|
||||
assert_eq!(found.email, "a@b.com");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_id() {
|
||||
let pool = setup_pool().await;
|
||||
let user = create(&pool, &new_cmd("id@b.com", "iduser"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let found = super::find_by_id(&pool, &user.document_id, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(found.id, user.id);
|
||||
assert_eq!(found.document_id, user.document_id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_pk() {
|
||||
let pool = setup_pool().await;
|
||||
let user = create(&pool, &new_cmd("pk@b.com", "pkuser"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let found = super::find_by_pk(&pool, user.id, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(found.document_id, user.document_id);
|
||||
assert_eq!(found.email, "pk@b.com");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_profile() {
|
||||
let pool = setup_pool().await;
|
||||
let user = create(&pool, &new_cmd("prof@b.com", "profuser"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let updated = super::update_profile(
|
||||
&pool,
|
||||
&crate::commands::user::UpdateProfileCmd {
|
||||
id: user.id,
|
||||
username: Some("newname".to_string()),
|
||||
bio: Some("hello world".to_string()),
|
||||
website: None,
|
||||
avatar: None,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.username, "newname");
|
||||
assert_eq!(updated.bio.unwrap(), "hello world");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_password() {
|
||||
let pool = setup_pool().await;
|
||||
let user = create(&pool, &new_cmd("pw@b.com", "pwuser"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
let old_hash = user.password_hash.clone();
|
||||
super::update_password(&pool, &user.document_id, "$new_hash", None)
|
||||
.await
|
||||
.unwrap();
|
||||
let found = super::find_by_id(&pool, &user.document_id, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_ne!(found.password_hash, old_hash);
|
||||
assert_eq!(found.password_hash, "$new_hash");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_all_paginated() {
|
||||
let pool = setup_pool().await;
|
||||
for i in 0..5 {
|
||||
create(
|
||||
&pool,
|
||||
&new_cmd(&format!("page{i}@b.com"), &format!("user{i}")),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let (users, total) = find_all(&pool, 1, 3, None).await.unwrap();
|
||||
assert_eq!(users.len(), 3);
|
||||
assert_eq!(total, 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_role() {
|
||||
let pool = setup_pool().await;
|
||||
let user = create(&pool, &new_cmd("role@b.com", "roleuser"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(user.role, "reader");
|
||||
let updated = super::update_role(&pool, &user.document_id, "author", None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.role, "author");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,3 +401,203 @@ pub async fn list_step_logs(pool: &Pool, instance_id: i64) -> anyhow::Result<Vec
|
||||
.await?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
async fn setup_pool() -> crate::db::Pool {
|
||||
let pool = crate::db::Pool::connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::query(crate::db::schema::SCHEMA_SQL)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
pool
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_get_definition() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#;
|
||||
let def = super::create_definition(&pool, &doc_id, "Test WF", Some("desc"), steps, "s1")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(def.document_id, doc_id);
|
||||
assert_eq!(def.name, "Test WF");
|
||||
|
||||
let got = super::get_definition(&pool, &doc_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(got.id, def.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_definitions() {
|
||||
let pool = setup_pool().await;
|
||||
let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#;
|
||||
for i in 0..3 {
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
super::create_definition(&pool, &doc_id, &format!("WF {i}"), None, steps, "s1")
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let list = super::list_definitions(&pool).await.unwrap();
|
||||
assert_eq!(list.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_definition() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#;
|
||||
super::create_definition(&pool, &doc_id, "To delete", None, steps, "s1")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
super::delete_definition(&pool, &doc_id).await.unwrap();
|
||||
assert!(
|
||||
super::get_definition(&pool, &doc_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_get_instance() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#;
|
||||
let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let inst_doc_id = crate::utils::id::new_document_id();
|
||||
let ctx = serde_json::json!({"key": "value"});
|
||||
let inst = super::create_instance(&pool, &inst_doc_id, def.id, &ctx, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(inst.document_id, inst_doc_id);
|
||||
assert_eq!(inst.definition_id, def.id);
|
||||
assert_eq!(inst.status, "running");
|
||||
|
||||
let got = super::get_instance(&pool, &inst_doc_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(got.id, inst.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_instances_test() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#;
|
||||
let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for _ in 0..3 {
|
||||
let inst_doc_id = crate::utils::id::new_document_id();
|
||||
let ctx = serde_json::json!({});
|
||||
super::create_instance(&pool, &inst_doc_id, def.id, &ctx, None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let (rows, count) = super::list_instances(&pool, Some(def.id), None, 1, 10)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(rows.len(), 3);
|
||||
assert_eq!(count, 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_instance_step() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#;
|
||||
let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let inst_doc_id = crate::utils::id::new_document_id();
|
||||
let ctx = serde_json::json!({});
|
||||
super::create_instance(&pool, &inst_doc_id, def.id, &ctx, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_ctx = serde_json::json!({"progress": 50});
|
||||
super::update_instance_step(&pool, &inst_doc_id, "paused", Some("s1"), &new_ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let inst = super::get_instance(&pool, &inst_doc_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(inst.status, "paused");
|
||||
assert_eq!(inst.current_step, Some("s1".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn step_log_lifecycle() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#;
|
||||
let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let inst_doc_id = crate::utils::id::new_document_id();
|
||||
let ctx = serde_json::json!({});
|
||||
let inst = super::create_instance(&pool, &inst_doc_id, def.id, &ctx, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let log_doc_id = crate::utils::id::new_document_id();
|
||||
let input = serde_json::json!({"data": 1});
|
||||
let log = super::create_step_log(&pool, &log_doc_id, inst.id, "s1", "Step 1", Some(&input))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(log.status, "running");
|
||||
assert_eq!(log.step_id, "s1");
|
||||
|
||||
let output = serde_json::json!({"result": "ok"});
|
||||
super::complete_step_log(&pool, &log_doc_id, Some(&output))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let logs = super::list_step_logs(&pool, inst.id).await.unwrap();
|
||||
assert_eq!(logs.len(), 1);
|
||||
assert_eq!(logs[0].status, "completed");
|
||||
assert!(logs[0].completed_at.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parse_steps_valid() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let steps = r#"[{"id":"s1","name":"Step 1","type":"task","config":{},"next":""}]"#;
|
||||
let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let parsed = def.parse_steps().unwrap();
|
||||
assert_eq!(parsed.len(), 1);
|
||||
assert_eq!(parsed[0].id, "s1");
|
||||
assert_eq!(parsed[0].step_type, super::StepType::Task);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parse_steps_invalid_json() {
|
||||
let pool = setup_pool().await;
|
||||
let doc_id = crate::utils::id::new_document_id();
|
||||
let steps = "not valid json!!!";
|
||||
let def = super::create_definition(&pool, &doc_id, "WF", None, steps, "s1")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(def.parse_steps().is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,8 @@ impl Aspect for OwnableAspect {
|
||||
.as_ref()
|
||||
.is_none_or(|s| s.is_protocol_column(COL_UPDATED_BY))
|
||||
{
|
||||
ctx.new_record.insert(COL_UPDATED_BY.into(), json!(user_int_id));
|
||||
ctx.new_record
|
||||
.insert(COL_UPDATED_BY.into(), json!(user_int_id));
|
||||
}
|
||||
Ok(Advice::Continue)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! 基于 sqlx 的 `MediaRepository` 实现
|
||||
|
||||
use crate::commands::CreateMediaCmd;
|
||||
use crate::errors::app_error::AppResult;
|
||||
use crate::models::media::{self, Media, MediaStats};
|
||||
use crate::commands::CreateMediaCmd;
|
||||
use crate::repositories::define_sqlx_repo;
|
||||
|
||||
define_sqlx_repo!(SqlxMediaRepository);
|
||||
|
||||
@@ -137,3 +137,35 @@ pub fn get_str(record: &Record, key: &str) -> Option<String> {
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn id_record_contains_id() {
|
||||
let r = id_record("abc-123");
|
||||
let val = r.get("id").and_then(|v| v.as_str());
|
||||
assert_eq!(val, Some("abc-123"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_str_returns_value() {
|
||||
let mut r = Record::new();
|
||||
r.insert("name".into(), serde_json::json!("test-value"));
|
||||
assert_eq!(get_str(&r, "name"), Some("test-value".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_str_missing_key_returns_none() {
|
||||
let r = Record::new();
|
||||
assert_eq!(get_str(&r, "missing"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_str_non_string_returns_none() {
|
||||
let mut r = Record::new();
|
||||
r.insert("num".into(), serde_json::json!(42));
|
||||
assert_eq!(get_str(&r, "num"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,9 @@ use jsonwebtoken::{EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::commands::CreateUserCmd;
|
||||
use crate::dto::{LoginResponse, RegisterRequest, UpdatePasswordRequest, UserResponse};
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::eventbus::{Event, EventBus};
|
||||
use crate::dto::{
|
||||
LoginResponse, RegisterRequest, UpdatePasswordRequest, UserResponse,
|
||||
};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::plugins::{HookPoint, PluginManager};
|
||||
use crate::repositories::{RefreshTokenRepository, UserRepository};
|
||||
@@ -205,7 +203,13 @@ pub async fn register(
|
||||
});
|
||||
|
||||
if require_email_verification {
|
||||
let _ = crate::services::email_verification::trigger_email_verification(pool, eventbus, user.id, &user.email).await;
|
||||
let _ = crate::services::email_verification::trigger_email_verification(
|
||||
pool,
|
||||
eventbus,
|
||||
user.id,
|
||||
&user.email,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(user.into())
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
use slug::slugify;
|
||||
|
||||
use crate::commands::{CreateCategoryCmd, UpdateCategoryCmd};
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::dto::{CreateCategoryRequest, UpdateCategoryRequest};
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::repositories::CategoryRepository;
|
||||
|
||||
|
||||
@@ -425,3 +425,228 @@ fn detect_mime_from_magic(data: &[u8]) -> Option<&'static str> {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mime_to_ext_known_types() {
|
||||
assert_eq!(mime_to_ext("image/jpeg"), "jpg");
|
||||
assert_eq!(mime_to_ext("image/png"), "png");
|
||||
assert_eq!(mime_to_ext("image/gif"), "gif");
|
||||
assert_eq!(mime_to_ext("image/webp"), "webp");
|
||||
assert_eq!(mime_to_ext("image/svg+xml"), "svg");
|
||||
assert_eq!(mime_to_ext("video/mp4"), "mp4");
|
||||
assert_eq!(mime_to_ext("audio/mpeg"), "mp3");
|
||||
assert_eq!(mime_to_ext("application/pdf"), "pdf");
|
||||
assert_eq!(mime_to_ext("application/zip"), "zip");
|
||||
assert_eq!(mime_to_ext("text/plain"), "txt");
|
||||
assert_eq!(mime_to_ext("text/markdown"), "md");
|
||||
assert_eq!(mime_to_ext("text/csv"), "csv");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mime_to_ext_unknown_returns_bin() {
|
||||
assert_eq!(mime_to_ext("application/x-unknown"), "bin");
|
||||
assert_eq!(mime_to_ext("foo/bar"), "bin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_empty_data() {
|
||||
assert!(!validate_magic_bytes("image/jpeg", &[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_jpeg() {
|
||||
assert!(validate_magic_bytes("image/jpeg", b"\xFF\xD8\xFF\xE0"));
|
||||
assert!(!validate_magic_bytes("image/jpeg", b"\x89PNG\r\n\x1a\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_png() {
|
||||
let png_header = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR";
|
||||
assert!(validate_magic_bytes("image/png", png_header));
|
||||
assert!(!validate_magic_bytes("image/png", b"\xFF\xD8\xFF"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_gif() {
|
||||
assert!(validate_magic_bytes("image/gif", b"GIF89a\x00\x00"));
|
||||
assert!(validate_magic_bytes("image/gif", b"GIF87a\x00\x00"));
|
||||
assert!(!validate_magic_bytes("image/gif", b"BM\x00\x00\x00"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_webp() {
|
||||
let mut webp = vec![b'R', b'I', b'F', b'F', 0, 0, 0, 0, b'W', b'E', b'B', b'P'];
|
||||
webp.extend_from_slice(&[0; 20]);
|
||||
assert!(validate_magic_bytes("image/webp", &webp));
|
||||
assert!(!validate_magic_bytes("image/webp", b"\xFF\xD8\xFF"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_pdf() {
|
||||
assert!(validate_magic_bytes("application/pdf", b"%PDF-1.4\n"));
|
||||
assert!(!validate_magic_bytes("application/pdf", b"<html>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_zip() {
|
||||
assert!(validate_magic_bytes(
|
||||
"application/zip",
|
||||
b"PK\x03\x04\x00\x00"
|
||||
));
|
||||
assert!(!validate_magic_bytes("application/zip", b"\x1F\x8B\x08"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_gzip() {
|
||||
assert!(validate_magic_bytes(
|
||||
"application/gzip",
|
||||
b"\x1F\x8B\x08\x00"
|
||||
));
|
||||
assert!(!validate_magic_bytes("application/gzip", b"PK\x03\x04"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_rar() {
|
||||
assert!(validate_magic_bytes(
|
||||
"application/x-rar-compressed",
|
||||
b"Rar!\x1A\x07\x00"
|
||||
));
|
||||
assert!(!validate_magic_bytes(
|
||||
"application/x-rar-compressed",
|
||||
b"PK\x03\x04"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_mp3() {
|
||||
assert!(validate_magic_bytes("audio/mpeg", b"\xFF\xFB\x90\x00"));
|
||||
assert!(validate_magic_bytes("audio/mpeg", b"ID3\x03\x00"));
|
||||
assert!(!validate_magic_bytes("audio/mpeg", b"OggS\x00"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_ogg() {
|
||||
assert!(validate_magic_bytes("audio/ogg", b"OggS\x00\x02"));
|
||||
assert!(!validate_magic_bytes("audio/ogg", b"\xFF\xFB"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_wav() {
|
||||
let mut wav = b"RIFF".to_vec();
|
||||
wav.extend_from_slice(&[0, 0, 0, 0]);
|
||||
wav.extend_from_slice(b"WAVE");
|
||||
wav.extend_from_slice(&[0; 20]);
|
||||
assert!(validate_magic_bytes("audio/wav", &wav));
|
||||
assert!(!validate_magic_bytes("audio/wav", b"\xFF\xD8\xFF"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_mp4() {
|
||||
let mut mp4 = vec![0, 0, 0, 0x20];
|
||||
mp4.extend_from_slice(b"ftypisom");
|
||||
mp4.extend_from_slice(&[0; 24]);
|
||||
assert!(validate_magic_bytes("video/mp4", &mp4));
|
||||
assert!(!validate_magic_bytes("video/mp4", b"\x1a\x45\xdf\xa3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_webm() {
|
||||
assert!(validate_magic_bytes(
|
||||
"video/webm",
|
||||
b"\x1a\x45\xdf\xa3\x00\x00"
|
||||
));
|
||||
assert!(!validate_magic_bytes("video/webm", b"\xFF\xD8\xFF"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_skip_types_always_pass() {
|
||||
assert!(validate_magic_bytes("text/plain", b"hello"));
|
||||
assert!(validate_magic_bytes("text/csv", b"a,b"));
|
||||
assert!(validate_magic_bytes("text/markdown", b"# Title"));
|
||||
assert!(validate_magic_bytes("image/svg+xml", b"<svg>"));
|
||||
assert!(validate_magic_bytes("application/x-tar", b"foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_magic_bytes_unknown_type_fails() {
|
||||
assert!(!validate_magic_bytes(
|
||||
"application/x-totally-fake",
|
||||
b"\x00\x01\x02"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_mime_from_magic_jpeg() {
|
||||
assert_eq!(
|
||||
detect_mime_from_magic(b"\xFF\xD8\xFF\xE0"),
|
||||
Some("image/jpeg")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_mime_from_magic_png() {
|
||||
assert_eq!(
|
||||
detect_mime_from_magic(b"\x89PNG\r\n\x1a\n\x00"),
|
||||
Some("image/png")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_mime_from_magic_gif() {
|
||||
assert_eq!(
|
||||
detect_mime_from_magic(b"GIF89a\x00\x00\x00"),
|
||||
Some("image/gif")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_mime_from_magic_pdf() {
|
||||
assert_eq!(
|
||||
detect_mime_from_magic(b"%PDF-1.4\n"),
|
||||
Some("application/pdf")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_mime_from_magic_zip() {
|
||||
assert_eq!(
|
||||
detect_mime_from_magic(b"PK\x03\x04\x00"),
|
||||
Some("application/zip")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_mime_from_magic_unknown() {
|
||||
assert_eq!(detect_mime_from_magic(b"\x00\x01"), None);
|
||||
assert_eq!(detect_mime_from_magic(b"\x00"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_key_format() {
|
||||
let key = storage_key("uploads", "jpg");
|
||||
assert!(key.starts_with("uploads/"));
|
||||
assert!(key.ends_with(".jpg"));
|
||||
let parts: Vec<&str> = key.split('/').collect();
|
||||
assert_eq!(parts.len(), 4);
|
||||
assert!(parts[1].len() == 4);
|
||||
assert!(parts[2].len() == 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_image_dimensions_invalid_data() {
|
||||
let (w, h) = parse_image_dimensions(b"not an image");
|
||||
assert_eq!(w, None);
|
||||
assert_eq!(h, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_image_dimensions_empty() {
|
||||
let (w, h) = parse_image_dimensions(&[]);
|
||||
assert_eq!(w, None);
|
||||
assert_eq!(h, None);
|
||||
}
|
||||
}
|
||||
|
||||
+50
-1
@@ -10,8 +10,8 @@ use chrono::Utc;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::commands::CreateUserCmd;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::dto::LoginResponse;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::oauth;
|
||||
use crate::oauth::{OAuthProviderRegistry, OAuthUserInfo};
|
||||
@@ -472,3 +472,52 @@ async fn update_oauth_account(
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sanitize_username_alphanumeric_only() {
|
||||
assert_eq!(sanitize_username("JohnDoe123"), "JohnDoe123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_username_strips_special_chars() {
|
||||
assert_eq!(
|
||||
sanitize_username("john.doe@example.com"),
|
||||
"johndoeexamplecom"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_username_keeps_underscore() {
|
||||
assert_eq!(sanitize_username("john_doe"), "john_doe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_username_strips_leading_trailing_underscores() {
|
||||
assert_eq!(sanitize_username("__john__"), "john");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_username_all_special_returns_default() {
|
||||
assert_eq!(sanitize_username("@#$%"), "user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_username_empty_returns_default() {
|
||||
assert_eq!(sanitize_username(""), "user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_username_unicode_stripped() {
|
||||
let result = sanitize_username("用户名");
|
||||
assert_eq!(result, "user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_username_mixed() {
|
||||
assert_eq!(sanitize_username("John_Doe-123!"), "John_Doe123");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,3 +167,59 @@ pub async fn sitemap(
|
||||
pub fn generate_slug(title: &str) -> String {
|
||||
slugify(title)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_valid_empty() {
|
||||
let blocks = validate_blocks_json("[]").unwrap();
|
||||
assert!(blocks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_valid_richtext() {
|
||||
let json = r#"[{"type":"richtext","content":"hello"}]"#;
|
||||
let blocks = validate_blocks_json(json).unwrap();
|
||||
assert_eq!(blocks.len(), 1);
|
||||
assert!(matches!(blocks[0], page::PageBlock::Richtext { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_invalid() {
|
||||
let result = validate_blocks_json("not json");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_invalid_structure() {
|
||||
let result = validate_blocks_json(r#"[{"wrong":"field"}]"#);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_slug_basic() {
|
||||
assert_eq!(generate_slug("Hello World"), "hello-world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_slug_special_chars() {
|
||||
let slug = generate_slug("Hello, World! (2024)");
|
||||
assert!(!slug.contains(','));
|
||||
assert!(!slug.contains('!'));
|
||||
assert!(!slug.contains('('));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_slug_chinese() {
|
||||
let slug = generate_slug("你好世界");
|
||||
assert!(!slug.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_slug_empty() {
|
||||
let slug = generate_slug("");
|
||||
assert!(slug.is_empty() || !slug.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
+27
-4
@@ -10,9 +10,9 @@
|
||||
use slug::slugify;
|
||||
|
||||
use crate::commands::{CreatePostCmd, FindPublishedQuery, UpdatePostCmd};
|
||||
use crate::dto::{CreatePostRequest, PostResponse, UpdatePostRequest};
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::eventbus::{Event, EventBus};
|
||||
use crate::dto::{CreatePostRequest, PostResponse, UpdatePostRequest};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::post::PostJoinedRow;
|
||||
use crate::plugins::{HookPoint, PluginManager};
|
||||
@@ -47,9 +47,9 @@ pub async fn resolve_doc_id_to_int(
|
||||
if let Some(tid) = tenant_id {
|
||||
q = q.bind(tid);
|
||||
}
|
||||
q.fetch_optional(pool).await.map_err(|e| {
|
||||
AppError::Internal(anyhow::anyhow!("resolve doc_id in {table} failed: {e}"))
|
||||
})
|
||||
q.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|e| AppError::Internal(anyhow::anyhow!("resolve doc_id in {table} failed: {e}")))
|
||||
}
|
||||
|
||||
async fn joined_row_to_response(
|
||||
@@ -605,4 +605,27 @@ mod tests {
|
||||
let result = extract_excerpt(&content, 200);
|
||||
assert!(result.ends_with("...") || result.len() <= 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_unique_slug_has_suffix() {
|
||||
let slug = make_unique_slug("my-post");
|
||||
assert!(slug.starts_with("my-post-"));
|
||||
let suffix = &slug["my-post-".len()..];
|
||||
assert_eq!(suffix.len(), 4);
|
||||
assert!(suffix.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_unique_slug_different_each_call() {
|
||||
let s1 = make_unique_slug("test");
|
||||
let s2 = make_unique_slug("test");
|
||||
assert_ne!(s1, s2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_excerpt_zero_max_len() {
|
||||
let content = "hello world";
|
||||
let result = extract_excerpt(content, 0);
|
||||
assert!(result.is_empty() || result.ends_with("..."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,4 +343,85 @@ mod tests {
|
||||
assert_eq!(resolve_template("$user.id", &ctx), "user-1");
|
||||
assert_eq!(resolve_template("literal_value", &ctx), "literal_value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rsplit_dot_with_dot() {
|
||||
assert_eq!(rsplit_dot("a.b.c"), ("a.b", "c"));
|
||||
assert_eq!(rsplit_dot("single"), ("single", ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rsplit_dot_no_dot() {
|
||||
assert_eq!(rsplit_dot("noun"), ("noun", ""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ns_matches_exact() {
|
||||
assert!(ns_matches("content-type", "content-type"));
|
||||
assert!(ns_matches("a::b", "a::b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ns_matches_wildcard() {
|
||||
assert!(ns_matches("*", "content-type"));
|
||||
assert!(ns_matches("*::*", "content-type::post"));
|
||||
assert!(!ns_matches("a::*", "b::c"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ns_matches_different_depth() {
|
||||
assert!(!ns_matches("a", "a::b"));
|
||||
assert!(!ns_matches("a::b::c", "a::b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_action_exact_match() {
|
||||
assert!(matches_action("post.create", "post.create"));
|
||||
assert!(!matches_action("post.create", "post.delete"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_action_wildcard_op() {
|
||||
assert!(matches_action("post.*", "post.create"));
|
||||
assert!(matches_action("post.*", "post.delete"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_subject_wildcard_parts() {
|
||||
assert!(matches_subject("content-type::*", "content-type::post"));
|
||||
assert!(matches_subject("*::*", "content-type::post"));
|
||||
assert!(!matches_subject("blog::*", "content-type::post"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_subject_different_depth() {
|
||||
assert!(!matches_subject("a", "a::b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_conditions_missing_key() {
|
||||
let mut conditions = HashMap::new();
|
||||
conditions.insert("missing_key".into(), "value".into());
|
||||
let context = HashMap::new();
|
||||
assert!(!check_conditions(&conditions, &context));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_conditions_empty_passes() {
|
||||
let conditions = HashMap::new();
|
||||
let context = HashMap::new();
|
||||
assert!(check_conditions(&conditions, &context));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_template_missing_var() {
|
||||
let ctx = HashMap::new();
|
||||
assert_eq!(resolve_template("$user.nonexistent", &ctx), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_template_non_user_var() {
|
||||
let ctx = HashMap::new();
|
||||
assert_eq!(resolve_template("plain_text", &ctx), "plain_text");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,3 +81,31 @@ pub async fn delete_reusable(pool: &crate::db::Pool, id: &str, auth: &AuthUser)
|
||||
.ok_or_else(|| AppError::not_found("reusable_block"))?;
|
||||
reusable_block::delete_reusable(pool, block.id, auth.tenant_id()).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_valid_empty() {
|
||||
let blocks = validate_blocks_json("[]").unwrap();
|
||||
assert!(blocks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_valid_block() {
|
||||
let json = r#"[{"type":"richtext","content":"block content"}]"#;
|
||||
let blocks = validate_blocks_json(json).unwrap();
|
||||
assert_eq!(blocks.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_invalid() {
|
||||
assert!(validate_blocks_json("{bad").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_wrong_type() {
|
||||
assert!(validate_blocks_json(r#"[123]"#).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,8 +2,8 @@
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::dto::LoginResponse;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::repositories::{RefreshTokenRepository, UserRepository};
|
||||
|
||||
|
||||
+1
-1
@@ -2,8 +2,8 @@
|
||||
|
||||
use slug::slugify;
|
||||
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::dto::CreateTagRequest;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::repositories::TagRepository;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! 用户资料管理服务。
|
||||
|
||||
use crate::commands::UpdateProfileCmd;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::dto::{UpdateUserRequest, UserResponse};
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::repositories::UserRepository;
|
||||
|
||||
|
||||
@@ -551,4 +551,112 @@ mod tests {
|
||||
assert_eq!(ctx["count"], 5);
|
||||
assert_eq!(ctx["name"], "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_steps_rejects_branch_with_non_array_next() {
|
||||
let steps = vec![make_step("s1", "Branch", StepType::Branch, json!("s2"))];
|
||||
assert!(validate_steps(&steps).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_steps_rejects_branch_unknown_ref() {
|
||||
let steps = vec![make_step(
|
||||
"s1",
|
||||
"Branch",
|
||||
StepType::Branch,
|
||||
json!([{"condition": {"x": "y"}, "step": "s99"}]),
|
||||
)];
|
||||
assert!(validate_steps(&steps).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_steps_accepts_branch_valid() {
|
||||
let steps = vec![
|
||||
make_step(
|
||||
"s1",
|
||||
"Branch",
|
||||
StepType::Branch,
|
||||
json!([{"condition": {"ok": true}, "step": "s2"}]),
|
||||
),
|
||||
make_step("s2", "End", StepType::Task, json!(null)),
|
||||
];
|
||||
assert!(validate_steps(&steps).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_steps_rejects_parallel_with_non_array_next() {
|
||||
let steps = vec![make_step("s1", "Par", StepType::Parallel, json!("s2"))];
|
||||
assert!(validate_steps(&steps).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_steps_rejects_parallel_unknown_ref() {
|
||||
let steps = vec![make_step("s1", "Par", StepType::Parallel, json!(["s99"]))];
|
||||
assert!(validate_steps(&steps).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_steps_accepts_parallel_valid() {
|
||||
let steps = vec![
|
||||
make_step("s1", "Par", StepType::Parallel, json!(["s2", "s3"])),
|
||||
make_step("s2", "A", StepType::Task, json!(null)),
|
||||
make_step("s3", "B", StepType::Task, json!(null)),
|
||||
];
|
||||
assert!(validate_steps(&steps).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_steps_accepts_empty_next_for_task() {
|
||||
let steps = vec![make_step("s1", "Final", StepType::Task, json!(null))];
|
||||
assert!(validate_steps(&steps).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_next_parallel_returns_first() {
|
||||
let step = make_step("s1", "Par", StepType::Parallel, json!(["s2", "s3"]));
|
||||
assert_eq!(resolve_next_step(&step, &json!({})), Some("s2".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_condition_bool_match() {
|
||||
let cond = json!({"active": true});
|
||||
let ctx = json!({"active": true, "name": "test"});
|
||||
assert!(evaluate_condition(&cond, &ctx));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_condition_number_mismatch() {
|
||||
let cond = json!({"count": 5});
|
||||
let ctx = json!({"count": 3});
|
||||
assert!(!evaluate_condition(&cond, &ctx));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_condition_missing_field() {
|
||||
let cond = json!({"missing": "value"});
|
||||
let ctx = json!({});
|
||||
assert!(!evaluate_condition(&cond, &ctx));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evaluate_condition_non_object_returns_false() {
|
||||
assert!(!evaluate_condition(&json!("string"), &json!({})));
|
||||
assert!(!evaluate_condition(&json!(42), &json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_output_preserves_existing_keys() {
|
||||
let mut ctx = json!({"a": 1, "b": 2});
|
||||
merge_output_into_context(&mut ctx, &json!({"b": 99, "c": 3}));
|
||||
assert_eq!(ctx["a"], 1);
|
||||
assert_eq!(ctx["b"], 99);
|
||||
assert_eq!(ctx["c"], 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_output_non_object_context_noop() {
|
||||
let mut ctx = json!("string");
|
||||
merge_output_into_context(&mut ctx, &json!({"a": 1}));
|
||||
assert_eq!(ctx, json!("string"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
//! 加密工具函数。
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hmac_sha1_deterministic() {
|
||||
let a = hmac_sha1_sign("key", "data");
|
||||
let b = hmac_sha1_sign("key", "data");
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_sha1_different_keys() {
|
||||
let a = hmac_sha1_sign("key1", "data");
|
||||
let b = hmac_sha1_sign("key2", "data");
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_sha1_different_data() {
|
||||
let a = hmac_sha1_sign("key", "data1");
|
||||
let b = hmac_sha1_sign("key", "data2");
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_sha1_is_base64() {
|
||||
let result = hmac_sha1_sign("secret", "message");
|
||||
use base64::Engine;
|
||||
assert!(
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(&result)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 使用 HMAC-SHA1 签名(Base64 编码)。
|
||||
///
|
||||
/// key 会自动追加 `&` 后缀(OAuth 1.0 签名规范)。
|
||||
|
||||
@@ -1,5 +1,51 @@
|
||||
//! ID 与时间戳生成工具
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_document_id_is_valid_uuid_v7() {
|
||||
let id = new_document_id();
|
||||
let parsed = uuid::Uuid::parse_str(&id).unwrap();
|
||||
assert_eq!(parsed.get_version_num(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_document_id_and_timestamp_returns_both() {
|
||||
let (id, ts) = new_document_id_and_timestamp();
|
||||
assert!(uuid::Uuid::parse_str(&id).is_ok());
|
||||
assert!(!ts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_id_and_timestamp_is_alias() {
|
||||
let (id, ts) = new_id_and_timestamp();
|
||||
assert!(uuid::Uuid::parse_str(&id).is_ok());
|
||||
assert!(!ts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_hex_length() {
|
||||
let hex = random_hex(16);
|
||||
assert_eq!(hex.len(), 32);
|
||||
assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_hex_uniqueness() {
|
||||
let a = random_hex(32);
|
||||
let b = random_hex(32);
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_hex_empty() {
|
||||
let hex = random_hex(0);
|
||||
assert_eq!(hex, "");
|
||||
}
|
||||
}
|
||||
|
||||
/// 生成 document_id(UUID v7)和当前站点时区时间戳。
|
||||
///
|
||||
/// 用于 model 层的 `create` 函数。`id` 由数据库自增生成,
|
||||
|
||||
+32
-3
@@ -19,9 +19,9 @@ use raisfast::cache::MemoryCache;
|
||||
use raisfast::config::app::AppConfig;
|
||||
use raisfast::handlers::{
|
||||
api_token as h_token, auth as h_auth, category as h_cat, comment as h_cmt, cron as h_cron,
|
||||
health as h_health, media as h_media, options as h_options, plugin as h_plugin, post as h_post,
|
||||
rbac as h_rbac, rss as h_rss, sse as h_sse, stats as h_stats, tag as h_tag, tenant as h_tenant,
|
||||
user as h_user,
|
||||
health as h_health, media as h_media, options as h_options, page as h_page, plugin as h_plugin,
|
||||
post as h_post, rbac as h_rbac, reusable_block as h_block, rss as h_rss, sse as h_sse,
|
||||
stats as h_stats, tag as h_tag, tenant as h_tenant, user as h_user,
|
||||
};
|
||||
use raisfast::middleware::locale::locale_middleware;
|
||||
use raisfast::middleware::rate_limit::{
|
||||
@@ -269,6 +269,31 @@ pub(crate) async fn test_app() -> (axum::Router, AppState) {
|
||||
"/admin/workflows/instances/{id}/logs",
|
||||
get(raisfast::handlers::workflow::get_step_logs),
|
||||
)
|
||||
.route("/pages", get(h_page::list).post(h_page::create))
|
||||
.route(
|
||||
"/pages/{slug}",
|
||||
get(h_page::get_by_slug)
|
||||
.put(h_page::update)
|
||||
.delete(h_page::delete),
|
||||
)
|
||||
.route("/admin/pages", get(h_page::admin_list))
|
||||
.route(
|
||||
"/admin/pages/{id}",
|
||||
get(h_page::admin_get)
|
||||
.put(h_page::update)
|
||||
.delete(h_page::delete),
|
||||
)
|
||||
.route("/admin/pages/{id}/status", put(h_page::update_status))
|
||||
.route(
|
||||
"/admin/reusable-blocks",
|
||||
get(h_block::list_reusable).post(h_block::create_reusable),
|
||||
)
|
||||
.route(
|
||||
"/admin/reusable-blocks/{id}",
|
||||
get(h_block::get_reusable)
|
||||
.put(h_block::update_reusable)
|
||||
.delete(h_block::delete_reusable),
|
||||
)
|
||||
.layer(from_fn(global_rate_limit))
|
||||
.layer(axum::Extension(RateLimiterSet::new_default()));
|
||||
|
||||
@@ -463,12 +488,16 @@ mod health;
|
||||
mod media;
|
||||
#[path = "api/options.rs"]
|
||||
mod options;
|
||||
#[path = "api/page.rs"]
|
||||
mod page;
|
||||
#[path = "api/plugin.rs"]
|
||||
mod plugin;
|
||||
#[path = "api/post.rs"]
|
||||
mod post;
|
||||
#[path = "api/rbac.rs"]
|
||||
mod rbac;
|
||||
#[path = "api/reusable_block.rs"]
|
||||
mod reusable_block;
|
||||
#[path = "api/rss.rs"]
|
||||
mod rss;
|
||||
#[path = "api/sse.rs"]
|
||||
|
||||
@@ -171,3 +171,91 @@ async fn logout_without_token() {
|
||||
send(&mut app, post_json("/api/v1/auth/logout", json!({}))).await;
|
||||
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_then_login_and_access_me() {
|
||||
let (mut app, _) = test_app().await;
|
||||
let (access, _) = register_and_login(
|
||||
&mut app,
|
||||
"lifecycle@test.com",
|
||||
"lifecycleuser",
|
||||
"Password123",
|
||||
)
|
||||
.await;
|
||||
|
||||
let (status, body) = send(&mut app, get_auth("/api/v1/users/me", &access)).await;
|
||||
assert!(status.is_success(), "{status} {body:?}");
|
||||
assert_eq!(body["data"]["email"], "lifecycle@test.com");
|
||||
assert_eq!(body["data"]["username"], "lifecycleuser");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_duplicate_username() {
|
||||
let (mut app, _) = test_app().await;
|
||||
let req_body =
|
||||
json!({"email": "dupu1@test.com", "username": "dupuser", "password": "Password123"});
|
||||
let (s, _): (StatusCode, Value) =
|
||||
send(&mut app, post_json("/api/v1/auth/register", req_body)).await;
|
||||
assert!(s.is_success());
|
||||
|
||||
let (status, body): (StatusCode, Value) = send(
|
||||
&mut app,
|
||||
post_json(
|
||||
"/api/v1/auth/register",
|
||||
json!({"email": "dupu2@test.com", "username": "dupuser", "password": "Password123"}),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::CONFLICT);
|
||||
assert_eq!(body["code"], 40900);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn refresh_token_after_password_change() {
|
||||
let (mut app, _) = test_app().await;
|
||||
let (access, refresh) =
|
||||
register_and_login(&mut app, "rpc@test.com", "rpcuser", "OldPass123").await;
|
||||
|
||||
let (status, _): (StatusCode, Value) = send(
|
||||
&mut app,
|
||||
put_json_auth(
|
||||
"/api/v1/users/me/password",
|
||||
json!({"old_password": "OldPass123", "new_password": "NewPass456"}),
|
||||
&access,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success());
|
||||
|
||||
let (status, _): (StatusCode, Value) = send(
|
||||
&mut app,
|
||||
post_json("/api/v1/auth/refresh", json!({"refresh_token": refresh})),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"old refresh token should be invalidated after password change"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_invalidates_access() {
|
||||
let (mut app, _) = test_app().await;
|
||||
let (access, _) =
|
||||
register_and_login(&mut app, "loinv@test.com", "loinvuser", "Password123").await;
|
||||
|
||||
let (status, _): (StatusCode, Value) = send(
|
||||
&mut app,
|
||||
post_json_auth("/api/v1/auth/logout", json!({}), &access),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success());
|
||||
|
||||
let (status, _): (StatusCode, Value) =
|
||||
send(&mut app, get_auth("/api/v1/users/me", &access)).await;
|
||||
assert!(
|
||||
status.is_success(),
|
||||
"JWT is stateless — still valid after logout"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,10 +182,11 @@ async fn update_status_admin() {
|
||||
)
|
||||
.await;
|
||||
|
||||
let cid: String = sqlx::query_scalar("SELECT document_id FROM comments WHERE content = 'mod me'")
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let cid: String =
|
||||
sqlx::query_scalar("SELECT document_id FROM comments WHERE content = 'mod me'")
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (admin_int_id, admin_doc_id) = create_admin(&state.pool).await;
|
||||
let admin_tok = make_token(&admin_doc_id, admin_int_id, "admin");
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
use super::*;
|
||||
|
||||
async fn admin_author() -> (axum::Router, AppState, String) {
|
||||
let (app, state) = test_app().await;
|
||||
let (int_id, doc_id) = create_author(&state.pool).await;
|
||||
let tok = make_token(&doc_id, int_id, "author");
|
||||
(app, state, tok)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_page_success() {
|
||||
let (mut app, _, tok) = admin_author().await;
|
||||
let (status, body) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/pages",
|
||||
json!({"title": "About Us", "content": "Our story", "status": "published"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "create page: {status} {body:?}");
|
||||
assert_eq!(body["data"]["title"], "About Us");
|
||||
assert!(body["data"]["slug"].is_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_page_validation_empty_title() {
|
||||
let (mut app, _, tok) = admin_author().await;
|
||||
let (status, body) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/pages",
|
||||
json!({"title": "", "content": "c", "status": "published"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(!status.is_success(), "expected failure: {status} {body:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_published_pages() {
|
||||
let (mut app, _, tok) = admin_author().await;
|
||||
let _: (StatusCode, Value) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/pages",
|
||||
json!({"title": "Page1", "content": "c", "status": "published"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let (status, body) = send(&mut app, get_req("/api/v1/pages")).await;
|
||||
assert!(status.is_success(), "list pages: {status} {body:?}");
|
||||
let items = body["data"]["items"].as_array().unwrap();
|
||||
assert!(!items.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_page_by_slug() {
|
||||
let (mut app, _, tok) = admin_author().await;
|
||||
let (_, create_body) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/pages",
|
||||
json!({"title": "Contact", "content": "info", "status": "published"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let slug = create_body["data"]["slug"].as_str().unwrap();
|
||||
let (status, body) = send(&mut app, get_req(&format!("/api/v1/pages/{slug}"))).await;
|
||||
assert!(status.is_success(), "get page: {status} {body:?}");
|
||||
assert_eq!(body["data"]["title"], "Contact");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_page_not_found() {
|
||||
let (mut app, _, _) = admin_author().await;
|
||||
let (status, _) = send(&mut app, get_req("/api/v1/pages/nonexistent")).await;
|
||||
assert_eq!(status, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_page() {
|
||||
let (mut app, _, tok) = admin_author().await;
|
||||
let (_, create_body) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/pages",
|
||||
json!({"title": "Original", "content": "c", "status": "published"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let doc_id = create_body["data"]["document_id"].as_str().unwrap();
|
||||
let (status, body) = send(
|
||||
&mut app,
|
||||
put_json_auth(
|
||||
&format!("/api/v1/pages/{doc_id}"),
|
||||
json!({"title": "Updated"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "update page: {status} {body:?}");
|
||||
assert_eq!(body["data"]["title"], "Updated");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_page() {
|
||||
let (mut app, _, tok) = admin_author().await;
|
||||
let (_, create_body) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/pages",
|
||||
json!({"title": "ToDelete", "content": "c", "status": "published"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let doc_id = create_body["data"]["document_id"].as_str().unwrap();
|
||||
let slug = create_body["data"]["slug"].as_str().unwrap();
|
||||
let (status, _) = send(
|
||||
&mut app,
|
||||
delete_auth(&format!("/api/v1/pages/{doc_id}"), &tok),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success());
|
||||
let (s, _) = send(&mut app, get_req(&format!("/api/v1/pages/{slug}"))).await;
|
||||
assert_eq!(s, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_list_pages() {
|
||||
let (mut app, _, tok) = admin_author().await;
|
||||
let _: (StatusCode, Value) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/pages",
|
||||
json!({"title": "Admin Page", "content": "c", "status": "draft"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let (status, body) = send(&mut app, get_auth("/api/v1/admin/pages", &tok)).await;
|
||||
assert!(status.is_success(), "admin list: {status} {body:?}");
|
||||
let items = body["data"]["items"].as_array().unwrap();
|
||||
assert!(!items.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_page_status() {
|
||||
let (mut app, state, tok) = admin_author().await;
|
||||
let (_, create_body) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/pages",
|
||||
json!({"title": "StatusTest", "content": "c", "status": "draft"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let doc_id = create_body["data"]["document_id"].as_str().unwrap();
|
||||
let (status, body) = send(
|
||||
&mut app,
|
||||
put_json_auth(
|
||||
&format!("/api/v1/admin/pages/{doc_id}/status"),
|
||||
json!({"status": "published"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "update status: {status} {body:?}");
|
||||
}
|
||||
@@ -270,3 +270,142 @@ async fn filter_by_category() {
|
||||
let cat_int: i64 = c.cat_id.parse().unwrap();
|
||||
assert!(items.iter().all(|p| p["category_id"] == cat_int));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn filter_by_status() {
|
||||
let mut c = setup().await;
|
||||
let _: (StatusCode, Value) = send(
|
||||
&mut c.app,
|
||||
post_json_auth(
|
||||
"/api/v1/posts",
|
||||
json!({"title": "Published1", "content": "c", "status": "published", "category_id": c.cat_id}),
|
||||
&c.tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let _: (StatusCode, Value) = send(
|
||||
&mut c.app,
|
||||
post_json_auth(
|
||||
"/api/v1/posts",
|
||||
json!({"title": "Draft1", "content": "c", "status": "draft", "category_id": c.cat_id}),
|
||||
&c.tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, body): (StatusCode, Value) =
|
||||
send(&mut c.app, get_req("/api/v1/posts?status=published")).await;
|
||||
let items = body["data"]["items"].as_array().unwrap();
|
||||
assert!(
|
||||
items.iter().all(|p| p["status"] == "published"),
|
||||
"expected all published, got: {items:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn filter_by_tag() {
|
||||
let mut c = setup().await;
|
||||
let (_, tb): (StatusCode, Value) = send(
|
||||
&mut c.app,
|
||||
post_json_auth("/api/v1/tags", json!({"name": "go"}), &c.tok),
|
||||
)
|
||||
.await;
|
||||
let tag_go = tb["data"]["id"].as_i64().unwrap().to_string();
|
||||
|
||||
let _: (StatusCode, Value) = send(
|
||||
&mut c.app,
|
||||
post_json_auth(
|
||||
"/api/v1/posts",
|
||||
json!({"title": "RustPost", "content": "c", "status": "published", "tag_ids": [c.tag_id]}),
|
||||
&c.tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let _: (StatusCode, Value) = send(
|
||||
&mut c.app,
|
||||
post_json_auth(
|
||||
"/api/v1/posts",
|
||||
json!({"title": "GoPost", "content": "c", "status": "published", "tag_ids": [&tag_go]}),
|
||||
&c.tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_, body): (StatusCode, Value) = send(
|
||||
&mut c.app,
|
||||
get_req(&format!("/api/v1/posts?tag_id={}", c.tag_id)),
|
||||
)
|
||||
.await;
|
||||
let items = body["data"]["items"].as_array().unwrap();
|
||||
assert_eq!(items.len(), 1, "expected 1 post, got: {items:?}");
|
||||
assert_eq!(items[0]["title"], "RustPost");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_changes_category() {
|
||||
let mut c = setup().await;
|
||||
let (_, cb): (StatusCode, Value) = send(
|
||||
&mut c.app,
|
||||
post_json_auth("/api/v1/categories", json!({"name": "CatB"}), &c.tok),
|
||||
)
|
||||
.await;
|
||||
let cat_b = cb["data"]["id"].as_i64().unwrap().to_string();
|
||||
|
||||
let slug = create_published_post(&mut c.app, &c.tok).await;
|
||||
let (status, body): (StatusCode, Value) = send(
|
||||
&mut c.app,
|
||||
put_json_auth(
|
||||
&format!("/api/v1/posts/{slug}"),
|
||||
json!({"category_id": &cat_b}),
|
||||
&c.tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "{status} {body:?}");
|
||||
assert_eq!(body["data"]["category_id"], cat_b.parse::<i64>().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_with_excerpt() {
|
||||
let mut c = setup().await;
|
||||
let (status, body): (StatusCode, Value) = send(
|
||||
&mut c.app,
|
||||
post_json_auth(
|
||||
"/api/v1/posts",
|
||||
json!({
|
||||
"title": "ExcerptPost",
|
||||
"content": "Some content here",
|
||||
"status": "published",
|
||||
"excerpt": "Custom excerpt text",
|
||||
"category_id": c.cat_id,
|
||||
}),
|
||||
&c.tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "{status} {body:?}");
|
||||
assert_eq!(body["data"]["excerpt"], "Custom excerpt text");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_sorting_by_created_at() {
|
||||
let mut c = setup().await;
|
||||
let uid = uuid::Uuid::now_v7().to_string();
|
||||
for i in 1..=3u8 {
|
||||
let (status, body): (StatusCode, Value) = send(
|
||||
&mut c.app,
|
||||
post_json_auth(
|
||||
"/api/v1/posts",
|
||||
json!({"title": format!("Sort{uid}{i}"), "content": "c", "status": "published"}),
|
||||
&c.tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "create post {i}: {status} {body:?}");
|
||||
}
|
||||
|
||||
let (status, body): (StatusCode, Value) = send(&mut c.app, get_req("/api/v1/posts")).await;
|
||||
assert!(status.is_success(), "list: {status} {body:?}");
|
||||
let total = body["data"]["total"].as_i64().unwrap_or(0);
|
||||
assert!(total >= 3, "expected total >= 3, got {total}");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
use super::*;
|
||||
|
||||
async fn admin_setup() -> (axum::Router, AppState, String) {
|
||||
let (app, state) = test_app().await;
|
||||
let (int_id, doc_id) = create_author(&state.pool).await;
|
||||
let tok = make_token(&doc_id, int_id, "author");
|
||||
(app, state, tok)
|
||||
}
|
||||
|
||||
fn block_content() -> serde_json::Value {
|
||||
json!([{"type": "richtext", "content": "Hello world"}])
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_reusable_block_success() {
|
||||
let (mut app, _, tok) = admin_setup().await;
|
||||
let (status, body) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/admin/reusable-blocks",
|
||||
json!({
|
||||
"name": "Hero Banner",
|
||||
"block_type": "hero",
|
||||
"content": block_content().to_string()
|
||||
}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "create block: {status} {body:?}");
|
||||
assert_eq!(body["data"]["name"], "Hero Banner");
|
||||
assert!(body["data"]["document_id"].is_string());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_block_validation_empty_name() {
|
||||
let (mut app, _, tok) = admin_setup().await;
|
||||
let (status, _) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/admin/reusable-blocks",
|
||||
json!({"name": "", "block_type": "hero", "content": "[]"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(!status.is_success());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_reusable_blocks() {
|
||||
let (mut app, _, tok) = admin_setup().await;
|
||||
let _: (StatusCode, Value) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/admin/reusable-blocks",
|
||||
json!({"name": "Block1", "block_type": "text", "content": "[]"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let (status, body) = send(&mut app, get_auth("/api/v1/admin/reusable-blocks", &tok)).await;
|
||||
assert!(status.is_success(), "list blocks: {status} {body:?}");
|
||||
let items = body["data"].as_array().unwrap();
|
||||
assert!(!items.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_reusable_block_by_id() {
|
||||
let (mut app, _, tok) = admin_setup().await;
|
||||
let (_, create_body) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/admin/reusable-blocks",
|
||||
json!({"name": "CTA", "block_type": "cta", "content": "[]"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let id = create_body["data"]["document_id"].as_str().unwrap();
|
||||
let (status, body) = send(
|
||||
&mut app,
|
||||
get_auth(&format!("/api/v1/admin/reusable-blocks/{id}"), &tok),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "get block: {status} {body:?}");
|
||||
assert_eq!(body["data"]["name"], "CTA");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_reusable_block_not_found() {
|
||||
let (mut app, _, tok) = admin_setup().await;
|
||||
let fake = uuid::Uuid::now_v7().to_string();
|
||||
let (status, _) = send(
|
||||
&mut app,
|
||||
get_auth(&format!("/api/v1/admin/reusable-blocks/{fake}"), &tok),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_reusable_block() {
|
||||
let (mut app, _, tok) = admin_setup().await;
|
||||
let (_, create_body) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/admin/reusable-blocks",
|
||||
json!({"name": "Old", "block_type": "text", "content": "[]"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let id = create_body["data"]["document_id"].as_str().unwrap();
|
||||
let (status, body) = send(
|
||||
&mut app,
|
||||
put_json_auth(
|
||||
&format!("/api/v1/admin/reusable-blocks/{id}"),
|
||||
json!({"name": "New", "description": "updated"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "update block: {status} {body:?}");
|
||||
assert_eq!(body["data"]["name"], "New");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_reusable_block() {
|
||||
let (mut app, _, tok) = admin_setup().await;
|
||||
let (_, create_body) = send(
|
||||
&mut app,
|
||||
post_json_auth(
|
||||
"/api/v1/admin/reusable-blocks",
|
||||
json!({"name": "Delete", "block_type": "text", "content": "[]"}),
|
||||
&tok,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
let id = create_body["data"]["document_id"].as_str().unwrap();
|
||||
let (status, _) = send(
|
||||
&mut app,
|
||||
delete_auth(&format!("/api/v1/admin/reusable-blocks/{id}"), &tok),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success());
|
||||
let (s, _) = send(
|
||||
&mut app,
|
||||
get_auth(&format!("/api/v1/admin/reusable-blocks/{id}"), &tok),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(s, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_reusable_block_not_found() {
|
||||
let (mut app, _, tok) = admin_setup().await;
|
||||
let fake = uuid::Uuid::now_v7().to_string();
|
||||
let (status, _) = send(
|
||||
&mut app,
|
||||
delete_auth(&format!("/api/v1/admin/reusable-blocks/{fake}"), &tok),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NOT_FOUND);
|
||||
}
|
||||
@@ -119,3 +119,82 @@ async fn list_users_admin_only() {
|
||||
assert!(body["data"]["items"].is_array());
|
||||
assert!(body["data"]["total"].is_number());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_me_with_bio_and_website() {
|
||||
let (mut app, _) = test_app().await;
|
||||
let (access, _) =
|
||||
register_and_login(&mut app, "bioweb@test.com", "biowebuser", "Password123").await;
|
||||
let (status, body): (StatusCode, Value) = send(
|
||||
&mut app,
|
||||
put_json_auth(
|
||||
"/api/v1/users/me",
|
||||
json!({"bio": "I write Rust", "website": "https://rust-lang.org"}),
|
||||
&access,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "{status} {body:?}");
|
||||
assert_eq!(body["data"]["bio"], "I write Rust");
|
||||
assert_eq!(body["data"]["website"], "https://rust-lang.org");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn change_password_too_short() {
|
||||
let (mut app, _) = test_app().await;
|
||||
let (access, _) =
|
||||
register_and_login(&mut app, "short@test.com", "shortuser", "Password123").await;
|
||||
let (status, body): (StatusCode, Value) = send(
|
||||
&mut app,
|
||||
put_json_auth(
|
||||
"/api/v1/users/me/password",
|
||||
json!({"old_password": "Password123", "new_password": "abc"}),
|
||||
&access,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::BAD_REQUEST, "{status} {body:?}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_can_update_role() {
|
||||
let (mut app, state) = test_app().await;
|
||||
let admin_id = create_admin(&state.pool).await;
|
||||
let admin_token = make_token(&admin_id.1, admin_id.0, "admin");
|
||||
let _ = register_and_login(&mut app, "roleuser@test.com", "roleuser", "Password123").await;
|
||||
let reader_id: String =
|
||||
sqlx::query_scalar("SELECT document_id FROM users WHERE email = 'roleuser@test.com'")
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let (status, body): (StatusCode, Value) = send(
|
||||
&mut app,
|
||||
put_json_auth(
|
||||
&format!("/api/v1/users/{reader_id}/role"),
|
||||
json!({"role": "author"}),
|
||||
&admin_token,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
assert!(status.is_success(), "{status} {body:?}");
|
||||
assert_eq!(body["data"]["role"], "author");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_user_by_id_returns_public_info() {
|
||||
let (mut app, state) = test_app().await;
|
||||
let _ = register_and_login(&mut app, "pubinfo@test.com", "pubinfouser", "Password123").await;
|
||||
let user_id: String =
|
||||
sqlx::query_scalar("SELECT document_id FROM users WHERE email = 'pubinfo@test.com'")
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let (status, body): (StatusCode, Value) =
|
||||
send(&mut app, get_req(&format!("/api/v1/users/{user_id}"))).await;
|
||||
assert!(status.is_success());
|
||||
assert_eq!(body["data"]["id"], user_id);
|
||||
assert_eq!(body["data"]["email"], "pubinfo@test.com");
|
||||
assert_eq!(body["data"]["username"], "pubinfouser");
|
||||
assert!(body["data"]["created_at"].is_string());
|
||||
assert!(body["data"]["updated_at"].is_string());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user