diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 3e5483b..6903650 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -19,7 +19,7 @@ jobs: - name: Run nix flake check run: nix flake check --print-build-logs - build: + build-test: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d8411fc..4b553f3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -44,6 +44,16 @@ jobs: test: runs-on: ubuntu-latest + services: + postgres: + image: postgres + env: + POSTGRES_USER: atuin + POSTGRES_PASSWORD: pass + POSTGRES_DB: atuin + ports: + - 5432:5432 + steps: - uses: actions/checkout@v3 @@ -62,6 +72,8 @@ jobs: - name: Run cargo test run: cargo test --all-features --workspace + env: + ATUIN_DB_URI: postgres://atuin:pass@localhost:5432/atuin - name: Run cargo check (all features) run: cargo check --all-features --workspace diff --git a/Cargo.lock b/Cargo.lock index bd122b3..afb4318 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,7 +111,9 @@ dependencies = [ "serde_json", "tiny-bip39", "tokio", + "tracing", "tracing-subscriber", + "tracing-tree", "unicode-width", "whoami", ] @@ -2947,6 +2949,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", ] [[package]] @@ -2965,6 +2979,19 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-tree" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d6b63348fad3ae0439b8bebf8d38fb5bda0b115d7a8a7e6f165f12790c58c3" +dependencies = [ + "is-terminal", + "nu-ansi-term", + "tracing-core", + "tracing-log", + "tracing-subscriber", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -3070,6 +3097,12 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/atuin-server/src/lib.rs b/atuin-server/src/lib.rs index fc4d9e0..810b7db 100644 --- a/atuin-server/src/lib.rs +++ b/atuin-server/src/lib.rs @@ -1,6 +1,9 @@ #![forbid(unsafe_code)] -use std::net::{IpAddr, SocketAddr}; +use std::{ + future::Future, + net::{IpAddr, SocketAddr, TcpListener}, +}; use atuin_server_database::Database; use axum::Server; @@ -16,10 +19,15 @@ use tokio::signal; #[cfg(target_family = "unix")] async fn shutdown_signal() { - signal::unix::signal(signal::unix::SignalKind::terminate()) - .expect("failed to register signal handler") - .recv() - .await; + let mut term = signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to register signal handler"); + let mut interrupt = signal::unix::signal(signal::unix::SignalKind::interrupt()) + .expect("failed to register signal handler"); + + tokio::select! { + _ = term.recv() => {}, + _ = interrupt.recv() => {}, + }; eprintln!("Shutting down gracefully..."); } @@ -38,16 +46,29 @@ pub async fn launch( port: u16, ) -> Result<()> { let host = host.parse::()?; + launch_with_listener::( + settings, + TcpListener::bind(SocketAddr::new(host, port)).context("could not connect to socket")?, + shutdown_signal(), + ) + .await +} +pub async fn launch_with_listener( + settings: Settings, + listener: TcpListener, + shutdown: impl Future, +) -> Result<()> { let db = Db::new(&settings.db_settings) .await .wrap_err_with(|| format!("failed to connect to db: {:?}", settings.db_settings))?; let r = router::router(db, settings); - Server::bind(&SocketAddr::new(host, port)) + Server::from_tcp(listener) + .context("could not launch server")? .serve(r.into_make_service()) - .with_graceful_shutdown(shutdown_signal()) + .with_graceful_shutdown(shutdown) .await?; Ok(()) diff --git a/atuin/Cargo.toml b/atuin/Cargo.toml index 4398918..551a1f1 100644 --- a/atuin/Cargo.toml +++ b/atuin/Cargo.toml @@ -71,9 +71,14 @@ futures-util = "0.3" fuzzy-matcher = "0.3.7" colored = "2.0.4" ratatui = "0.21" +tracing = "0.1" + [dependencies.tracing-subscriber] version = "0.3" default-features = false features = ["ansi", "fmt", "registry", "env-filter"] optional = true + +[dev-dependencies] +tracing-tree = "0.2" diff --git a/atuin/tests/sync.rs b/atuin/tests/sync.rs new file mode 100644 index 0000000..35aa35b --- /dev/null +++ b/atuin/tests/sync.rs @@ -0,0 +1,99 @@ +use std::{env, net::TcpListener, time::Duration}; + +use atuin_client::api_client; +use atuin_common::utils::uuid_v7; +use atuin_server::{launch_with_listener, Settings as ServerSettings}; +use atuin_server_postgres::{Postgres, PostgresSettings}; +use futures_util::TryFutureExt; +use tokio::{sync::oneshot, task::JoinHandle}; +use tracing::{dispatcher, Dispatch}; +use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; + +async fn start_server(path: &str) -> (String, oneshot::Sender<()>, JoinHandle<()>) { + let formatting_layer = tracing_tree::HierarchicalLayer::default() + .with_writer(tracing_subscriber::fmt::TestWriter::new()) + .with_indent_lines(true) + .with_ansi(true) + .with_targets(true) + .with_indent_amount(2); + + let dispatch: Dispatch = tracing_subscriber::registry() + .with(formatting_layer) + .with(EnvFilter::new("atuin_server=debug,info")) + .into(); + + let db_uri = env::var("ATUIN_DB_URI") + .unwrap_or_else(|_| "postgres://atuin:pass@localhost:5432/atuin".to_owned()); + + let server_settings = ServerSettings { + host: "127.0.0.1".to_owned(), + port: 0, + path: path.to_owned(), + open_registration: true, + max_history_length: 8192, + max_record_size: 1024 * 1024 * 1024, + page_size: 1100, + register_webhook_url: None, + register_webhook_username: String::new(), + db_settings: PostgresSettings { db_uri }, + }; + + let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel(); + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + let _tracing_guard = dispatcher::set_default(&dispatch); + + if let Err(e) = launch_with_listener::( + server_settings, + listener, + shutdown_rx.unwrap_or_else(|_| ()), + ) + .await + { + tracing::error!(error=?e, "server error"); + panic!("error running server: {e:?}"); + } + }); + + // let the server come online + tokio::time::sleep(Duration::from_millis(200)).await; + + (format!("http://{addr}{path}"), shutdown_tx, server) +} + +#[tokio::test] +async fn registration() { + let path = format!("/{}", uuid_v7().as_simple()); + let (address, shutdown, server) = start_server(&path).await; + dbg!(&address); + + let username = uuid_v7().as_simple().to_string(); + let email = format!("{}@example.com", uuid_v7().as_simple()); + let password = uuid_v7().as_simple().to_string(); + + // registration works + let registration_response = api_client::register(&address, &username, &email, &password) + .await + .unwrap(); + + let client = api_client::Client::new(&address, ®istration_response.session).unwrap(); + + // the session token works + let status = client.status().await.unwrap(); + assert_eq!(status.username, username); + + // login works + let login_response = api_client::login( + &address, + atuin_common::api::LoginRequest { username, password }, + ) + .await + .unwrap(); + + // currently we return the same session token + assert_eq!(registration_response.session, login_response.session); + + shutdown.send(()).unwrap(); + server.await.unwrap(); +}