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:
parent
9a46bb3613
commit
f8ffb37110
2 changed files with 130 additions and 307 deletions
|
|
@ -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'))",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
foursquare_url,
|
||||||
bag_repo,
|
foursquare_api_key: "test-api-key".to_string(),
|
||||||
gear_repo,
|
..test_state_config()
|
||||||
brew_repo,
|
},
|
||||||
cafe_repo,
|
|
||||||
cup_repo,
|
|
||||||
timeline_repo,
|
|
||||||
user_repo,
|
|
||||||
token_repo,
|
|
||||||
session_repo,
|
|
||||||
passkey_repo,
|
|
||||||
registration_token_repo,
|
|
||||||
foursquare_url,
|
|
||||||
"test-api-key".to_string(),
|
|
||||||
brewlog::infrastructure::ai::OPENROUTER_URL.to_string(),
|
|
||||||
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,
|
openrouter_url,
|
||||||
bag_repo,
|
..test_state_config()
|
||||||
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,
|
|
||||||
Some(mock_server),
|
Some(mock_server),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue