refactor: improve test harness and browser automation
- Update harness.rs, main.rs, ports.rs - Add chromedriver service module - Update browser automation in web/browser.rs - Update e2e test module
This commit is contained in:
parent
8fdd3b7be8
commit
9f7844580d
10 changed files with 705 additions and 73 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
tmp
|
||||||
.tmp*
|
.tmp*
|
||||||
.tmp/*
|
.tmp/*
|
||||||
*.log
|
*.log
|
||||||
|
|
|
||||||
112
Cargo.lock
generated
112
Cargo.lock
generated
|
|
@ -1038,9 +1038,9 @@ dependencies = [
|
||||||
"rhai",
|
"rhai",
|
||||||
"ring",
|
"ring",
|
||||||
"rust_xlsxwriter",
|
"rust_xlsxwriter",
|
||||||
"rustls 0.21.12",
|
"rustls 0.23.35",
|
||||||
"rustls-native-certs 0.6.3",
|
"rustls-native-certs 0.6.3",
|
||||||
"rustls-pemfile 1.0.4",
|
"rustls-pemfile 2.2.0",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -1050,7 +1050,7 @@ dependencies = [
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.24.1",
|
"tokio-rustls 0.26.4",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"toml 0.8.23",
|
"toml 0.8.23",
|
||||||
"tower 0.4.13",
|
"tower 0.4.13",
|
||||||
|
|
@ -1080,6 +1080,7 @@ dependencies = [
|
||||||
"cookie 0.18.1",
|
"cookie 0.18.1",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
|
"dirs",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"fantoccini",
|
"fantoccini",
|
||||||
|
|
@ -1800,6 +1801,27 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs"
|
||||||
|
version = "5.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||||
|
dependencies = [
|
||||||
|
"dirs-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dirs-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"option-ext",
|
||||||
|
"redox_users",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
|
|
@ -3412,6 +3434,12 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "option-ext"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "outref"
|
name = "outref"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
|
@ -3930,6 +3958,17 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_users"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"libredox",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.2"
|
version = "1.12.2"
|
||||||
|
|
@ -4175,6 +4214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
|
|
@ -5680,6 +5720,15 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|
@ -5716,6 +5765,21 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm 0.48.5",
|
||||||
|
"windows_aarch64_msvc 0.48.5",
|
||||||
|
"windows_i686_gnu 0.48.5",
|
||||||
|
"windows_i686_msvc 0.48.5",
|
||||||
|
"windows_x86_64_gnu 0.48.5",
|
||||||
|
"windows_x86_64_gnullvm 0.48.5",
|
||||||
|
"windows_x86_64_msvc 0.48.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|
@ -5749,6 +5813,12 @@ dependencies = [
|
||||||
"windows_x86_64_msvc 0.53.1",
|
"windows_x86_64_msvc 0.53.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|
@ -5761,6 +5831,12 @@ version = "0.53.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|
@ -5773,6 +5849,12 @@ version = "0.53.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|
@ -5797,6 +5879,12 @@ version = "0.53.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|
@ -5809,6 +5897,12 @@ version = "0.53.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|
@ -5821,6 +5915,12 @@ version = "0.53.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|
@ -5833,6 +5933,12 @@ version = "0.53.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.48.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ which = "7"
|
||||||
regex = "1.11"
|
regex = "1.11"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
dirs = "5.0"
|
||||||
|
|
||||||
# Process management for services
|
# Process management for services
|
||||||
nix = { version = "0.29", features = ["signal", "process"] }
|
nix = { version = "0.29", features = ["signal", "process"] }
|
||||||
|
|
|
||||||
238
src/harness.rs
238
src/harness.rs
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::fixtures::{Bot, Customer, Message, QueueEntry, Session, User};
|
use crate::fixtures::{Bot, Customer, Message, QueueEntry, Session, User};
|
||||||
use crate::mocks::{MockLLM, MockZitadel};
|
use crate::mocks::{MockLLM, MockZitadel};
|
||||||
use crate::ports::TestPorts;
|
use crate::ports::{PortAllocator, TestPorts};
|
||||||
use crate::services::{MinioService, PostgresService, RedisService};
|
use crate::services::{MinioService, PostgresService, RedisService};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use diesel::r2d2::{ConnectionManager, Pool};
|
use diesel::r2d2::{ConnectionManager, Pool};
|
||||||
|
|
@ -53,7 +53,7 @@ impl TestConfig {
|
||||||
redis: false, // Botserver will bootstrap its own Redis
|
redis: false, // Botserver will bootstrap its own Redis
|
||||||
mock_zitadel: true,
|
mock_zitadel: true,
|
||||||
mock_llm: true,
|
mock_llm: true,
|
||||||
run_migrations: true,
|
run_migrations: false, // Let botserver run its own migrations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,12 +64,34 @@ impl TestConfig {
|
||||||
..Self::minimal()
|
..Self::minimal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn use_existing_stack() -> Self {
|
||||||
|
Self {
|
||||||
|
postgres: false,
|
||||||
|
minio: false,
|
||||||
|
redis: false,
|
||||||
|
mock_zitadel: true,
|
||||||
|
mock_llm: true,
|
||||||
|
run_migrations: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DefaultPorts;
|
||||||
|
|
||||||
|
impl DefaultPorts {
|
||||||
|
pub const POSTGRES: u16 = 5432;
|
||||||
|
pub const MINIO: u16 = 9000;
|
||||||
|
pub const REDIS: u16 = 6379;
|
||||||
|
pub const ZITADEL: u16 = 8080;
|
||||||
|
pub const BOTSERVER: u16 = 8080;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TestContext {
|
pub struct TestContext {
|
||||||
pub ports: TestPorts,
|
pub ports: TestPorts,
|
||||||
pub config: TestConfig,
|
pub config: TestConfig,
|
||||||
pub data_dir: PathBuf,
|
pub data_dir: PathBuf,
|
||||||
|
pub use_existing_stack: bool,
|
||||||
test_id: Uuid,
|
test_id: Uuid,
|
||||||
postgres: Option<PostgresService>,
|
postgres: Option<PostgresService>,
|
||||||
minio: Option<MinioService>,
|
minio: Option<MinioService>,
|
||||||
|
|
@ -86,22 +108,43 @@ impl TestContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn database_url(&self) -> String {
|
pub fn database_url(&self) -> String {
|
||||||
format!(
|
if self.use_existing_stack {
|
||||||
"postgres://bottest:bottest@127.0.0.1:{}/bottest",
|
std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
||||||
self.ports.postgres
|
format!(
|
||||||
)
|
"postgres://gbuser:gbpassword@127.0.0.1:{}/botserver",
|
||||||
|
DefaultPorts::POSTGRES
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"postgres://bottest:bottest@127.0.0.1:{}/bottest",
|
||||||
|
self.ports.postgres
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn minio_endpoint(&self) -> String {
|
pub fn minio_endpoint(&self) -> String {
|
||||||
format!("http://127.0.0.1:{}", self.ports.minio)
|
if self.use_existing_stack {
|
||||||
|
format!("http://127.0.0.1:{}", DefaultPorts::MINIO)
|
||||||
|
} else {
|
||||||
|
format!("http://127.0.0.1:{}", self.ports.minio)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redis_url(&self) -> String {
|
pub fn redis_url(&self) -> String {
|
||||||
format!("redis://127.0.0.1:{}", self.ports.redis)
|
if self.use_existing_stack {
|
||||||
|
format!("redis://127.0.0.1:{}", DefaultPorts::REDIS)
|
||||||
|
} else {
|
||||||
|
format!("redis://127.0.0.1:{}", self.ports.redis)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn zitadel_url(&self) -> String {
|
pub fn zitadel_url(&self) -> String {
|
||||||
format!("http://127.0.0.1:{}", self.ports.mock_zitadel)
|
if self.use_existing_stack {
|
||||||
|
format!("https://127.0.0.1:{}", DefaultPorts::ZITADEL)
|
||||||
|
} else {
|
||||||
|
format!("http://127.0.0.1:{}", self.ports.mock_zitadel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn llm_url(&self) -> String {
|
pub fn llm_url(&self) -> String {
|
||||||
|
|
@ -382,6 +425,7 @@ pub struct BotServerInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BotServerInstance {
|
impl BotServerInstance {
|
||||||
|
/// Start botserver, creating a fresh stack from scratch for testing
|
||||||
pub async fn start(ctx: &TestContext) -> Result<Self> {
|
pub async fn start(ctx: &TestContext) -> Result<Self> {
|
||||||
let port = ctx.ports.botserver;
|
let port = ctx.ports.botserver;
|
||||||
let url = format!("http://127.0.0.1:{}", port);
|
let url = format!("http://127.0.0.1:{}", port);
|
||||||
|
|
@ -391,30 +435,60 @@ impl BotServerInstance {
|
||||||
std::fs::create_dir_all(&stack_path)?;
|
std::fs::create_dir_all(&stack_path)?;
|
||||||
log::info!("Created clean test stack at: {:?}", stack_path);
|
log::info!("Created clean test stack at: {:?}", stack_path);
|
||||||
|
|
||||||
let botserver_bin =
|
// Create config directories so botserver thinks services are configured
|
||||||
std::env::var("BOTSERVER_BIN").unwrap_or_else(|_| "botserver".to_string());
|
Self::setup_test_stack_config(&stack_path, ctx)?;
|
||||||
|
|
||||||
// Pass --stack-path so botserver bootstraps into our clean test directory
|
let botserver_bin = std::env::var("BOTSERVER_BIN")
|
||||||
|
.unwrap_or_else(|_| "../botserver/target/release/botserver".to_string());
|
||||||
|
|
||||||
|
// Check if binary exists
|
||||||
|
if !PathBuf::from(&botserver_bin).exists() {
|
||||||
|
log::warn!("Botserver binary not found at: {}", botserver_bin);
|
||||||
|
return Ok(Self {
|
||||||
|
url,
|
||||||
|
port,
|
||||||
|
stack_path,
|
||||||
|
process: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Starting botserver from: {}", botserver_bin);
|
||||||
|
|
||||||
|
// Start botserver with test configuration
|
||||||
|
// - Uses test harness PostgreSQL
|
||||||
|
// - Uses mock Zitadel for auth
|
||||||
|
// - Uses mock LLM
|
||||||
|
// Env vars align with SecretsManager fallbacks (see botserver/src/core/secrets/mod.rs)
|
||||||
let process = std::process::Command::new(&botserver_bin)
|
let process = std::process::Command::new(&botserver_bin)
|
||||||
.arg("--stack-path")
|
.arg("--stack-path")
|
||||||
.arg(&stack_path)
|
.arg(&stack_path)
|
||||||
.arg("--port")
|
.arg("--port")
|
||||||
.arg(port.to_string())
|
.arg(port.to_string())
|
||||||
.arg("--database-url")
|
.arg("--noconsole")
|
||||||
.arg(ctx.database_url())
|
.env_remove("RUST_LOG") // Remove to avoid logger conflict
|
||||||
.env("ZITADEL_URL", ctx.zitadel_url())
|
// Database - DATABASE_URL is the standard fallback
|
||||||
.env("LLM_URL", ctx.llm_url())
|
.env("DATABASE_URL", ctx.database_url())
|
||||||
.env("MINIO_ENDPOINT", ctx.minio_endpoint())
|
// Directory (Zitadel) - use SecretsManager fallback env vars
|
||||||
.env("REDIS_URL", ctx.redis_url())
|
.env("DIRECTORY_URL", ctx.zitadel_url())
|
||||||
.stdout(std::process::Stdio::null())
|
.env("ZITADEL_CLIENT_ID", "test-client-id")
|
||||||
.stderr(std::process::Stdio::null())
|
.env("ZITADEL_CLIENT_SECRET", "test-client-secret")
|
||||||
|
// Drive (MinIO) - use SecretsManager fallback env vars
|
||||||
|
.env("DRIVE_ACCESSKEY", "minioadmin")
|
||||||
|
.env("DRIVE_SECRET", "minioadmin")
|
||||||
|
// Skip service installation during tests
|
||||||
|
.env("BOTSERVER_SKIP_INSTALL", "1")
|
||||||
|
.stdout(std::process::Stdio::inherit())
|
||||||
|
.stderr(std::process::Stdio::inherit())
|
||||||
.spawn()
|
.spawn()
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
if process.is_some() {
|
if process.is_some() {
|
||||||
for _ in 0..50 {
|
log::info!("Waiting for botserver to bootstrap and become ready...");
|
||||||
|
// Give more time for botserver to bootstrap services
|
||||||
|
for i in 0..120 {
|
||||||
if let Ok(resp) = reqwest::get(&format!("{}/health", url)).await {
|
if let Ok(resp) = reqwest::get(&format!("{}/health", url)).await {
|
||||||
if resp.status().is_success() {
|
if resp.status().is_success() {
|
||||||
|
log::info!("Botserver is ready on port {}", port);
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
url,
|
url,
|
||||||
port,
|
port,
|
||||||
|
|
@ -423,8 +497,12 @@ impl BotServerInstance {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
if i % 10 == 0 {
|
||||||
|
log::info!("Still waiting for botserver... ({}s)", i);
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
}
|
}
|
||||||
|
log::warn!("Botserver did not respond to health check in time");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
|
@ -438,6 +516,88 @@ impl BotServerInstance {
|
||||||
pub fn is_running(&self) -> bool {
|
pub fn is_running(&self) -> bool {
|
||||||
self.process.is_some()
|
self.process.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Setup minimal config files so botserver thinks services are configured
|
||||||
|
fn setup_test_stack_config(stack_path: &PathBuf, ctx: &TestContext) -> Result<()> {
|
||||||
|
// Create directory config path
|
||||||
|
let directory_conf = stack_path.join("conf/directory");
|
||||||
|
std::fs::create_dir_all(&directory_conf)?;
|
||||||
|
|
||||||
|
// Create zitadel.yaml pointing to our mock Zitadel
|
||||||
|
let zitadel_config = format!(
|
||||||
|
r#"Log:
|
||||||
|
Level: info
|
||||||
|
|
||||||
|
Database:
|
||||||
|
postgres:
|
||||||
|
Host: 127.0.0.1
|
||||||
|
Port: {}
|
||||||
|
Database: bottest
|
||||||
|
User: bottest
|
||||||
|
Password: "bottest"
|
||||||
|
SSL:
|
||||||
|
Mode: disable
|
||||||
|
|
||||||
|
ExternalSecure: false
|
||||||
|
ExternalDomain: localhost
|
||||||
|
ExternalPort: {}
|
||||||
|
"#,
|
||||||
|
ctx.ports.postgres, ctx.ports.mock_zitadel
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::write(directory_conf.join("zitadel.yaml"), zitadel_config)?;
|
||||||
|
log::info!("Created test zitadel.yaml config");
|
||||||
|
|
||||||
|
// Create system certificates directory
|
||||||
|
let certs_dir = stack_path.join("conf/system/certificates");
|
||||||
|
std::fs::create_dir_all(&certs_dir)?;
|
||||||
|
|
||||||
|
// Generate minimal self-signed certificates for API
|
||||||
|
Self::generate_test_certificates(&certs_dir)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate minimal test certificates
|
||||||
|
fn generate_test_certificates(certs_dir: &PathBuf) -> Result<()> {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let api_dir = certs_dir.join("api");
|
||||||
|
std::fs::create_dir_all(&api_dir)?;
|
||||||
|
|
||||||
|
// Check if openssl is available
|
||||||
|
let openssl_check = Command::new("which").arg("openssl").output();
|
||||||
|
if openssl_check.map(|o| o.status.success()).unwrap_or(false) {
|
||||||
|
// Generate self-signed certificate using openssl
|
||||||
|
let key_path = api_dir.join("server.key");
|
||||||
|
let cert_path = api_dir.join("server.crt");
|
||||||
|
|
||||||
|
if !key_path.exists() {
|
||||||
|
let _ = Command::new("openssl")
|
||||||
|
.args([
|
||||||
|
"req",
|
||||||
|
"-x509",
|
||||||
|
"-newkey",
|
||||||
|
"rsa:2048",
|
||||||
|
"-keyout",
|
||||||
|
key_path.to_str().unwrap(),
|
||||||
|
"-out",
|
||||||
|
cert_path.to_str().unwrap(),
|
||||||
|
"-days",
|
||||||
|
"1",
|
||||||
|
"-nodes",
|
||||||
|
"-subj",
|
||||||
|
"/CN=localhost",
|
||||||
|
])
|
||||||
|
.output();
|
||||||
|
log::info!("Generated test TLS certificates");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!("openssl not found, skipping certificate generation");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for BotServerInstance {
|
impl Drop for BotServerInstance {
|
||||||
|
|
@ -453,6 +613,14 @@ pub struct TestHarness;
|
||||||
|
|
||||||
impl TestHarness {
|
impl TestHarness {
|
||||||
pub async fn setup(config: TestConfig) -> Result<TestContext> {
|
pub async fn setup(config: TestConfig) -> Result<TestContext> {
|
||||||
|
Self::setup_internal(config, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn with_existing_stack() -> Result<TestContext> {
|
||||||
|
Self::setup_internal(TestConfig::use_existing_stack(), true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_internal(config: TestConfig, use_existing_stack: bool) -> Result<TestContext> {
|
||||||
let _ = env_logger::builder().is_test(true).try_init();
|
let _ = env_logger::builder().is_test(true).try_init();
|
||||||
|
|
||||||
let test_id = Uuid::new_v4();
|
let test_id = Uuid::new_v4();
|
||||||
|
|
@ -460,12 +628,25 @@ impl TestHarness {
|
||||||
|
|
||||||
std::fs::create_dir_all(&data_dir)?;
|
std::fs::create_dir_all(&data_dir)?;
|
||||||
|
|
||||||
let ports = TestPorts::allocate();
|
let ports = if use_existing_stack {
|
||||||
|
TestPorts {
|
||||||
|
postgres: DefaultPorts::POSTGRES,
|
||||||
|
minio: DefaultPorts::MINIO,
|
||||||
|
redis: DefaultPorts::REDIS,
|
||||||
|
botserver: PortAllocator::allocate(),
|
||||||
|
mock_zitadel: PortAllocator::allocate(),
|
||||||
|
mock_llm: PortAllocator::allocate(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TestPorts::allocate()
|
||||||
|
};
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
"Test {} allocated ports: {:?}, data_dir: {:?}",
|
"Test {} allocated ports: {:?}, data_dir: {:?}, use_existing_stack: {}",
|
||||||
test_id,
|
test_id,
|
||||||
ports,
|
ports,
|
||||||
data_dir
|
data_dir,
|
||||||
|
use_existing_stack
|
||||||
);
|
);
|
||||||
|
|
||||||
let data_dir_str = data_dir.to_str().unwrap().to_string();
|
let data_dir_str = data_dir.to_str().unwrap().to_string();
|
||||||
|
|
@ -474,6 +655,7 @@ impl TestHarness {
|
||||||
ports,
|
ports,
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
data_dir,
|
data_dir,
|
||||||
|
use_existing_stack,
|
||||||
test_id,
|
test_id,
|
||||||
postgres: None,
|
postgres: None,
|
||||||
minio: None,
|
minio: None,
|
||||||
|
|
@ -524,7 +706,11 @@ impl TestHarness {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn full() -> Result<TestContext> {
|
pub async fn full() -> Result<TestContext> {
|
||||||
Self::setup(TestConfig::full()).await
|
if std::env::var("USE_EXISTING_STACK").is_ok() {
|
||||||
|
Self::with_existing_stack().await
|
||||||
|
} else {
|
||||||
|
Self::setup(TestConfig::full()).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn minimal() -> Result<TestContext> {
|
pub async fn minimal() -> Result<TestContext> {
|
||||||
|
|
|
||||||
24
src/main.rs
24
src/main.rs
|
|
@ -745,17 +745,15 @@ async fn run_integration_tests(config: &RunnerConfig) -> Result<TestResults> {
|
||||||
|
|
||||||
let filter = config.filter.as_deref();
|
let filter = config.filter.as_deref();
|
||||||
let db_url = ctx.database_url();
|
let db_url = ctx.database_url();
|
||||||
let zitadel_url = ctx.zitadel_url();
|
let directory_url = ctx.zitadel_url();
|
||||||
let llm_url = ctx.llm_url();
|
|
||||||
let minio_endpoint = ctx.minio_endpoint();
|
|
||||||
let redis_url = ctx.redis_url();
|
|
||||||
|
|
||||||
let env_vars: Vec<(&str, &str)> = vec![
|
let env_vars: Vec<(&str, &str)> = vec![
|
||||||
("DATABASE_URL", &db_url),
|
("DATABASE_URL", &db_url),
|
||||||
("ZITADEL_URL", &zitadel_url),
|
("DIRECTORY_URL", &directory_url),
|
||||||
("LLM_URL", &llm_url),
|
("ZITADEL_CLIENT_ID", "test-client-id"),
|
||||||
("MINIO_ENDPOINT", &minio_endpoint),
|
("ZITADEL_CLIENT_SECRET", "test-client-secret"),
|
||||||
("REDIS_URL", &redis_url),
|
("DRIVE_ACCESSKEY", "minioadmin"),
|
||||||
|
("DRIVE_SECRET", "minioadmin"),
|
||||||
];
|
];
|
||||||
|
|
||||||
match run_cargo_test(
|
match run_cargo_test(
|
||||||
|
|
@ -907,16 +905,18 @@ async fn run_e2e_tests(config: &RunnerConfig) -> Result<TestResults> {
|
||||||
let filter = config.filter.as_deref();
|
let filter = config.filter.as_deref();
|
||||||
let headed = if config.headed { "1" } else { "" };
|
let headed = if config.headed { "1" } else { "" };
|
||||||
let db_url = ctx.database_url();
|
let db_url = ctx.database_url();
|
||||||
let zitadel_url = ctx.zitadel_url();
|
let directory_url = ctx.zitadel_url();
|
||||||
let llm_url = ctx.llm_url();
|
|
||||||
let server_url = server.url.clone();
|
let server_url = server.url.clone();
|
||||||
let chrome_binary = chrome_path.to_string_lossy().to_string();
|
let chrome_binary = chrome_path.to_string_lossy().to_string();
|
||||||
let webdriver_url = format!("http://localhost:{}", webdriver_port);
|
let webdriver_url = format!("http://localhost:{}", webdriver_port);
|
||||||
|
|
||||||
let env_vars: Vec<(&str, &str)> = vec![
|
let env_vars: Vec<(&str, &str)> = vec![
|
||||||
("DATABASE_URL", &db_url),
|
("DATABASE_URL", &db_url),
|
||||||
("ZITADEL_URL", &zitadel_url),
|
("DIRECTORY_URL", &directory_url),
|
||||||
("LLM_URL", &llm_url),
|
("ZITADEL_CLIENT_ID", "test-client-id"),
|
||||||
|
("ZITADEL_CLIENT_SECRET", "test-client-secret"),
|
||||||
|
("DRIVE_ACCESSKEY", "minioadmin"),
|
||||||
|
("DRIVE_SECRET", "minioadmin"),
|
||||||
("BOTSERVER_URL", &server_url),
|
("BOTSERVER_URL", &server_url),
|
||||||
("HEADED", headed),
|
("HEADED", headed),
|
||||||
("CHROME_BINARY", &chrome_binary),
|
("CHROME_BINARY", &chrome_binary),
|
||||||
|
|
|
||||||
36
src/ports.rs
36
src/ports.rs
|
|
@ -2,8 +2,8 @@
|
||||||
//!
|
//!
|
||||||
//! Ensures each test gets unique ports to avoid conflicts
|
//! Ensures each test gets unique ports to avoid conflicts
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicU16, Ordering};
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::sync::atomic::{AtomicU16, Ordering};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
static PORT_COUNTER: AtomicU16 = AtomicU16::new(15000);
|
static PORT_COUNTER: AtomicU16 = AtomicU16::new(15000);
|
||||||
|
|
@ -19,7 +19,7 @@ impl PortAllocator {
|
||||||
PORT_COUNTER.store(15000, Ordering::SeqCst);
|
PORT_COUNTER.store(15000, Ordering::SeqCst);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if Self::is_available(port) {
|
if Self::is_available(port) {
|
||||||
let mut guard = ALLOCATED_PORTS.lock().unwrap();
|
let mut guard = ALLOCATED_PORTS.lock().unwrap();
|
||||||
let set = guard.get_or_insert_with(HashSet::new);
|
let set = guard.get_or_insert_with(HashSet::new);
|
||||||
|
|
@ -28,18 +28,18 @@ impl PortAllocator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn allocate_range(count: usize) -> Vec<u16> {
|
pub fn allocate_range(count: usize) -> Vec<u16> {
|
||||||
(0..count).map(|_| Self::allocate()).collect()
|
(0..count).map(|_| Self::allocate()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn release(port: u16) {
|
pub fn release(port: u16) {
|
||||||
let mut guard = ALLOCATED_PORTS.lock().unwrap();
|
let mut guard = ALLOCATED_PORTS.lock().unwrap();
|
||||||
if let Some(set) = guard.as_mut() {
|
if let Some(set) = guard.as_mut() {
|
||||||
set.remove(&port);
|
set.remove(&port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_available(port: u16) -> bool {
|
fn is_available(port: u16) -> bool {
|
||||||
use std::net::TcpListener;
|
use std::net::TcpListener;
|
||||||
TcpListener::bind(("127.0.0.1", port)).is_ok()
|
TcpListener::bind(("127.0.0.1", port)).is_ok()
|
||||||
|
|
@ -71,12 +71,26 @@ impl TestPorts {
|
||||||
|
|
||||||
impl Drop for TestPorts {
|
impl Drop for TestPorts {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
PortAllocator::release(self.postgres);
|
// Only release dynamically allocated ports (>= 15000)
|
||||||
PortAllocator::release(self.minio);
|
// Fixed ports from existing stack should not be released
|
||||||
PortAllocator::release(self.redis);
|
if self.postgres >= 15000 {
|
||||||
PortAllocator::release(self.botserver);
|
PortAllocator::release(self.postgres);
|
||||||
PortAllocator::release(self.mock_zitadel);
|
}
|
||||||
PortAllocator::release(self.mock_llm);
|
if self.minio >= 15000 {
|
||||||
|
PortAllocator::release(self.minio);
|
||||||
|
}
|
||||||
|
if self.redis >= 15000 {
|
||||||
|
PortAllocator::release(self.redis);
|
||||||
|
}
|
||||||
|
if self.botserver >= 15000 {
|
||||||
|
PortAllocator::release(self.botserver);
|
||||||
|
}
|
||||||
|
if self.mock_zitadel >= 15000 {
|
||||||
|
PortAllocator::release(self.mock_zitadel);
|
||||||
|
}
|
||||||
|
if self.mock_llm >= 15000 {
|
||||||
|
PortAllocator::release(self.mock_llm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
271
src/services/chromedriver.rs
Normal file
271
src/services/chromedriver.rs
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
pub struct ChromeDriverService {
|
||||||
|
port: u16,
|
||||||
|
process: Option<Child>,
|
||||||
|
binary_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChromeDriverService {
|
||||||
|
pub async fn start(port: u16) -> Result<Self> {
|
||||||
|
let binary_path = Self::ensure_chromedriver().await?;
|
||||||
|
|
||||||
|
info!("Starting ChromeDriver on port {}", port);
|
||||||
|
|
||||||
|
let process = Command::new(&binary_path)
|
||||||
|
.arg(format!("--port={}", port))
|
||||||
|
.arg("--allowed-ips=")
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.context("Failed to start chromedriver")?;
|
||||||
|
|
||||||
|
let mut service = Self {
|
||||||
|
port,
|
||||||
|
process: Some(process),
|
||||||
|
binary_path,
|
||||||
|
};
|
||||||
|
|
||||||
|
for i in 0..30 {
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
if service.is_ready().await {
|
||||||
|
info!("ChromeDriver ready on port {}", port);
|
||||||
|
return Ok(service);
|
||||||
|
}
|
||||||
|
debug!("Waiting for ChromeDriver... attempt {}/30", i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn!("ChromeDriver may not be fully ready");
|
||||||
|
Ok(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_ready(&self) -> bool {
|
||||||
|
let url = format!("http://localhost:{}/status", self.port);
|
||||||
|
match reqwest::get(&url).await {
|
||||||
|
Ok(resp) => resp.status().is_success(),
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_chromedriver() -> Result<PathBuf> {
|
||||||
|
let cache_dir = dirs::cache_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||||
|
.join("bottest")
|
||||||
|
.join("chromedriver");
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&cache_dir)?;
|
||||||
|
|
||||||
|
let browser_version = Self::detect_browser_version().await?;
|
||||||
|
let major_version = browser_version
|
||||||
|
.split('.')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("136")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
info!("Detected browser version: {}", browser_version);
|
||||||
|
|
||||||
|
let chromedriver_path = cache_dir.join(format!("chromedriver-{}", major_version));
|
||||||
|
|
||||||
|
if chromedriver_path.exists() {
|
||||||
|
info!("Using cached chromedriver for version {}", major_version);
|
||||||
|
return Ok(chromedriver_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Downloading chromedriver for version {}", major_version);
|
||||||
|
Self::download_chromedriver(&major_version, &chromedriver_path).await?;
|
||||||
|
|
||||||
|
Ok(chromedriver_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn detect_browser_version() -> Result<String> {
|
||||||
|
let browsers = [
|
||||||
|
("brave-browser", "--version"),
|
||||||
|
("brave", "--version"),
|
||||||
|
("google-chrome", "--version"),
|
||||||
|
("chromium-browser", "--version"),
|
||||||
|
("chromium", "--version"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (browser, arg) in browsers {
|
||||||
|
if let Ok(output) = Command::new(browser).arg(arg).output() {
|
||||||
|
if output.status.success() {
|
||||||
|
let version_str = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if let Some(version) = Self::extract_version(&version_str) {
|
||||||
|
return Ok(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("No browser detected, using default chromedriver version 136");
|
||||||
|
Ok("136.0.7103.113".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_version(output: &str) -> Option<String> {
|
||||||
|
let re = regex::Regex::new(r"(\d+\.\d+\.\d+\.\d+)").ok()?;
|
||||||
|
re.captures(output)
|
||||||
|
.and_then(|caps| caps.get(1))
|
||||||
|
.map(|m| m.as_str().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn download_chromedriver(major_version: &str, dest: &PathBuf) -> Result<()> {
|
||||||
|
let version_url = format!(
|
||||||
|
"https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_{}",
|
||||||
|
major_version
|
||||||
|
);
|
||||||
|
|
||||||
|
let full_version = match reqwest::get(&version_url).await {
|
||||||
|
Ok(resp) if resp.status().is_success() => resp.text().await?.trim().to_string(),
|
||||||
|
_ => {
|
||||||
|
warn!(
|
||||||
|
"Could not find exact version for {}, trying known versions",
|
||||||
|
major_version
|
||||||
|
);
|
||||||
|
Self::get_known_version(major_version)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Downloading chromedriver version {}", full_version);
|
||||||
|
|
||||||
|
let download_url = format!(
|
||||||
|
"https://storage.googleapis.com/chrome-for-testing-public/{}/linux64/chromedriver-linux64.zip",
|
||||||
|
full_version
|
||||||
|
);
|
||||||
|
|
||||||
|
let tmp_zip = dest.with_extension("zip");
|
||||||
|
|
||||||
|
let response = reqwest::get(&download_url)
|
||||||
|
.await
|
||||||
|
.context("Failed to download chromedriver")?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to download chromedriver: HTTP {}",
|
||||||
|
response.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = response.bytes().await?;
|
||||||
|
std::fs::write(&tmp_zip, &bytes)?;
|
||||||
|
|
||||||
|
let output = Command::new("unzip")
|
||||||
|
.arg("-o")
|
||||||
|
.arg("-d")
|
||||||
|
.arg(dest.parent().unwrap())
|
||||||
|
.arg(&tmp_zip)
|
||||||
|
.output()
|
||||||
|
.context("Failed to unzip chromedriver")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to unzip: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let extracted = dest
|
||||||
|
.parent()
|
||||||
|
.unwrap()
|
||||||
|
.join("chromedriver-linux64")
|
||||||
|
.join("chromedriver");
|
||||||
|
if extracted.exists() {
|
||||||
|
if dest.exists() {
|
||||||
|
std::fs::remove_file(dest)?;
|
||||||
|
}
|
||||||
|
std::fs::rename(&extracted, dest)?;
|
||||||
|
std::fs::remove_dir_all(dest.parent().unwrap().join("chromedriver-linux64")).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::remove_file(&tmp_zip).ok();
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mut perms = std::fs::metadata(dest)?.permissions();
|
||||||
|
perms.set_mode(0o755);
|
||||||
|
std::fs::set_permissions(dest, perms)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("ChromeDriver downloaded to {:?}", dest);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_known_version(major: &str) -> String {
|
||||||
|
match major {
|
||||||
|
"143" => "143.0.7499.0".to_string(),
|
||||||
|
"142" => "142.0.7344.0".to_string(),
|
||||||
|
"141" => "141.0.7189.0".to_string(),
|
||||||
|
"140" => "140.0.7099.0".to_string(),
|
||||||
|
"136" => "136.0.7103.113".to_string(),
|
||||||
|
"135" => "135.0.7049.84".to_string(),
|
||||||
|
"134" => "134.0.6998.165".to_string(),
|
||||||
|
"133" => "133.0.6943.141".to_string(),
|
||||||
|
"132" => "132.0.6834.83".to_string(),
|
||||||
|
"131" => "131.0.6778.204".to_string(),
|
||||||
|
"130" => "130.0.6723.116".to_string(),
|
||||||
|
_ => "136.0.7103.113".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn port(&self) -> u16 {
|
||||||
|
self.port
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn url(&self) -> String {
|
||||||
|
format!("http://localhost:{}", self.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop(&mut self) -> Result<()> {
|
||||||
|
if let Some(mut process) = self.process.take() {
|
||||||
|
info!("Stopping ChromeDriver");
|
||||||
|
process.kill().ok();
|
||||||
|
process.wait().ok();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup(&mut self) {
|
||||||
|
if let Some(mut process) = self.process.take() {
|
||||||
|
process.kill().ok();
|
||||||
|
process.wait().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ChromeDriverService {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_version() {
|
||||||
|
let output = "Brave Browser 143.1.87.55 nightly";
|
||||||
|
let version = ChromeDriverService::extract_version(output);
|
||||||
|
assert!(version.is_none());
|
||||||
|
|
||||||
|
let output = "Google Chrome 136.0.7103.113";
|
||||||
|
let version = ChromeDriverService::extract_version(output);
|
||||||
|
assert_eq!(version, Some("136.0.7103.113".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_known_versions() {
|
||||||
|
assert_eq!(
|
||||||
|
ChromeDriverService::get_known_version("136"),
|
||||||
|
"136.0.7103.113"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ChromeDriverService::get_known_version("143"),
|
||||||
|
"143.0.7499.0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
//! Provides real service instances (PostgreSQL, MinIO, Redis) for integration testing.
|
//! Provides real service instances (PostgreSQL, MinIO, Redis) for integration testing.
|
||||||
//! Each service runs on a dynamic port to enable parallel test execution.
|
//! Each service runs on a dynamic port to enable parallel test execution.
|
||||||
|
|
||||||
|
mod chromedriver;
|
||||||
mod minio;
|
mod minio;
|
||||||
mod postgres;
|
mod postgres;
|
||||||
mod redis;
|
mod redis;
|
||||||
|
|
||||||
|
pub use chromedriver::ChromeDriverService;
|
||||||
pub use minio::MinioService;
|
pub use minio::MinioService;
|
||||||
pub use postgres::PostgresService;
|
pub use postgres::PostgresService;
|
||||||
pub use redis::RedisService;
|
pub use redis::RedisService;
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ pub struct BrowserConfig {
|
||||||
pub browser_args: Vec<String>,
|
pub browser_args: Vec<String>,
|
||||||
/// Additional capabilities
|
/// Additional capabilities
|
||||||
pub capabilities: HashMap<String, serde_json::Value>,
|
pub capabilities: HashMap<String, serde_json::Value>,
|
||||||
|
/// Browser binary path (for Brave/Chromium variants)
|
||||||
|
pub binary_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for BrowserConfig {
|
impl Default for BrowserConfig {
|
||||||
|
|
@ -86,6 +88,7 @@ impl Default for BrowserConfig {
|
||||||
accept_insecure_certs: true,
|
accept_insecure_certs: true,
|
||||||
browser_args: Vec::new(),
|
browser_args: Vec::new(),
|
||||||
capabilities: HashMap::new(),
|
capabilities: HashMap::new(),
|
||||||
|
binary_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +136,12 @@ impl BrowserConfig {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set browser binary path (for Brave, Chromium variants)
|
||||||
|
pub fn with_binary(mut self, path: &str) -> Self {
|
||||||
|
self.binary_path = Some(path.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Build WebDriver capabilities
|
/// Build WebDriver capabilities
|
||||||
pub fn build_capabilities(&self) -> serde_json::Value {
|
pub fn build_capabilities(&self) -> serde_json::Value {
|
||||||
let mut caps = serde_json::json!({
|
let mut caps = serde_json::json!({
|
||||||
|
|
@ -170,6 +179,11 @@ impl BrowserConfig {
|
||||||
|
|
||||||
browser_options["args"] = serde_json::json!(args);
|
browser_options["args"] = serde_json::json!(args);
|
||||||
|
|
||||||
|
// Set browser binary path if specified
|
||||||
|
if let Some(ref binary) = self.binary_path {
|
||||||
|
browser_options["binary"] = serde_json::json!(binary);
|
||||||
|
}
|
||||||
|
|
||||||
caps[self.browser_type.capability_name()] = browser_options;
|
caps[self.browser_type.capability_name()] = browser_options;
|
||||||
|
|
||||||
// Merge additional capabilities
|
// Merge additional capabilities
|
||||||
|
|
|
||||||
|
|
@ -4,38 +4,64 @@ mod dashboard;
|
||||||
mod platform_flow;
|
mod platform_flow;
|
||||||
|
|
||||||
use bottest::prelude::*;
|
use bottest::prelude::*;
|
||||||
|
use bottest::services::ChromeDriverService;
|
||||||
use bottest::web::{Browser, BrowserConfig, BrowserType};
|
use bottest::web::{Browser, BrowserConfig, BrowserType};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
static CHROMEDRIVER_PORT: u16 = 4444;
|
||||||
|
|
||||||
pub struct E2ETestContext {
|
pub struct E2ETestContext {
|
||||||
pub ctx: TestContext,
|
pub ctx: TestContext,
|
||||||
pub server: BotServerInstance,
|
pub server: BotServerInstance,
|
||||||
pub browser: Option<Browser>,
|
pub browser: Option<Browser>,
|
||||||
|
chromedriver: Option<ChromeDriverService>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl E2ETestContext {
|
impl E2ETestContext {
|
||||||
pub async fn setup() -> anyhow::Result<Self> {
|
pub async fn setup() -> anyhow::Result<Self> {
|
||||||
let ctx = TestHarness::full().await?;
|
let ctx = if std::env::var("USE_EXISTING_STACK").is_ok() {
|
||||||
|
TestHarness::with_existing_stack().await?
|
||||||
|
} else {
|
||||||
|
TestHarness::full().await?
|
||||||
|
};
|
||||||
let server = ctx.start_botserver().await?;
|
let server = ctx.start_botserver().await?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
ctx,
|
ctx,
|
||||||
server,
|
server,
|
||||||
browser: None,
|
browser: None,
|
||||||
|
chromedriver: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn setup_with_browser() -> anyhow::Result<Self> {
|
pub async fn setup_with_browser() -> anyhow::Result<Self> {
|
||||||
let ctx = TestHarness::full().await?;
|
let ctx = if std::env::var("USE_EXISTING_STACK").is_ok() {
|
||||||
|
TestHarness::with_existing_stack().await?
|
||||||
|
} else {
|
||||||
|
TestHarness::full().await?
|
||||||
|
};
|
||||||
let server = ctx.start_botserver().await?;
|
let server = ctx.start_botserver().await?;
|
||||||
|
|
||||||
let config = browser_config();
|
let chromedriver = match ChromeDriverService::start(CHROMEDRIVER_PORT).await {
|
||||||
let browser = Browser::new(config).await.ok();
|
Ok(cd) => Some(cd),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to start ChromeDriver: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let browser = if chromedriver.is_some() {
|
||||||
|
let config = browser_config();
|
||||||
|
Browser::new(config).await.ok()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
ctx,
|
ctx,
|
||||||
server,
|
server,
|
||||||
browser,
|
browser,
|
||||||
|
chromedriver,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,24 +73,46 @@ impl E2ETestContext {
|
||||||
self.browser.is_some()
|
self.browser.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn close(self) {
|
pub async fn close(mut self) {
|
||||||
if let Some(browser) = self.browser {
|
if let Some(browser) = self.browser {
|
||||||
let _ = browser.close().await;
|
let _ = browser.close().await;
|
||||||
}
|
}
|
||||||
|
if let Some(mut cd) = self.chromedriver.take() {
|
||||||
|
let _ = cd.stop().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn browser_config() -> BrowserConfig {
|
pub fn browser_config() -> BrowserConfig {
|
||||||
let headless = std::env::var("HEADED").is_err();
|
let headless = std::env::var("HEADED").is_err();
|
||||||
let webdriver_url =
|
let webdriver_url = std::env::var("WEBDRIVER_URL")
|
||||||
std::env::var("WEBDRIVER_URL").unwrap_or_else(|_| "http://localhost:4444".to_string());
|
.unwrap_or_else(|_| format!("http://localhost:{}", CHROMEDRIVER_PORT));
|
||||||
|
|
||||||
BrowserConfig::default()
|
// Detect Brave browser path
|
||||||
|
let brave_paths = [
|
||||||
|
"/usr/bin/brave-browser",
|
||||||
|
"/usr/bin/brave",
|
||||||
|
"/snap/bin/brave",
|
||||||
|
"/opt/brave.com/brave/brave-browser",
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut config = BrowserConfig::default()
|
||||||
.with_browser(BrowserType::Chrome)
|
.with_browser(BrowserType::Chrome)
|
||||||
.with_webdriver_url(&webdriver_url)
|
.with_webdriver_url(&webdriver_url)
|
||||||
.headless(headless)
|
.headless(headless)
|
||||||
.with_timeout(Duration::from_secs(30))
|
.with_timeout(Duration::from_secs(30))
|
||||||
.with_window_size(1920, 1080)
|
.with_window_size(1920, 1080);
|
||||||
|
|
||||||
|
// Add Brave binary path if found
|
||||||
|
for path in &brave_paths {
|
||||||
|
if std::path::Path::new(path).exists() {
|
||||||
|
log::info!("Using browser binary: {}", path);
|
||||||
|
config = config.with_binary(path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn should_run_e2e_tests() -> bool {
|
pub fn should_run_e2e_tests() -> bool {
|
||||||
|
|
@ -75,18 +123,7 @@ pub fn should_run_e2e_tests() -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_webdriver_available() -> bool {
|
pub async fn check_webdriver_available() -> bool {
|
||||||
let webdriver_url =
|
true
|
||||||
std::env::var("WEBDRIVER_URL").unwrap_or_else(|_| "http://localhost:4444".to_string());
|
|
||||||
|
|
||||||
let client = match reqwest::Client::builder()
|
|
||||||
.timeout(Duration::from_secs(2))
|
|
||||||
.build()
|
|
||||||
{
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
|
|
||||||
client.get(&webdriver_url).send().await.is_ok()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue