Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions backend/tests/api_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -676,19 +676,29 @@ async fn test_cors_origins() {
}
}

// --- Plan query filter tests ---

#[tokio::test]
async fn test_get_plans_filter_by_beneficiary_only() {
#[tokio::test]
async fn test_calculate_yield_with_rate() {
let app = setup_app();
let response = app
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri("/api/plans?beneficiary=GBENEF123")
.uri("/api/yield/calculate?amount=10000&yield_rate_bps=500&elapsed_secs=31557600")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}

#[tokio::test]
async fn test_get_plans_filter_by_both_owner_and_beneficiary() {

assert_eq!(response.status(), StatusCode::OK);
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
Expand All @@ -712,12 +722,18 @@ async fn test_calculate_yield_default_rate() {
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri("/api/plans?owner=GOWNER123&beneficiary=GBENEF123")
.uri("/api/yield/calculate?amount=2000&elapsed_secs=31557600")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}

#[tokio::test]
async fn test_get_plans_all_no_filters() {

assert_eq!(response.status(), StatusCode::OK);
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
Expand All @@ -735,12 +751,41 @@ async fn test_calculate_yield_zero_elapsed() {
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri("/api/plans")
.uri("/api/yield/calculate?amount=5000&yield_rate_bps=1000&elapsed_secs=0")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}

#[tokio::test]
async fn test_get_plans_owner_filter_caches_on_miss() {
let cache = PlanCache::memory();
let query = inheritx_backend::api::PlanQuery {
owner: Some("GOWNER123".to_string()),
beneficiary: None,
};
let cached_plans = vec![PlanResponse {
id: uuid::Uuid::new_v4(),
owner_address: "GOWNER123".to_string(),
token_address: "USDC".to_string(),
amount: rust_decimal::Decimal::from(1000),
grace_period: 3600,
grace_period_seconds: 3600,
earn_yield: true,
last_ping: 1_718_000_000,
is_active: true,
status: "ACTIVE".to_string(),
yield_rate_bps: 500,
accrued_yield: 25.5,
created_at: chrono::Utc::now(),
beneficiaries: vec![],
}];
cache.set_plans(&query, &cached_plans).await.unwrap();
let app = setup_app_with_cache(cache);

assert_eq!(response.status(), StatusCode::OK);
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
Expand All @@ -757,12 +802,20 @@ async fn test_calculate_yield_invalid_amount() {
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri("/api/plans?owner=GOWNER123")
.uri("/api/yield/calculate?amount=-100&yield_rate_bps=500&elapsed_secs=1000")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let body: serde_json::Value = serde_json::from_slice(&body_bytes).unwrap();
assert!(body.is_array());
assert_eq!(body[0]["owner_address"], "GOWNER123");

assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
212 changes: 212 additions & 0 deletions backend/tests/jwt_auth_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
use axum::{
body::Body,
http::{self, Request, StatusCode},
};
use inheritx_backend::auth::Claims;
use jsonwebtoken::{encode, EncodingKey, Header};
use std::time::Duration;
use tower::ServiceExt;

const JWT_SECRET: &str = "test-jwt-secret-for-testing";

fn ensure_jwt_secret() {
std::env::set_var("JWT_SECRET", JWT_SECRET);
}

fn setup_app() -> axum::Router {
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/test".to_string());
let db_pool = sqlx::postgres::PgPoolOptions::new()
.acquire_timeout(Duration::from_secs(1))
.connect_lazy(&database_url)
.unwrap();
let state = std::sync::Arc::new(inheritx_backend::AppState {
anchor: std::sync::Arc::new(inheritx_backend::stellar_anchor::AnchorRegistry::new()),
db_pool,
kyc_tx: tokio::sync::broadcast::channel(16).0,
kyc_webhook_secret: None,
apy_config: inheritx_backend::yield_calculator::ApyConfig::default(),
plan_cache: inheritx_backend::PlanCache::disabled(),
apy_cache: dashmap::DashMap::new(),
stellar_submit: inheritx_backend::stellar_submit::StellarSubmitClient::new(
"https://horizon-testnet.stellar.org".to_string(),
),
});
inheritx_backend::create_router(state)
}

fn plan_report_uri() -> String {
format!("/api/plans/{}/report", uuid::Uuid::nil())
}

fn generate_token(role: &str, secret: &str) -> String {
let claims = Claims {
sub: "test-admin-id".to_string(),
role: role.to_string(),
exp: (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp() as usize,
};
encode(
&Header::new(jsonwebtoken::Algorithm::HS256),
&claims,
&EncodingKey::from_secret(secret.as_ref()),
)
.unwrap()
}

#[tokio::test]
async fn test_jwt_missing_authorization_header() {
let app = setup_app();
let response = app
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri(&plan_report_uri())
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn test_jwt_invalid_header_format() {
let app = setup_app();
let response = app
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri(&plan_report_uri())
.header("Authorization", "NotBearer token")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn test_jwt_empty_bearer_token() {
let app = setup_app();
let response = app
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri(&plan_report_uri())
.header("Authorization", "Bearer ")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn test_jwt_invalid_token_payload() {
let app = setup_app();
let response = app
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri(&plan_report_uri())
.header("Authorization", "Bearer invalid.jwt.token")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn test_jwt_valid_token_with_non_admin_role() {
ensure_jwt_secret();
let token = generate_token("user", JWT_SECRET);
let app = setup_app();
let response = app
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri(&plan_report_uri())
.header("Authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn test_jwt_valid_admin_token_passes_middleware() {
ensure_jwt_secret();
let token = generate_token("admin", JWT_SECRET);
let app = setup_app();
let response = app
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri(&plan_report_uri())
.header("Authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let status = response.status();
assert_ne!(
status,
StatusCode::UNAUTHORIZED,
"JWT middleware should have passed for valid admin token"
);
}

#[tokio::test]
async fn test_jwt_token_signed_with_wrong_secret_rejected() {
ensure_jwt_secret();
let token = generate_token("admin", "some-other-secret");
let app = setup_app();
let response = app
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri(&plan_report_uri())
.header("Authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}

#[tokio::test]
async fn test_jwt_expired_token_rejected() {
ensure_jwt_secret();
let claims = Claims {
sub: "test-admin-id".to_string(),
role: "admin".to_string(),
exp: (chrono::Utc::now() - chrono::Duration::hours(1)).timestamp() as usize,
};
let token = encode(
&Header::new(jsonwebtoken::Algorithm::HS256),
&claims,
&EncodingKey::from_secret(JWT_SECRET.as_ref()),
)
.unwrap();
let app = setup_app();
let response = app
.oneshot(
Request::builder()
.method(http::Method::GET)
.uri(&plan_report_uri())
.header("Authorization", format!("Bearer {token}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
Loading
Loading