Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
//! Integration tests for McpOAuthService with real SQLite.
|
||||
//!
|
||||
//! Tests from test-plan §4 (OAuth) at the service layer.
|
||||
//! These tests exercise check_status, logout, get_authenticated_servers,
|
||||
//! and get_token with a real DB. The full login flow (browser + callback)
|
||||
//! cannot be tested end-to-end here; it requires a mock OAuth server.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use nomifun_db::{IOAuthTokenRepository, SqliteOAuthTokenRepository, UpsertOAuthTokenParams};
|
||||
use nomifun_mcp::McpOAuthService;
|
||||
|
||||
async fn make_service() -> (McpOAuthService, Arc<dyn IOAuthTokenRepository>) {
|
||||
let db = nomifun_db::init_database_memory().await.unwrap();
|
||||
let repo: Arc<dyn IOAuthTokenRepository> = Arc::new(SqliteOAuthTokenRepository::new(db.pool().clone()));
|
||||
let svc = McpOAuthService::new(repo.clone(), reqwest::Client::new());
|
||||
// Keep db alive by leaking it (integration test only).
|
||||
std::mem::forget(db);
|
||||
(svc, repo)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OA-1: Unauthenticated server returns false
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_status_unauthenticated_returns_false() {
|
||||
let (svc, _repo) = make_service().await;
|
||||
let status = svc.check_oauth_status("https://new-server.example.com").await.unwrap();
|
||||
assert!(!status.authenticated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OA-2: Authenticated server returns true
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_status_authenticated_returns_true() {
|
||||
let (svc, repo) = make_service().await;
|
||||
|
||||
// Seed a valid token.
|
||||
repo.upsert(UpsertOAuthTokenParams {
|
||||
server_url: "https://mcp.example.com",
|
||||
access_token: "access_123",
|
||||
refresh_token: Some("refresh_456"),
|
||||
token_type: "bearer",
|
||||
// Expires in the far future.
|
||||
expires_at: Some(nomifun_common::now_ms() + 3_600_000),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let status = svc.check_oauth_status("https://mcp.example.com").await.unwrap();
|
||||
assert!(status.authenticated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OA-2b: Expired token treated as unauthenticated
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_status_expired_token_returns_false() {
|
||||
let (svc, repo) = make_service().await;
|
||||
|
||||
repo.upsert(UpsertOAuthTokenParams {
|
||||
server_url: "https://expired.example.com",
|
||||
access_token: "old_token",
|
||||
refresh_token: None,
|
||||
token_type: "bearer",
|
||||
// Already expired.
|
||||
expires_at: Some(1000),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let status = svc.check_oauth_status("https://expired.example.com").await.unwrap();
|
||||
assert!(!status.authenticated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OA-2c: Token with no expiry treated as valid
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_status_no_expiry_treated_as_valid() {
|
||||
let (svc, repo) = make_service().await;
|
||||
|
||||
repo.upsert(UpsertOAuthTokenParams {
|
||||
server_url: "https://no-expiry.example.com",
|
||||
access_token: "no_exp_token",
|
||||
refresh_token: None,
|
||||
token_type: "bearer",
|
||||
expires_at: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let status = svc.check_oauth_status("https://no-expiry.example.com").await.unwrap();
|
||||
assert!(status.authenticated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OA-3: Get all authenticated URLs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_authenticated_servers_returns_all_urls() {
|
||||
let (svc, repo) = make_service().await;
|
||||
|
||||
repo.upsert(UpsertOAuthTokenParams {
|
||||
server_url: "https://a.example.com",
|
||||
access_token: "tok_a",
|
||||
refresh_token: None,
|
||||
token_type: "bearer",
|
||||
expires_at: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
repo.upsert(UpsertOAuthTokenParams {
|
||||
server_url: "https://b.example.com",
|
||||
access_token: "tok_b",
|
||||
refresh_token: None,
|
||||
token_type: "bearer",
|
||||
expires_at: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let urls = svc.get_authenticated_servers().await.unwrap();
|
||||
assert_eq!(urls.len(), 2);
|
||||
assert!(urls.contains(&"https://a.example.com".to_string()));
|
||||
assert!(urls.contains(&"https://b.example.com".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_authenticated_servers_empty_when_no_tokens() {
|
||||
let (svc, _repo) = make_service().await;
|
||||
let urls = svc.get_authenticated_servers().await.unwrap();
|
||||
assert!(urls.is_empty());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OA-5: Login with invalid URL (no OAuth endpoints discoverable)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn login_invalid_url_returns_error() {
|
||||
let (svc, _repo) = make_service().await;
|
||||
// This URL won't have .well-known endpoints.
|
||||
let result = svc.login("https://127.0.0.1:1").await;
|
||||
// Should return an McpError::OAuth about discovery failure.
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OA-6: Logout deletes stored token
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_deletes_stored_token() {
|
||||
let (svc, repo) = make_service().await;
|
||||
|
||||
repo.upsert(UpsertOAuthTokenParams {
|
||||
server_url: "https://logout.example.com",
|
||||
access_token: "to_delete",
|
||||
refresh_token: None,
|
||||
token_type: "bearer",
|
||||
expires_at: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify token exists.
|
||||
let status = svc.check_oauth_status("https://logout.example.com").await.unwrap();
|
||||
assert!(status.authenticated);
|
||||
|
||||
// Logout.
|
||||
svc.logout("https://logout.example.com").await.unwrap();
|
||||
|
||||
// Verify token is gone.
|
||||
let status = svc.check_oauth_status("https://logout.example.com").await.unwrap();
|
||||
assert!(!status.authenticated);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OA-7: Logout is idempotent for non-authenticated URL
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn logout_idempotent_for_unauthenticated() {
|
||||
let (svc, _repo) = make_service().await;
|
||||
// Should not error.
|
||||
svc.logout("https://never-authed.example.com").await.unwrap();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// get_token tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_token_returns_none_for_unknown_url() {
|
||||
let (svc, _repo) = make_service().await;
|
||||
let token = svc.get_token("https://unknown.example.com").await.unwrap();
|
||||
assert!(token.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_token_returns_access_token_when_valid() {
|
||||
let (svc, repo) = make_service().await;
|
||||
|
||||
repo.upsert(UpsertOAuthTokenParams {
|
||||
server_url: "https://valid.example.com",
|
||||
access_token: "my_access_token",
|
||||
refresh_token: None,
|
||||
token_type: "bearer",
|
||||
expires_at: Some(nomifun_common::now_ms() + 3_600_000),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token = svc.get_token("https://valid.example.com").await.unwrap();
|
||||
assert_eq!(token.as_deref(), Some("my_access_token"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_token_returns_expired_token_when_no_refresh_token() {
|
||||
let (svc, repo) = make_service().await;
|
||||
|
||||
repo.upsert(UpsertOAuthTokenParams {
|
||||
server_url: "https://expired.example.com",
|
||||
access_token: "old_access",
|
||||
refresh_token: None,
|
||||
token_type: "bearer",
|
||||
expires_at: Some(1000),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// With no refresh_token, returns the expired token as-is.
|
||||
let token = svc.get_token("https://expired.example.com").await.unwrap();
|
||||
assert_eq!(token.as_deref(), Some("old_access"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_token_returns_no_expiry_token() {
|
||||
let (svc, repo) = make_service().await;
|
||||
|
||||
repo.upsert(UpsertOAuthTokenParams {
|
||||
server_url: "https://noexp.example.com",
|
||||
access_token: "forever_token",
|
||||
refresh_token: None,
|
||||
token_type: "bearer",
|
||||
expires_at: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let token = svc.get_token("https://noexp.example.com").await.unwrap();
|
||||
assert_eq!(token.as_deref(), Some("forever_token"));
|
||||
}
|
||||
Reference in New Issue
Block a user