feat(oidc): fix oidc, tests, and everything in between

This commit is contained in:
perf3ct 2025-06-27 05:03:27 +00:00
parent e9496b921e
commit 3c5b7c7dfb
15 changed files with 348 additions and 48 deletions

View File

@ -44,6 +44,7 @@ Object.defineProperty(window, 'location', {
writable: true
});
// Mock AuthContext
const mockAuthContextValue = {
user: null,
@ -54,16 +55,35 @@ const mockAuthContextValue = {
};
const MockAuthProvider = ({ children }: { children: React.ReactNode }) => (
<div data-testid="mock-auth-provider">{children}</div>
<AuthProvider>
{children}
</AuthProvider>
);
const MockThemeProvider = ({ children }: { children: React.ReactNode }) => (
<div data-testid="mock-theme-provider">{children}</div>
<ThemeProvider>
{children}
</ThemeProvider>
);
describe('Login - OIDC Features', () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
});
const renderLogin = () => {

View File

@ -17,14 +17,16 @@ vi.mock('../../../services/api', () => ({
}
}));
// Mock useNavigate
// Mock useNavigate and useSearchParams
const mockNavigate = vi.fn();
const mockUseSearchParams = vi.fn(() => [new URLSearchParams('code=test-code&state=test-state')]);
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useNavigate: () => mockNavigate,
useSearchParams: () => [new URLSearchParams('code=test-code&state=test-state')]
useSearchParams: mockUseSearchParams
};
});
@ -45,6 +47,7 @@ Object.defineProperty(window, 'location', {
writable: true
});
// Mock AuthContext
const mockAuthContextValue = {
user: null,
@ -55,12 +58,29 @@ const mockAuthContextValue = {
};
const MockAuthProvider = ({ children }: { children: React.ReactNode }) => (
<div data-testid="mock-auth-provider">{children}</div>
<AuthProvider>
{children}
</AuthProvider>
);
describe('OidcCallback', () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
});
const renderOidcCallback = () => {
@ -106,7 +126,7 @@ describe('OidcCallback', () => {
it('handles authentication error from URL params', () => {
// Mock useSearchParams to return error
vi.mocked(require('react-router-dom').useSearchParams).mockReturnValue([
mockUseSearchParams.mockReturnValueOnce([
new URLSearchParams('error=access_denied&error_description=User+denied+access')
]);
@ -118,7 +138,7 @@ describe('OidcCallback', () => {
it('handles missing authorization code', () => {
// Mock useSearchParams to return no code
vi.mocked(require('react-router-dom').useSearchParams).mockReturnValue([
mockUseSearchParams.mockReturnValueOnce([
new URLSearchParams('')
]);

View File

@ -20,3 +20,18 @@ vi.mock('axios', () => ({
})),
},
}))
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
})

View File

@ -88,23 +88,41 @@ impl Database {
.execute(&self.pool)
.await?;
// Create users table
// Create users table with OIDC support
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(10) DEFAULT 'user',
password_hash VARCHAR(255),
role VARCHAR(20) DEFAULT 'user',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
updated_at TIMESTAMPTZ DEFAULT NOW(),
oidc_subject VARCHAR(255),
oidc_issuer VARCHAR(255),
oidc_email VARCHAR(255),
auth_provider VARCHAR(50) DEFAULT 'local',
CONSTRAINT check_auth_method CHECK (
(auth_provider = 'local' AND password_hash IS NOT NULL) OR
(auth_provider = 'oidc' AND oidc_subject IS NOT NULL AND oidc_issuer IS NOT NULL)
),
CONSTRAINT check_user_role CHECK (role IN ('admin', 'user'))
)
"#,
)
.execute(&self.pool)
.await?;
// Create indexes for OIDC
sqlx::query(r#"CREATE INDEX IF NOT EXISTS idx_users_oidc_subject_issuer ON users(oidc_subject, oidc_issuer)"#)
.execute(&self.pool)
.await?;
sqlx::query(r#"CREATE INDEX IF NOT EXISTS idx_users_auth_provider ON users(auth_provider)"#)
.execute(&self.pool)
.await?;
// Create documents table
sqlx::query(

View File

@ -23,6 +23,7 @@ pub fn router() -> Router<Arc<AppState>> {
.route("/oidc/callback", get(oidc_callback))
}
#[utoipa::path(
post,
path = "/api/auth/register",
@ -170,6 +171,9 @@ async fn oidc_callback(
State(state): State<Arc<AppState>>,
Query(params): Query<OidcCallbackQuery>,
) -> Result<Json<LoginResponse>, StatusCode> {
tracing::info!("OIDC callback called with params: code={:?}, state={:?}, error={:?}",
params.code, params.state, params.error);
if let Some(error) = params.error {
tracing::error!("OIDC callback error: {}", error);
return Err(StatusCode::UNAUTHORIZED);
@ -201,9 +205,15 @@ async fn oidc_callback(
})?;
// Find or create user in database
let user = match state.db.get_user_by_oidc_subject(&user_info.sub, &state.config.oidc_issuer_url.as_ref().unwrap()).await {
Ok(Some(existing_user)) => existing_user,
let issuer_url = state.config.oidc_issuer_url.as_ref().unwrap();
tracing::debug!("Looking up user by OIDC subject: {} and issuer: {}", user_info.sub, issuer_url);
let user = match state.db.get_user_by_oidc_subject(&user_info.sub, issuer_url).await {
Ok(Some(existing_user)) => {
tracing::debug!("Found existing OIDC user: {}", existing_user.username);
existing_user
},
Ok(None) => {
tracing::debug!("Creating new OIDC user");
// Create new user
let username = user_info.preferred_username
.or_else(|| user_info.email.clone())
@ -211,6 +221,8 @@ async fn oidc_callback(
let email = user_info.email.unwrap_or_else(|| format!("{}@oidc.local", username));
tracing::debug!("New user details - username: {}, email: {}", username, email);
let create_user = CreateUser {
username,
email: email.clone(),
@ -218,15 +230,23 @@ async fn oidc_callback(
role: Some(UserRole::User),
};
state.db.create_oidc_user(
let result = state.db.create_oidc_user(
create_user,
&user_info.sub,
&state.config.oidc_issuer_url.as_ref().unwrap(),
issuer_url,
&email,
).await.map_err(|e| {
tracing::error!("Failed to create OIDC user: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?
).await;
match result {
Ok(user) => {
tracing::info!("Successfully created OIDC user: {}", user.username);
user
},
Err(e) => {
tracing::error!("Failed to create OIDC user: {} (full error: {:#})", e, e);
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
}
}
Err(e) => {
tracing::error!("Database error during OIDC lookup: {}", e);
@ -236,7 +256,10 @@ async fn oidc_callback(
// Create JWT token
let token = create_jwt(&user, &state.config.jwt_secret)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
.map_err(|e| {
tracing::error!("Failed to create JWT token: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Json(LoginResponse {
token,

View File

@ -40,11 +40,22 @@ mod tests {
#[test]
fn test_oidc_enabled_from_env() {
// Clean up environment first to ensure test isolation
env::remove_var("OIDC_ENABLED");
env::remove_var("OIDC_CLIENT_ID");
env::remove_var("OIDC_CLIENT_SECRET");
env::remove_var("OIDC_ISSUER_URL");
env::remove_var("OIDC_REDIRECT_URI");
env::remove_var("DATABASE_URL");
env::remove_var("JWT_SECRET");
env::set_var("OIDC_ENABLED", "true");
env::set_var("OIDC_CLIENT_ID", "test-client-id");
env::set_var("OIDC_CLIENT_SECRET", "test-client-secret");
env::set_var("OIDC_ISSUER_URL", "https://provider.example.com");
env::set_var("OIDC_REDIRECT_URI", "http://localhost:8000/auth/oidc/callback");
env::set_var("DATABASE_URL", "postgresql://test:test@localhost/test");
env::set_var("JWT_SECRET", "test-secret");
let config = Config::from_env().unwrap();
@ -60,6 +71,8 @@ mod tests {
env::remove_var("OIDC_CLIENT_SECRET");
env::remove_var("OIDC_ISSUER_URL");
env::remove_var("OIDC_REDIRECT_URI");
env::remove_var("DATABASE_URL");
env::remove_var("JWT_SECRET");
}
#[test]
@ -83,6 +96,15 @@ mod tests {
];
for (value, expected) in test_cases {
// Clean up environment first for each iteration
env::remove_var("OIDC_ENABLED");
env::remove_var("OIDC_CLIENT_ID");
env::remove_var("OIDC_CLIENT_SECRET");
env::remove_var("OIDC_ISSUER_URL");
env::remove_var("OIDC_REDIRECT_URI");
env::remove_var("DATABASE_URL");
env::remove_var("JWT_SECRET");
env::set_var("OIDC_ENABLED", value);
env::set_var("DATABASE_URL", "postgresql://test:test@localhost/test");
env::set_var("JWT_SECRET", "test-secret");

View File

@ -1,16 +1,76 @@
#[cfg(test)]
mod tests {
use crate::models::{AuthProvider, CreateUser, UserRole};
use super::super::helpers::{create_test_app};
use axum::http::StatusCode;
use serde_json::json;
use tower::util::ServiceExt;
use wiremock::{matchers::{method, path, query_param}, Mock, MockServer, ResponseTemplate};
use wiremock::{matchers::{method, path, query_param, header}, Mock, MockServer, ResponseTemplate};
use std::sync::Arc;
use crate::{AppState, oidc::OidcClient};
async fn create_test_app_with_oidc() -> (axum::Router, testcontainers::ContainerAsync<testcontainers_modules::postgres::Postgres>, MockServer) {
let (mut app, container) = create_test_app().await;
async fn create_test_app_simple() -> (axum::Router, ()) {
// Use TEST_DATABASE_URL directly, no containers
let database_url = std::env::var("TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| "postgresql://readur:readur@localhost:5432/readur".to_string());
let config = crate::config::Config {
database_url: database_url.clone(),
server_address: "127.0.0.1:0".to_string(),
jwt_secret: "test-secret".to_string(),
upload_path: "./test-uploads".to_string(),
watch_folder: "./test-watch".to_string(),
allowed_file_types: vec!["pdf".to_string()],
watch_interval_seconds: Some(30),
file_stability_check_ms: Some(500),
max_file_age_hours: None,
ocr_language: "eng".to_string(),
concurrent_ocr_jobs: 2,
ocr_timeout_seconds: 60,
max_file_size_mb: 10,
memory_limit_mb: 256,
cpu_priority: "normal".to_string(),
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
};
let db = crate::db::Database::new(&config.database_url).await.unwrap();
// Retry migration up to 3 times to handle concurrent test execution
for attempt in 1..=3 {
match db.migrate().await {
Ok(_) => break,
Err(e) if attempt < 3 && e.to_string().contains("tuple concurrently updated") => {
// Wait a bit and retry
tokio::time::sleep(tokio::time::Duration::from_millis(100 * attempt)).await;
continue;
}
Err(e) => panic!("Migration failed after {} attempts: {}", attempt, e),
}
}
let app = axum::Router::new()
.nest("/api/auth", crate::routes::auth::router())
.with_state(Arc::new(AppState {
db: db.clone(),
config,
webdav_scheduler: None,
source_scheduler: None,
queue_service: Arc::new(crate::ocr_queue::OcrQueueService::new(
db.clone(),
db.pool.clone(),
2
)),
oidc_client: None,
}));
(app, ())
}
async fn create_test_app_with_oidc() -> (axum::Router, MockServer) {
let mock_server = MockServer::start().await;
// Mock OIDC discovery endpoint
@ -27,9 +87,14 @@ mod tests {
.mount(&mock_server)
.await;
// Use TEST_DATABASE_URL directly, no containers
let database_url = std::env::var("TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| "postgresql://readur:readur@localhost:5432/readur".to_string());
// Update the app state to include OIDC client
let config = crate::config::Config {
database_url: "postgresql://test:test@localhost/test".to_string(),
database_url: database_url.clone(),
server_address: "127.0.0.1:0".to_string(),
jwt_secret: "test-secret".to_string(),
upload_path: "./test-uploads".to_string(),
@ -51,34 +116,51 @@ mod tests {
oidc_redirect_uri: Some("http://localhost:8000/auth/oidc/callback".to_string()),
};
let oidc_client = OidcClient::new(&config).await.ok().map(Arc::new);
let oidc_client = match OidcClient::new(&config).await {
Ok(client) => Some(Arc::new(client)),
Err(e) => {
panic!("OIDC client creation failed: {}", e);
}
};
// We need to extract the state from the existing app and recreate it
// This is a bit hacky, but necessary for testing
app = axum::Router::new()
// Connect to the database and run migrations with retry logic for concurrency
let db = crate::db::Database::new(&config.database_url).await.unwrap();
// Retry migration up to 3 times to handle concurrent test execution
for attempt in 1..=3 {
match db.migrate().await {
Ok(_) => break,
Err(e) if attempt < 3 && e.to_string().contains("tuple concurrently updated") => {
// Wait a bit and retry
tokio::time::sleep(tokio::time::Duration::from_millis(100 * attempt)).await;
continue;
}
Err(e) => panic!("Migration failed after {} attempts: {}", attempt, e),
}
}
// Create app with OIDC configuration
let app = axum::Router::new()
.nest("/api/auth", crate::routes::auth::router())
.with_state(Arc::new(AppState {
db: crate::db::Database::new(&format!("postgresql://test:test@localhost:{}/test",
container.get_host_port_ipv4(5432).await.unwrap())).await.unwrap(),
db: db.clone(),
config,
webdav_scheduler: None,
source_scheduler: None,
queue_service: Arc::new(crate::ocr_queue::OcrQueueService::new(
crate::db::Database::new(&format!("postgresql://test:test@localhost:{}/test",
container.get_host_port_ipv4(5432).await.unwrap())).await.unwrap(),
sqlx::PgPool::connect(&format!("postgresql://test:test@localhost:{}/test",
container.get_host_port_ipv4(5432).await.unwrap())).await.unwrap(),
db.clone(),
db.pool.clone(),
2
)),
oidc_client,
}));
(app, container, mock_server)
(app, mock_server)
}
#[tokio::test]
async fn test_oidc_login_redirect() {
let (app, _container, _mock_server) = create_test_app_with_oidc().await;
let (app, _mock_server) = create_test_app_with_oidc().await;
let response = app
.oneshot(
@ -101,7 +183,7 @@ mod tests {
#[tokio::test]
async fn test_oidc_login_disabled() {
let (app, _container) = create_test_app().await; // Regular app without OIDC
let (app, _container) = create_test_app_simple().await; // Regular app without OIDC
let response = app
.oneshot(
@ -119,7 +201,7 @@ mod tests {
#[tokio::test]
async fn test_oidc_callback_missing_code() {
let (app, _container, _mock_server) = create_test_app_with_oidc().await;
let (app, _mock_server) = create_test_app_with_oidc().await;
let response = app
.oneshot(
@ -137,7 +219,7 @@ mod tests {
#[tokio::test]
async fn test_oidc_callback_with_error() {
let (app, _container, _mock_server) = create_test_app_with_oidc().await;
let (app, _mock_server) = create_test_app_with_oidc().await;
let response = app
.oneshot(
@ -155,7 +237,21 @@ mod tests {
#[tokio::test]
async fn test_oidc_callback_success_new_user() {
let (app, _container, mock_server) = create_test_app_with_oidc().await;
let (app, mock_server) = create_test_app_with_oidc().await;
// Clean up any existing test user to ensure test isolation
let database_url = std::env::var("TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| "postgresql://readur:readur@localhost:5432/readur".to_string());
let db = crate::db::Database::new(&database_url).await.unwrap();
// Delete any existing user with the test username or OIDC subject
let _ = sqlx::query("DELETE FROM users WHERE username = $1 OR oidc_subject = $2")
.bind("oidcuser")
.bind("oidc-user-123")
.execute(&db.pool)
.await;
// Mock token exchange
let token_response = json!({
@ -166,7 +262,10 @@ mod tests {
Mock::given(method("POST"))
.and(path("/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(token_response))
.and(header("content-type", "application/x-www-form-urlencoded"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(token_response)
.insert_header("content-type", "application/json"))
.mount(&mock_server)
.await;
@ -180,10 +279,15 @@ mod tests {
Mock::given(method("GET"))
.and(path("/userinfo"))
.respond_with(ResponseTemplate::new(200).set_body_json(user_info_response))
.respond_with(ResponseTemplate::new(200)
.set_body_json(user_info_response)
.insert_header("content-type", "application/json"))
.mount(&mock_server)
.await;
// Add a small delay to make sure everything is set up
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let response = app
.oneshot(
axum::http::Request::builder()
@ -195,11 +299,31 @@ mod tests {
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
if status != StatusCode::OK {
let error_text = String::from_utf8_lossy(&body);
eprintln!("Response status: {}", status);
eprintln!("Response body: {}", error_text);
// Also check if we made the expected API calls to the mock server
eprintln!("Mock server received calls:");
let received_requests = mock_server.received_requests().await.unwrap();
for req in received_requests {
eprintln!(" {} {} - {}", req.method, req.url.path(), String::from_utf8_lossy(&req.body));
}
// Try to parse as JSON to see if there's a more detailed error message
if let Ok(error_json) = serde_json::from_slice::<serde_json::Value>(&body) {
eprintln!("Error JSON: {:#}", error_json);
}
}
assert_eq!(status, StatusCode::OK);
let login_response: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(login_response["token"].is_string());
@ -209,7 +333,7 @@ mod tests {
#[tokio::test]
async fn test_oidc_callback_invalid_token() {
let (app, _container, mock_server) = create_test_app_with_oidc().await;
let (app, mock_server) = create_test_app_with_oidc().await;
// Mock failed token exchange
Mock::given(method("POST"))
@ -236,7 +360,18 @@ mod tests {
#[tokio::test]
async fn test_oidc_callback_invalid_user_info() {
let (app, _container, mock_server) = create_test_app_with_oidc().await;
let (app, mock_server) = create_test_app_with_oidc().await;
// Clean up any existing test user to ensure test isolation
let database_url = std::env::var("TEST_DATABASE_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
.unwrap_or_else(|_| "postgresql://readur:readur@localhost:5432/readur".to_string());
let db = crate::db::Database::new(&database_url).await.unwrap();
// Delete any existing user that might conflict
let _ = sqlx::query("DELETE FROM users WHERE username LIKE 'oidc%' OR oidc_subject IS NOT NULL")
.execute(&db.pool)
.await;
// Mock successful token exchange
let token_response = json!({

View File

@ -47,6 +47,11 @@ async fn create_test_app_state() -> Arc<AppState> {
max_file_size_mb: 50,
memory_limit_mb: 512,
cpu_priority: "normal".to_string(),
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
};
let db = Database::new(&config.database_url).await.unwrap();
@ -62,6 +67,7 @@ async fn create_test_app_state() -> Arc<AppState> {
webdav_scheduler: None,
source_scheduler: None,
queue_service,
oidc_client: None,
})
}

View File

@ -76,6 +76,11 @@ async fn create_test_app_state() -> Result<Arc<AppState>> {
max_file_size_mb: 10,
memory_limit_mb: 256,
cpu_priority: "normal".to_string(),
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
}
});
let db = Database::new(&config.database_url).await?;
@ -89,6 +94,7 @@ async fn create_test_app_state() -> Result<Arc<AppState>> {
webdav_scheduler: None,
source_scheduler: None,
queue_service,
oidc_client: None,
}))
}

View File

@ -31,6 +31,11 @@ async fn create_test_app_state() -> Result<Arc<AppState>> {
max_file_size_mb: 10,
memory_limit_mb: 256,
cpu_priority: "normal".to_string(),
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
}
});
@ -43,6 +48,7 @@ async fn create_test_app_state() -> Result<Arc<AppState>> {
webdav_scheduler: None,
source_scheduler: None,
queue_service,
oidc_client: None,
}))
}

View File

@ -39,6 +39,11 @@ async fn create_test_app_state() -> Arc<AppState> {
max_file_size_mb: 100,
memory_limit_mb: 512,
cpu_priority: "normal".to_string(),
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
};
let db = Database::new(&config.database_url).await.unwrap();
@ -50,6 +55,7 @@ async fn create_test_app_state() -> Arc<AppState> {
webdav_scheduler: None,
source_scheduler: None,
queue_service,
oidc_client: None,
})
}

View File

@ -176,6 +176,11 @@ async fn create_test_app_state() -> Arc<AppState> {
max_file_size_mb: 10,
memory_limit_mb: 256,
cpu_priority: "normal".to_string(),
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
};
let db = Database::new(&config.database_url).await.unwrap();
@ -187,6 +192,7 @@ async fn create_test_app_state() -> Arc<AppState> {
webdav_scheduler: None,
source_scheduler: None,
queue_service,
oidc_client: None,
})
}

View File

@ -113,6 +113,11 @@ async fn create_test_app_state() -> Result<Arc<AppState>> {
max_file_size_mb: 10,
memory_limit_mb: 256,
cpu_priority: "normal".to_string(),
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
}
});
let db = Database::new(&config.database_url).await?;
@ -126,6 +131,7 @@ async fn create_test_app_state() -> Result<Arc<AppState>> {
webdav_scheduler: None,
source_scheduler: None,
queue_service,
oidc_client: None,
}))
}

View File

@ -340,6 +340,11 @@ fn test_webdav_scheduler_creation() {
max_file_size_mb: 50,
ocr_language: "eng".to_string(),
ocr_timeout_seconds: 300,
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
};
// Note: This is a minimal test since we can't easily mock the database

View File

@ -93,6 +93,11 @@ async fn setup_test_app() -> (Router, Arc<AppState>) {
max_file_size_mb: 50,
ocr_language: "eng".to_string(),
ocr_timeout_seconds: 300,
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
};
// Use the environment-based database URL
@ -106,6 +111,7 @@ async fn setup_test_app() -> (Router, Arc<AppState>) {
webdav_scheduler: None,
source_scheduler: None,
queue_service,
oidc_client: None,
});
let app = Router::new()