refactor(tests): use in-process server and shared AppState factory

Replace CLI test child process with in-process server on a background
thread, eliminating orphan processes after test runs. Simplify server
test helpers to use AppState::from_database() instead of manual repo
construction.
This commit is contained in:
Jon Seager 2026-02-06 13:48:15 +00:00
parent 9a46bb3613
commit f8ffb37110
No known key found for this signature in database
2 changed files with 130 additions and 307 deletions

View file

@ -1,30 +1,51 @@
use once_cell::sync::Lazy; use std::process::Command;
use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex};
use std::sync::Mutex;
use std::time::Duration; use std::time::Duration;
use tempfile::TempDir;
/// Shared test server state use once_cell::sync::Lazy;
/// Fields prefixed with `_` are kept alive to prevent cleanup: use tempfile::TempDir;
/// - `_temp_dir`: keeps temporary database file from being deleted use tokio::net::TcpListener;
/// - `_process`: keeps server process running use webauthn_rs::prelude::*;
use brewlog::application::routes::app_router;
use brewlog::application::state::{AppState, AppStateConfig};
use brewlog::infrastructure::database::Database;
/// Shared test server state.
/// `_temp_dir` is kept alive to prevent the temporary database file from being deleted.
/// The server itself runs on a background thread and dies when the test binary exits.
struct SharedServer { struct SharedServer {
address: String, address: String,
db_url: String, db_url: String,
_temp_dir: TempDir, _temp_dir: TempDir,
_process: std::process::Child,
} }
/// Single shared test server for all CLI tests /// Single shared test server for all CLI tests
static TEST_SERVER: Lazy<Mutex<Option<SharedServer>>> = Lazy::new(|| Mutex::new(None)); static TEST_SERVER: Lazy<Mutex<Option<SharedServer>>> = Lazy::new(|| Mutex::new(None));
#[allow(clippy::expect_used)]
fn test_webauthn() -> Arc<Webauthn> {
let rp_origin = url::Url::parse("http://localhost:0").expect("valid URL");
Arc::new(
WebauthnBuilder::new("localhost", &rp_origin)
.expect("valid RP config")
.rp_name("Brewlog Test")
.build()
.expect("valid WebAuthn"),
)
}
/// Get path to the brewlog binary /// Get path to the brewlog binary
pub fn brewlog_bin() -> String { pub fn brewlog_bin() -> String {
// Cargo sets this for integration tests - works for both debug and release // Cargo sets this for integration tests - works for both debug and release
env!("CARGO_BIN_EXE_brewlog").to_string() env!("CARGO_BIN_EXE_brewlog").to_string()
} }
/// Get or start the shared test server - handles mutex poisoning gracefully /// Get or start the shared test server - handles mutex poisoning gracefully.
///
/// Spawns the server in-process on a background thread (not as a child process),
/// so the server dies automatically when the test binary exits.
#[allow(clippy::too_many_lines)]
fn ensure_server_started() -> Result<(String, String), String> { fn ensure_server_started() -> Result<(String, String), String> {
let mut server = match TEST_SERVER.lock() { let mut server = match TEST_SERVER.lock() {
Ok(guard) => guard, Ok(guard) => guard,
@ -34,74 +55,79 @@ fn ensure_server_started() -> Result<(String, String), String> {
if server.is_none() { if server.is_none() {
eprintln!("Starting test server..."); eprintln!("Starting test server...");
// Create temporary database // Create temporary database (file-based so create_token() can connect independently)
let temp_dir = TempDir::new().map_err(|e| format!("Failed to create temp dir: {}", e))?; let temp_dir = TempDir::new().map_err(|e| format!("Failed to create temp dir: {e}"))?;
let db_path = temp_dir.path().join("test.db"); let db_path = temp_dir.path().join("test.db");
let db_url = format!("sqlite:{}", db_path.display()); let db_url = format!("sqlite:{}", db_path.display());
// Start server on a random port // Pick a random port
let port = portpicker::pick_unused_port().ok_or("No ports available")?; let port = portpicker::pick_unused_port().ok_or("No ports available")?;
let address = format!("http://localhost:{}", port); let address = format!("http://127.0.0.1:{port}");
let bind_address = format!("127.0.0.1:{port}");
eprintln!("Starting server on {}", address); eprintln!("Starting server on {address}");
let bind_address = format!("127.0.0.1:{}", port); let db_url_for_thread = db_url.clone();
let process = Command::new(brewlog_bin()) let (tx, rx) = std::sync::mpsc::sync_channel::<()>(1);
.args(&[
"serve", // Start server on a background thread with its own tokio runtime.
"--bind-address", // The thread (and server) live until the test binary exits — no cleanup needed.
&bind_address, std::thread::spawn(move || {
"--database-url", #[allow(clippy::expect_used)]
&db_url, let rt = tokio::runtime::Builder::new_current_thread()
]) .enable_all()
.env("BREWLOG_RP_ID", "localhost") .build()
.env("BREWLOG_RP_ORIGIN", &address) .expect("Failed to create tokio runtime");
.env("BREWLOG_OPENROUTER_API_KEY", "test-key")
.env("BREWLOG_FOURSQUARE_API_KEY", "test-key") rt.block_on(async move {
.env("RUST_LOG", "error") #[allow(clippy::expect_used)]
.stdout(Stdio::null()) let database = Database::connect(&db_url_for_thread)
.stderr(Stdio::null()) .await
.spawn() .expect("Failed to connect to test database");
.map_err(|e| format!("Failed to start brewlog server: {}", e))?;
let state = AppState::from_database(
&database,
AppStateConfig {
webauthn: test_webauthn(),
foursquare_url: brewlog::infrastructure::foursquare::FOURSQUARE_SEARCH_URL
.to_string(),
foursquare_api_key: String::new(),
openrouter_url: brewlog::infrastructure::ai::OPENROUTER_URL.to_string(),
openrouter_api_key: String::new(),
openrouter_model: "openrouter/free".to_string(),
},
);
let app = app_router(state);
#[allow(clippy::expect_used)]
let listener = TcpListener::bind(&bind_address)
.await
.expect("Failed to bind to port");
// Signal readiness to the main thread
let _ = tx.send(());
// Run server until the process exits
#[allow(clippy::expect_used)]
axum::serve(listener, app).await.expect("Server failed");
});
});
// Wait for server to be ready // Wait for server to be ready
let client = reqwest::blocking::Client::builder() rx.recv_timeout(Duration::from_secs(30))
.timeout(Duration::from_secs(3)) .map_err(|e| format!("Server failed to start within timeout: {e}"))?;
.build()
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
let health_url = format!("{}/api/v1/roasters", address);
let mut server_ready = false; eprintln!("Server ready on {address}");
for attempt in 0..100 {
match client.get(&health_url).send() {
Ok(_) => {
eprintln!("Server ready after {} attempts", attempt + 1);
server_ready = true;
break;
}
Err(_) => {
if attempt == 99 {
return Err(
"Server failed to start after 100 attempts (10 seconds)".to_string()
);
}
std::thread::sleep(Duration::from_millis(100));
}
}
}
if !server_ready {
return Err("Server never became ready".to_string());
}
*server = Some(SharedServer { *server = Some(SharedServer {
address: address.clone(), address: address.clone(),
db_url: db_url.clone(), db_url: db_url.clone(),
_temp_dir: temp_dir, _temp_dir: temp_dir,
_process: process,
}); });
} }
#[allow(clippy::unwrap_used)]
let srv = server.as_ref().unwrap(); let srv = server.as_ref().unwrap();
Ok((srv.address.clone(), srv.db_url.clone())) Ok((srv.address.clone(), srv.db_url.clone()))
} }
@ -115,18 +141,21 @@ pub fn server_info() -> (String, String) {
pub fn create_token(name: &str) -> String { pub fn create_token(name: &str) -> String {
let (_, db_url) = ensure_server_started().expect("Failed to start test server"); let (_, db_url) = ensure_server_started().expect("Failed to start test server");
#[allow(clippy::expect_used)]
let rt = tokio::runtime::Builder::new_current_thread() let rt = tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build() .build()
.expect("Failed to create tokio runtime"); .expect("Failed to create tokio runtime");
rt.block_on(async { rt.block_on(async {
#[allow(clippy::expect_used)]
let pool = sqlx::SqlitePool::connect(&db_url) let pool = sqlx::SqlitePool::connect(&db_url)
.await .await
.expect("Failed to connect to test database"); .expect("Failed to connect to test database");
// Ensure a user exists (INSERT OR IGNORE to handle concurrent test threads) // Ensure a user exists (INSERT OR IGNORE to handle concurrent test threads)
let uuid = uuid::Uuid::new_v4().to_string(); let uuid = uuid::Uuid::new_v4().to_string();
#[allow(clippy::expect_used)]
sqlx::query( sqlx::query(
"INSERT OR IGNORE INTO users (username, uuid, created_at) VALUES (?, ?, datetime('now'))", "INSERT OR IGNORE INTO users (username, uuid, created_at) VALUES (?, ?, datetime('now'))",
) )
@ -136,6 +165,7 @@ pub fn create_token(name: &str) -> String {
.await .await
.expect("Failed to ensure test user"); .expect("Failed to ensure test user");
#[allow(clippy::expect_used)]
let user_id: i64 = let user_id: i64 =
sqlx::query_scalar::<_, i64>("SELECT id FROM users WHERE username = ?") sqlx::query_scalar::<_, i64>("SELECT id FROM users WHERE username = ?")
.bind("admin") .bind("admin")
@ -144,10 +174,12 @@ pub fn create_token(name: &str) -> String {
.expect("Failed to query test user"); .expect("Failed to query test user");
// Generate and insert a bearer token // Generate and insert a bearer token
#[allow(clippy::expect_used)]
let token_value = let token_value =
brewlog::infrastructure::auth::generate_token().expect("Failed to generate token"); brewlog::infrastructure::auth::generate_token().expect("Failed to generate token");
let token_hash = brewlog::infrastructure::auth::hash_token(&token_value); let token_hash = brewlog::infrastructure::auth::hash_token(&token_value);
#[allow(clippy::expect_used)]
sqlx::query( sqlx::query(
"INSERT INTO tokens (user_id, token_hash, name, created_at) VALUES (?, ?, ?, datetime('now'))", "INSERT INTO tokens (user_id, token_hash, name, created_at) VALUES (?, ?, ?, datetime('now'))",
) )

View file

@ -1,34 +1,15 @@
use std::sync::Arc; use std::sync::Arc;
use brewlog::application::routes::app_router; use brewlog::application::routes::app_router;
use brewlog::application::server::AppState; use brewlog::application::state::{AppState, AppStateConfig};
use brewlog::application::services::{
BagService, BrewService, CafeService, CupService, GearService, RoastService, RoasterService,
};
use brewlog::domain::cafes::{Cafe, NewCafe}; use brewlog::domain::cafes::{Cafe, NewCafe};
use brewlog::domain::repositories::{ use brewlog::domain::repositories::{
BagRepository, BrewRepository, CafeRepository, CupRepository, GearRepository, CafeRepository, RoastRepository, RoasterRepository, SessionRepository, TimelineEventRepository,
PasskeyCredentialRepository, RegistrationTokenRepository, RoastRepository, RoasterRepository, TokenRepository, UserRepository,
SessionRepository, TimelineEventRepository, TokenRepository, UserRepository,
}; };
use brewlog::domain::roasters::{NewRoaster, Roaster}; use brewlog::domain::roasters::{NewRoaster, Roaster};
use brewlog::domain::users::NewUser; use brewlog::domain::users::NewUser;
use brewlog::infrastructure::backup::BackupService;
use brewlog::infrastructure::database::Database; use brewlog::infrastructure::database::Database;
use brewlog::infrastructure::repositories::bags::SqlBagRepository;
use brewlog::infrastructure::repositories::brews::SqlBrewRepository;
use brewlog::infrastructure::repositories::cafes::SqlCafeRepository;
use brewlog::infrastructure::repositories::cups::SqlCupRepository;
use brewlog::infrastructure::repositories::gear::SqlGearRepository;
use brewlog::infrastructure::repositories::passkey_credentials::SqlPasskeyCredentialRepository;
use brewlog::infrastructure::repositories::registration_tokens::SqlRegistrationTokenRepository;
use brewlog::infrastructure::repositories::roasters::SqlRoasterRepository;
use brewlog::infrastructure::repositories::roasts::SqlRoastRepository;
use brewlog::infrastructure::repositories::sessions::SqlSessionRepository;
use brewlog::infrastructure::repositories::timeline_events::SqlTimelineEventRepository;
use brewlog::infrastructure::repositories::tokens::SqlTokenRepository;
use brewlog::infrastructure::repositories::users::SqlUserRepository;
use brewlog::infrastructure::webauthn::ChallengeStore;
use reqwest::Client; use reqwest::Client;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::task::AbortHandle; use tokio::task::AbortHandle;
@ -83,160 +64,42 @@ fn test_webauthn() -> Arc<Webauthn> {
} }
pub async fn spawn_app() -> TestApp { pub async fn spawn_app() -> TestApp {
// Use in-memory SQLite database for testing
let database = Database::connect("sqlite::memory:") let database = Database::connect("sqlite::memory:")
.await .await
.expect("Failed to connect to in-memory database"); .expect("Failed to connect to in-memory database");
// Run migrations spawn_app_inner(database, test_state_config(), None).await
database
.migrate()
.await
.expect("Failed to migrate database");
// Create repositories
let roaster_repo = Arc::new(SqlRoasterRepository::new(database.clone_pool()));
let roast_repo = Arc::new(SqlRoastRepository::new(database.clone_pool()));
let bag_repo = Arc::new(SqlBagRepository::new(database.clone_pool()));
let gear_repo = Arc::new(SqlGearRepository::new(database.clone_pool()));
let brew_repo = Arc::new(SqlBrewRepository::new(database.clone_pool()));
let cafe_repo = Arc::new(SqlCafeRepository::new(database.clone_pool()));
let cup_repo = Arc::new(SqlCupRepository::new(database.clone_pool()));
let timeline_repo = Arc::new(SqlTimelineEventRepository::new(database.clone_pool()));
let user_repo: Arc<dyn UserRepository> =
Arc::new(SqlUserRepository::new(database.clone_pool()));
let token_repo: Arc<dyn TokenRepository> =
Arc::new(SqlTokenRepository::new(database.clone_pool()));
let session_repo: Arc<dyn SessionRepository> =
Arc::new(SqlSessionRepository::new(database.clone_pool()));
let passkey_repo: Arc<dyn PasskeyCredentialRepository> =
Arc::new(SqlPasskeyCredentialRepository::new(database.clone_pool()));
let registration_token_repo: Arc<dyn RegistrationTokenRepository> =
Arc::new(SqlRegistrationTokenRepository::new(database.clone_pool()));
spawn_app_inner(
database,
roaster_repo,
roast_repo,
bag_repo,
gear_repo,
brew_repo,
cafe_repo,
cup_repo,
timeline_repo,
user_repo,
token_repo,
session_repo,
passkey_repo,
registration_token_repo,
brewlog::infrastructure::foursquare::FOURSQUARE_SEARCH_URL.to_string(),
String::new(),
brewlog::infrastructure::ai::OPENROUTER_URL.to_string(),
None,
)
.await
} }
#[allow(clippy::too_many_arguments)] fn test_state_config() -> AppStateConfig {
async fn spawn_app_inner( AppStateConfig {
_database: Database,
roaster_repo: Arc<SqlRoasterRepository>,
roast_repo: Arc<SqlRoastRepository>,
bag_repo: Arc<SqlBagRepository>,
gear_repo: Arc<SqlGearRepository>,
brew_repo: Arc<SqlBrewRepository>,
cafe_repo: Arc<SqlCafeRepository>,
cup_repo: Arc<SqlCupRepository>,
timeline_repo: Arc<SqlTimelineEventRepository>,
user_repo: Arc<dyn UserRepository>,
token_repo: Arc<dyn TokenRepository>,
session_repo: Arc<dyn SessionRepository>,
passkey_repo: Arc<dyn PasskeyCredentialRepository>,
registration_token_repo: Arc<dyn RegistrationTokenRepository>,
foursquare_url: String,
foursquare_api_key: String,
openrouter_url: String,
mock_server: Option<wiremock::MockServer>,
) -> TestApp {
let backup_service = Arc::new(BackupService::new(_database.clone_pool()));
let session_repo_clone: Arc<dyn SessionRepository> = session_repo.clone();
// Create services
let roaster_service = RoasterService::new(
roaster_repo.clone() as Arc<dyn RoasterRepository>,
timeline_repo.clone() as Arc<dyn TimelineEventRepository>,
);
let roast_service = RoastService::new(
roast_repo.clone() as Arc<dyn RoastRepository>,
roaster_repo.clone() as Arc<dyn RoasterRepository>,
timeline_repo.clone() as Arc<dyn TimelineEventRepository>,
);
let bag_service = BagService::new(
bag_repo.clone() as Arc<dyn BagRepository>,
roast_repo.clone() as Arc<dyn RoastRepository>,
roaster_repo.clone() as Arc<dyn RoasterRepository>,
timeline_repo.clone() as Arc<dyn TimelineEventRepository>,
);
let brew_service = BrewService::new(
brew_repo.clone() as Arc<dyn BrewRepository>,
timeline_repo.clone() as Arc<dyn TimelineEventRepository>,
);
let gear_service = GearService::new(
gear_repo.clone() as Arc<dyn GearRepository>,
timeline_repo.clone() as Arc<dyn TimelineEventRepository>,
);
let cafe_service = CafeService::new(
cafe_repo.clone() as Arc<dyn CafeRepository>,
timeline_repo.clone() as Arc<dyn TimelineEventRepository>,
);
let cup_service = CupService::new(
cup_repo.clone() as Arc<dyn CupRepository>,
timeline_repo.clone() as Arc<dyn TimelineEventRepository>,
);
// Create application state
let state = AppState {
roaster_repo: roaster_repo.clone(),
roast_repo: roast_repo.clone(),
bag_repo: bag_repo.clone(),
gear_repo: gear_repo.clone(),
brew_repo: brew_repo.clone(),
cafe_repo: cafe_repo.clone(),
cup_repo: cup_repo.clone(),
timeline_repo: timeline_repo.clone(),
user_repo: user_repo.clone(),
token_repo: token_repo.clone(),
session_repo,
passkey_repo,
registration_token_repo,
ai_usage_repo: Arc::new(
brewlog::infrastructure::repositories::ai_usage::SqlAiUsageRepository::new(
_database.clone_pool(),
),
),
webauthn: test_webauthn(), webauthn: test_webauthn(),
challenge_store: Arc::new(ChallengeStore::new()), foursquare_url: brewlog::infrastructure::foursquare::FOURSQUARE_SEARCH_URL.to_string(),
http_client: reqwest::Client::new(), foursquare_api_key: String::new(),
foursquare_url, openrouter_url: brewlog::infrastructure::ai::OPENROUTER_URL.to_string(),
foursquare_api_key,
openrouter_url,
openrouter_api_key: String::new(), openrouter_api_key: String::new(),
openrouter_model: "openrouter/free".to_string(), openrouter_model: "openrouter/free".to_string(),
backup_service, }
roaster_service, }
roast_service,
bag_service, async fn spawn_app_inner(
brew_service, database: Database,
gear_service, config: AppStateConfig,
cafe_service, mock_server: Option<wiremock::MockServer>,
cup_service, ) -> TestApp {
}; let state = AppState::from_database(&database, config);
// Clone repos we need for TestApp before consuming state in the router
let roaster_repo = state.roaster_repo.clone();
let roast_repo = state.roast_repo.clone();
let cafe_repo = state.cafe_repo.clone();
let timeline_repo = state.timeline_repo.clone();
let user_repo = state.user_repo.clone();
let token_repo = state.token_repo.clone();
let session_repo = state.session_repo.clone();
// Create router
let app = app_router(state); let app = app_router(state);
// Bind to a random port
let listener = TcpListener::bind("127.0.0.1:0") let listener = TcpListener::bind("127.0.0.1:0")
.await .await
.expect("Failed to bind to random port"); .expect("Failed to bind to random port");
@ -244,7 +107,6 @@ async fn spawn_app_inner(
let local_addr = listener.local_addr().expect("Failed to get local address"); let local_addr = listener.local_addr().expect("Failed to get local address");
let address = format!("http://{}", local_addr); let address = format!("http://{}", local_addr);
// Spawn the server in a background task
let server_handle = tokio::spawn(async move { let server_handle = tokio::spawn(async move {
axum::serve(listener, app) axum::serve(listener, app)
.await .await
@ -260,7 +122,7 @@ async fn spawn_app_inner(
timeline_repo, timeline_repo,
user_repo: Some(user_repo), user_repo: Some(user_repo),
token_repo: Some(token_repo), token_repo: Some(token_repo),
session_repo: Some(session_repo_clone), session_repo: Some(session_repo),
auth_token: None, auth_token: None,
mock_server, mock_server,
server_handle, server_handle,
@ -280,48 +142,13 @@ pub async fn spawn_app_with_foursquare_mock() -> TestApp {
.await .await
.expect("Failed to connect to in-memory database"); .expect("Failed to connect to in-memory database");
database
.migrate()
.await
.expect("Failed to migrate database");
let roaster_repo = Arc::new(SqlRoasterRepository::new(database.clone_pool()));
let roast_repo = Arc::new(SqlRoastRepository::new(database.clone_pool()));
let bag_repo = Arc::new(SqlBagRepository::new(database.clone_pool()));
let gear_repo = Arc::new(SqlGearRepository::new(database.clone_pool()));
let brew_repo = Arc::new(SqlBrewRepository::new(database.clone_pool()));
let cafe_repo = Arc::new(SqlCafeRepository::new(database.clone_pool()));
let cup_repo = Arc::new(SqlCupRepository::new(database.clone_pool()));
let timeline_repo = Arc::new(SqlTimelineEventRepository::new(database.clone_pool()));
let user_repo: Arc<dyn UserRepository> =
Arc::new(SqlUserRepository::new(database.clone_pool()));
let token_repo: Arc<dyn TokenRepository> =
Arc::new(SqlTokenRepository::new(database.clone_pool()));
let session_repo: Arc<dyn SessionRepository> =
Arc::new(SqlSessionRepository::new(database.clone_pool()));
let passkey_repo: Arc<dyn PasskeyCredentialRepository> =
Arc::new(SqlPasskeyCredentialRepository::new(database.clone_pool()));
let registration_token_repo: Arc<dyn RegistrationTokenRepository> =
Arc::new(SqlRegistrationTokenRepository::new(database.clone_pool()));
let app = spawn_app_inner( let app = spawn_app_inner(
database, database,
roaster_repo, AppStateConfig {
roast_repo,
bag_repo,
gear_repo,
brew_repo,
cafe_repo,
cup_repo,
timeline_repo,
user_repo,
token_repo,
session_repo,
passkey_repo,
registration_token_repo,
foursquare_url, foursquare_url,
"test-api-key".to_string(), foursquare_api_key: "test-api-key".to_string(),
brewlog::infrastructure::ai::OPENROUTER_URL.to_string(), ..test_state_config()
},
Some(mock_server), Some(mock_server),
) )
.await; .await;
@ -628,48 +455,12 @@ pub async fn spawn_app_with_openrouter_mock() -> TestApp {
.await .await
.expect("Failed to connect to in-memory database"); .expect("Failed to connect to in-memory database");
database
.migrate()
.await
.expect("Failed to migrate database");
let roaster_repo = Arc::new(SqlRoasterRepository::new(database.clone_pool()));
let roast_repo = Arc::new(SqlRoastRepository::new(database.clone_pool()));
let bag_repo = Arc::new(SqlBagRepository::new(database.clone_pool()));
let gear_repo = Arc::new(SqlGearRepository::new(database.clone_pool()));
let brew_repo = Arc::new(SqlBrewRepository::new(database.clone_pool()));
let cafe_repo = Arc::new(SqlCafeRepository::new(database.clone_pool()));
let cup_repo = Arc::new(SqlCupRepository::new(database.clone_pool()));
let timeline_repo = Arc::new(SqlTimelineEventRepository::new(database.clone_pool()));
let user_repo: Arc<dyn UserRepository> =
Arc::new(SqlUserRepository::new(database.clone_pool()));
let token_repo: Arc<dyn TokenRepository> =
Arc::new(SqlTokenRepository::new(database.clone_pool()));
let session_repo: Arc<dyn SessionRepository> =
Arc::new(SqlSessionRepository::new(database.clone_pool()));
let passkey_repo: Arc<dyn PasskeyCredentialRepository> =
Arc::new(SqlPasskeyCredentialRepository::new(database.clone_pool()));
let registration_token_repo: Arc<dyn RegistrationTokenRepository> =
Arc::new(SqlRegistrationTokenRepository::new(database.clone_pool()));
let app = spawn_app_inner( let app = spawn_app_inner(
database, database,
roaster_repo, AppStateConfig {
roast_repo,
bag_repo,
gear_repo,
brew_repo,
cafe_repo,
cup_repo,
timeline_repo,
user_repo,
token_repo,
session_repo,
passkey_repo,
registration_token_repo,
brewlog::infrastructure::foursquare::FOURSQUARE_SEARCH_URL.to_string(),
String::new(),
openrouter_url, openrouter_url,
..test_state_config()
},
Some(mock_server), Some(mock_server),
) )
.await; .await;