From 489bd75f29f348cf60fb61468dbb7c8ab2844663 Mon Sep 17 00:00:00 2001 From: ymgyt Date: Fri, 31 May 2024 19:12:56 +0900 Subject: [PATCH] test(term): setup application in helper method --- crates/synd_term/src/application/mod.rs | 14 +- crates/synd_term/src/keymap/macros.rs | 9 ++ crates/synd_term/tests/integration.rs | 138 +++++++----------- ...ntegration__test__device_flow_prompt.snap} | 1 + .../integration__test__hello_world.snap | 64 -------- ... => integration__test__initial_login.snap} | 35 ++--- ...> integration__test__landing_entries.snap} | 1 + crates/synd_term/tests/test/helper.rs | 94 +++++++++++- 8 files changed, 184 insertions(+), 172 deletions(-) rename crates/synd_term/tests/snapshots/{integration__test__hello_world-2.snap => integration__test__device_flow_prompt.snap} (99%) delete mode 100644 crates/synd_term/tests/snapshots/integration__test__hello_world.snap rename crates/synd_term/tests/snapshots/{integration__test__happy.snap => integration__test__initial_login.snap} (51%) rename crates/synd_term/tests/snapshots/{integration__test__happy-2.snap => integration__test__landing_entries.snap} (99%) diff --git a/crates/synd_term/src/application/mod.rs b/crates/synd_term/src/application/mod.rs index f56057b2..0beefd12 100644 --- a/crates/synd_term/src/application/mod.rs +++ b/crates/synd_term/src/application/mod.rs @@ -1002,7 +1002,6 @@ impl Application { #[cfg(feature = "integration")] { tracing::debug!("Quit for idle"); - self.should_render(); self.flags.insert(Should::Quit); } } @@ -1026,4 +1025,17 @@ impl Application { pub fn buffer(&self) -> &ratatui::buffer::Buffer { self.terminal.buffer() } + + pub async fn wait_until_jobs_completed(&mut self, input: &mut S) + where + S: Stream> + Unpin, + { + loop { + self.event_loop_until_idle(input).await; + if self.jobs.futures.is_empty() { + break; + } + self.reset_idle_timer(); + } + } } diff --git a/crates/synd_term/src/keymap/macros.rs b/crates/synd_term/src/keymap/macros.rs index 8b95d4bd..42bc0d5a 100644 --- a/crates/synd_term/src/keymap/macros.rs +++ b/crates/synd_term/src/keymap/macros.rs @@ -34,3 +34,12 @@ macro_rules! keymap { } pub(crate) use keymap; + +#[macro_export] +macro_rules! key { + ( enter ) => { + crossterm::event::Event::Key(crossterm::event::KeyEvent::from( + crossterm::event::KeyCode::Enter, + )) + }; +} diff --git a/crates/synd_term/tests/integration.rs b/crates/synd_term/tests/integration.rs index 2605a005..dbd76de4 100644 --- a/crates/synd_term/tests/integration.rs +++ b/crates/synd_term/tests/integration.rs @@ -1,97 +1,68 @@ #[cfg(feature = "integration")] mod test { - mod helper; - - use std::time::Duration; - - use crossterm::event::{Event, KeyCode, KeyEvent}; use serial_test::file_serial; - - use synd_auth::device_flow::{provider, DeviceFlow}; - - use synd_term::{ - application::{Application, Authenticator, Config, DeviceFlows}, - client::Client, - config::Categories, - ui::theme::Theme, - }; - use tokio::net::TcpListener; + use synd_term::key; use tokio_stream::wrappers::UnboundedReceiverStream; - use tracing_subscriber::EnvFilter; + + mod helper; + use crate::test::helper::TestCase; #[tokio::test(flavor = "multi_thread")] #[file_serial(a)] async fn happy() -> anyhow::Result<()> { - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .with_line_number(true) - .with_file(true) - .with_target(false) - .init(); - - let mock_port = 6000; - let api_port = 6001; - let oauth_addr = ("127.0.0.1", mock_port); - let oauth_listener = TcpListener::bind(oauth_addr).await?; - tokio::spawn(synd_test::mock::serve(oauth_listener)); - helper::serve_api(mock_port, api_port).await?; - - check_command_test(api_port); - - let endpoint = format!("https://localhost:{api_port}/graphql") - .parse() - .unwrap(); - let terminal = helper::new_test_terminal(120, 30); - let client = Client::new(endpoint, Duration::from_secs(30)).unwrap(); - let device_flows = DeviceFlows { - github: DeviceFlow::new( - provider::Github::new("dummy") - .with_device_authorization_endpoint(format!( - "http://localhost:{mock_port}/case1/github/login/device/code", - )) - .with_token_endpoint( - "http://localhost:6000/case1/github/login/oauth/access_token", - ), - ), - google: DeviceFlow::new(provider::Google::new("dummy", "dummy")), - }; - let authenticator = Authenticator::new().with_device_flows(device_flows); - let config = Config { - idle_timer_interval: Duration::from_millis(2000), - throbber_timer_interval: Duration::from_secs(3600), // disable throbber - ..Default::default() + helper::init_tracing(); + + let test_case = TestCase { + oauth_provider_port: 6000, + synd_api_port: 6001, + kvsd_port: 47379, + terminal_col_row: (120, 30), + device_flow_case: "case1", }; - // or mpsc and tokio_stream ReceiverStream + let mut application = test_case.init_app().await?; + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); let mut event_stream = UnboundedReceiverStream::new(rx); - let theme = Theme::default(); - - let mut application = - Application::with(terminal, client, Categories::default_toml(), config) - .with_theme(theme.clone()) - .with_authenticator(authenticator); - application.event_loop_until_idle(&mut event_stream).await; - - insta::assert_debug_snapshot!(application.buffer()); - - tracing::info!("Login assertion OK"); - - // push enter => start auth flow - let event = Event::Key(KeyEvent::from(KeyCode::Enter)); - tx.send(Ok(event)).unwrap(); - application.event_loop_until_idle(&mut event_stream).await; - - insta::assert_debug_snapshot!(application.buffer()); - - tracing::info!("Login prompt assertion OK"); - - // for quit event loop after polling job complete - application.reset_idle_timer(); - // polling device access token complete - application.event_loop_until_idle(&mut event_stream).await; - export_command_test(api_port); - clean_command_test(); + { + application + .wait_until_jobs_completed(&mut event_stream) + .await; + insta::with_settings!({ + description => "initial login prompt", + }, { + insta::assert_debug_snapshot!("initial_login", application.buffer()); + }); + } + + { + // push enter => start auth flow + tx.send(Ok(key!(enter))).unwrap(); + application.event_loop_until_idle(&mut event_stream).await; + insta::with_settings!({ + description => "show device flow code", + },{ + insta::assert_debug_snapshot!("device_flow_prompt", application.buffer()); + }); + } + + { + // polling device access token complete + application + .wait_until_jobs_completed(&mut event_stream) + .await; + insta::with_settings!({ + description => "initial landing entries", + },{ + insta::assert_debug_snapshot!("landing_entries", application.buffer()); + }); + } + + { + check_command_test(test_case.synd_api_port); + export_command_test(test_case.synd_api_port); + clean_command_test(); + } Ok(()) } @@ -127,6 +98,7 @@ mod test { fn clean_command_test() { let mut cmd = assert_cmd::Command::cargo_bin("synd").unwrap(); - cmd.args(["clean"]).assert().success(); + // TODO: Currently clear real cache :( + cmd.args(["clean", "--help"]).assert().success(); } } diff --git a/crates/synd_term/tests/snapshots/integration__test__hello_world-2.snap b/crates/synd_term/tests/snapshots/integration__test__device_flow_prompt.snap similarity index 99% rename from crates/synd_term/tests/snapshots/integration__test__hello_world-2.snap rename to crates/synd_term/tests/snapshots/integration__test__device_flow_prompt.snap index 64225a2e..1295dde9 100644 --- a/crates/synd_term/tests/snapshots/integration__test__hello_world-2.snap +++ b/crates/synd_term/tests/snapshots/integration__test__device_flow_prompt.snap @@ -1,5 +1,6 @@ --- source: crates/synd_term/tests/integration.rs +description: show device flow code expression: application.buffer() --- Buffer { diff --git a/crates/synd_term/tests/snapshots/integration__test__hello_world.snap b/crates/synd_term/tests/snapshots/integration__test__hello_world.snap deleted file mode 100644 index 990dae85..00000000 --- a/crates/synd_term/tests/snapshots/integration__test__hello_world.snap +++ /dev/null @@ -1,64 +0,0 @@ ---- -source: crates/synd_term/tests/integration.rs -expression: application.buffer() ---- -Buffer { - area: Rect { x: 0, y: 0, width: 120, height: 30 }, - content: [ - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ▐█▌ ▐█ █ ▐ █ ▐█ ", - " █ █ █ █ █ ", - " █▌ █ █ ██▌ █ ▐█ ▐█▌ ▐█▌ ▐██ ▐█ ▐█▌ ██▌ █ ", - " ▐█ █ █ █ █ ▐██ █ █ █ █ █ █ █ █ █ █ ▐██ ", - " ▐█ █ █ █ █ █ █ █ █ ▐██ █ █ █ █ █ █ █ █ ", - " █ █ ▐██ █ █ █ █ █ █ █ █ █ █▐ █ █ █ █ █ █ █ ", - " ▐█▌ █ █ █ ▐█▐▌▐█▌ ▐█▌ ▐█▐▌ ▐▌ ▐█▌ ▐█▌ █ █ ▐█▐▌ ", - " ██▌ ", - " ", - " Login ", - " ──────────────────────────────────────────────── ", - "  󰊤 GitHub ", - " 󰊭 Google ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " ", - " j/k:󰹹 Ent:󰏌 q: ", - ], - styles: [ - x: 0, y: 0, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 36, y: 7, fg: White, bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 84, y: 7, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 36, y: 8, fg: White, bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 84, y: 8, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 36, y: 9, fg: White, bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 84, y: 9, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 36, y: 10, fg: White, bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 84, y: 10, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 36, y: 11, fg: White, bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 84, y: 11, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 36, y: 12, fg: White, bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 84, y: 12, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 36, y: 13, fg: White, bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 84, y: 13, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 36, y: 14, fg: White, bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 84, y: 14, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 58, y: 16, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: BOLD, - x: 63, y: 16, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 36, y: 18, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: BOLD, - x: 84, y: 18, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 51, y: 29, fg: Rgb(111, 93, 99), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 70, y: 29, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - ] -} diff --git a/crates/synd_term/tests/snapshots/integration__test__happy.snap b/crates/synd_term/tests/snapshots/integration__test__initial_login.snap similarity index 51% rename from crates/synd_term/tests/snapshots/integration__test__happy.snap rename to crates/synd_term/tests/snapshots/integration__test__initial_login.snap index 8e8ed931..c370d0ab 100644 --- a/crates/synd_term/tests/snapshots/integration__test__happy.snap +++ b/crates/synd_term/tests/snapshots/integration__test__initial_login.snap @@ -1,5 +1,6 @@ --- source: crates/synd_term/tests/integration.rs +description: initial login prompt expression: application.buffer() --- Buffer { @@ -12,19 +13,7 @@ Buffer { " ", " ", " ", - " ▐█▌ ▐█ █ ▐ █ ▐█ ", - " █ █ █ █ █ ", - " █▌ █ █ ██▌ █ ▐█ ▐█▌ ▐█▌ ▐██ ▐█ ▐█▌ ██▌ █ ", - " ▐█ █ █ █ █ ▐██ █ █ █ █ █ █ █ █ █ █ ▐██ ", - " ▐█ █ █ █ █ █ █ █ █ ▐██ █ █ █ █ █ █ █ █ ", - " █ █ ▐██ █ █ █ █ █ █ █ █ █ █▐ █ █ █ █ █ █ █ ", - " ▐█▌ █ █ █ ▐█▐▌▐█▌ ▐█▌ ▐█▐▌ ▐▌ ▐█▌ ▐█▌ █ █ ▐█▐▌ ", - " ██▌ ", " ", - " Login ", - " ──────────────────────────────────────────────── ", - "  󰊤 GitHub ", - " 󰊭 Google ", " ", " ", " ", @@ -34,15 +23,21 @@ Buffer { " ", " ", " ", - " j/k:󰹹 Ent:󰏌 q: ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", ], styles: [ - x: 0, y: 0, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 58, y: 16, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: BOLD, - x: 63, y: 16, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 36, y: 18, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: BOLD, - x: 84, y: 18, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 51, y: 29, fg: Rgb(111, 93, 99), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, - x: 70, y: 29, fg: Rgb(254, 205, 178), bg: Rgb(43, 41, 45), underline: Reset, modifier: NONE, + x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE, ] } diff --git a/crates/synd_term/tests/snapshots/integration__test__happy-2.snap b/crates/synd_term/tests/snapshots/integration__test__landing_entries.snap similarity index 99% rename from crates/synd_term/tests/snapshots/integration__test__happy-2.snap rename to crates/synd_term/tests/snapshots/integration__test__landing_entries.snap index f0226ee3..8b0a7ccd 100644 --- a/crates/synd_term/tests/snapshots/integration__test__happy-2.snap +++ b/crates/synd_term/tests/snapshots/integration__test__landing_entries.snap @@ -1,5 +1,6 @@ --- source: crates/synd_term/tests/integration.rs +description: initial entries expression: application.buffer() --- Buffer { diff --git a/crates/synd_term/tests/test/helper.rs b/crates/synd_term/tests/test/helper.rs index 087208a9..fbce3f00 100644 --- a/crates/synd_term/tests/test/helper.rs +++ b/crates/synd_term/tests/test/helper.rs @@ -9,8 +9,90 @@ use synd_api::{ repository::kvsd::KvsdClient, shutdown::Shutdown, }; -use synd_term::terminal::Terminal; +use synd_auth::device_flow::{provider, DeviceFlow}; +use synd_term::{ + application::{Application, Authenticator, Config, DeviceFlows}, + client::Client, + config::Categories, + terminal::Terminal, + ui::theme::Theme, +}; use tokio::net::{TcpListener, TcpStream}; +use tracing_subscriber::EnvFilter; + +#[derive(Clone)] +pub struct TestCase { + pub oauth_provider_port: u16, + pub synd_api_port: u16, + pub kvsd_port: u16, + pub terminal_col_row: (u16, u16), + pub device_flow_case: &'static str, +} + +impl TestCase { + pub async fn init_app(&self) -> anyhow::Result { + let TestCase { + oauth_provider_port, + synd_api_port, + kvsd_port, + terminal_col_row: (term_col, term_row), + device_flow_case, + } = self.clone(); + + // Start mock oauth server + { + let addr = ("127.0.0.1", oauth_provider_port); + let listener = TcpListener::bind(addr).await?; + tokio::spawn(synd_test::mock::serve(listener)); + } + + // Start synd api server + { + serve_api(oauth_provider_port, synd_api_port, kvsd_port).await?; + } + + // Configure application + let application = { + let endpoint = format!("https://localhost:{synd_api_port}/graphql") + .parse() + .unwrap(); + let terminal = new_test_terminal(term_col, term_row); + let client = Client::new(endpoint, Duration::from_secs(10)).unwrap(); + let device_flows = DeviceFlows { + github: DeviceFlow::new( + provider::Github::new("dummy") + .with_device_authorization_endpoint(format!( + "http://localhost:{oauth_provider_port}/{device_flow_case}/github/login/device/code", + )) + .with_token_endpoint( + format!("http://localhost:{oauth_provider_port}/{device_flow_case}/github/login/oauth/access_token"), + ), + ), + google: DeviceFlow::new(provider::Google::new("dummy", "dummy")), + }; + let authenticator = Authenticator::new().with_device_flows(device_flows); + let config = Config { + idle_timer_interval: Duration::from_millis(1000), + throbber_timer_interval: Duration::from_secs(3600), // disable throbber + ..Default::default() + }; + Application::with(terminal, client, Categories::default_toml(), config) + .with_theme(Theme::default()) + .with_authenticator(authenticator) + }; + + Ok(application) + } +} + +pub fn init_tracing() { + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_line_number(true) + .with_file(true) + .with_target(false) + .init(); +} pub fn new_test_terminal(width: u16, height: u16) -> Terminal { let backend = TestBackend::new(width, height); @@ -18,10 +100,14 @@ pub fn new_test_terminal(width: u16, height: u16) -> Terminal { Terminal::with(terminal) } -pub async fn serve_api(mock_port: u16, api_port: u16) -> anyhow::Result<()> { +pub async fn serve_api( + oauth_provider_port: u16, + api_port: u16, + kvsd_port: u16, +) -> anyhow::Result<()> { let kvsd_options = KvsdOptions { kvsd_host: "localhost".into(), - kvsd_port: 47379, + kvsd_port, kvsd_username: "test".into(), kvsd_password: "test".into(), }; @@ -58,7 +144,7 @@ pub async fn serve_api(mock_port: u16, api_port: u16) -> anyhow::Result<()> { { let github_endpoint: &'static str = - format!("http://localhost:{mock_port}/github/graphql").leak(); + format!("http://localhost:{oauth_provider_port}/github/graphql").leak(); let github_client = GithubClient::new()?.with_endpoint(github_endpoint); dep.authenticator = dep.authenticator.with_client(github_client);