mirror of
https://github.com/apognu/tuigreet.git
synced 2024-09-11 07:25:29 +03:00
Added infrastructure to do integration testing.
This commit is contained in:
parent
71cf19e233
commit
c5fa133170
128
Cargo.lock
generated
128
Cargo.lock
generated
@ -97,12 +97,6 @@ dependencies = [
|
||||
"rustc-demangle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.5.0"
|
||||
@ -253,7 +247,7 @@ version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"futures-core",
|
||||
"libc",
|
||||
@ -353,6 +347,22 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
|
||||
[[package]]
|
||||
name = "find-crate"
|
||||
version = "0.6.3"
|
||||
@ -531,6 +541,20 @@ version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
|
||||
[[package]]
|
||||
name = "greetd-stub"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5813b31008b93f6a134866a206c5e383edb1257e85891f6b7b5e942a611d2c68"
|
||||
dependencies = [
|
||||
"getopts",
|
||||
"greetd_ipc",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greetd_ipc"
|
||||
version = "0.10.0"
|
||||
@ -546,9 +570,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.3"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
@ -726,9 +750,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
version = "0.2.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
||||
|
||||
[[package]]
|
||||
name = "locale_config"
|
||||
@ -745,9 +775,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
|
||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
@ -810,7 +840,7 @@ version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@ -913,9 +943,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
@ -923,15 +953,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.9"
|
||||
version = "0.9.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
|
||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1048,7 +1078,7 @@ version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a564a852040e82671dc50a37d88f3aa83bbc690dfc6844cfe7a2591620206a80"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
@ -1064,11 +1094,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
|
||||
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1157,6 +1187,19 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.15"
|
||||
@ -1201,18 +1244,18 @@ checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.198"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
|
||||
checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.198"
|
||||
version = "1.0.199"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
|
||||
checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1323,9 +1366,9 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.6"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
|
||||
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
@ -1396,6 +1439,18 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.1"
|
||||
@ -1641,6 +1696,7 @@ dependencies = [
|
||||
"crossterm",
|
||||
"futures",
|
||||
"getopts",
|
||||
"greetd-stub",
|
||||
"greetd_ipc",
|
||||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
@ -1651,12 +1707,14 @@ dependencies = [
|
||||
"rust-embed",
|
||||
"rust-ini",
|
||||
"smart-default",
|
||||
"tempfile",
|
||||
"textwrap",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"unic-langid",
|
||||
"unicode-width",
|
||||
"uzers",
|
||||
"zeroize",
|
||||
]
|
||||
@ -1715,9 +1773,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.11"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
|
||||
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
|
||||
|
||||
[[package]]
|
||||
name = "uzers"
|
||||
@ -1829,9 +1887,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "134306a13c5647ad6453e8deaec55d3a44d6021970129e6188735e74bf546697"
|
||||
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@ -1992,9 +2050,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.6"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
|
||||
checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -47,3 +47,8 @@ tracing = "0.1.40"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
[dev-dependencies]
|
||||
greetd-stub = "0.3.0"
|
||||
tempfile = "3.10.1"
|
||||
unicode-width = "0.1.12"
|
||||
|
12
src/event.rs
12
src/event.rs
@ -1,12 +1,15 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm::event::{Event as TermEvent, EventStream, KeyEvent};
|
||||
use crossterm::event::{Event as TermEvent, KeyEvent};
|
||||
use futures::{future::FutureExt, StreamExt};
|
||||
use tokio::{
|
||||
process::Command,
|
||||
sync::mpsc::{self, Sender},
|
||||
};
|
||||
|
||||
#[cfg(not(test))]
|
||||
use crossterm::event::EventStream;
|
||||
|
||||
use crate::AuthStatus;
|
||||
|
||||
const FRAME_RATE: f64 = 2.0;
|
||||
@ -31,7 +34,14 @@ impl Events {
|
||||
let tx = tx.clone();
|
||||
|
||||
async move {
|
||||
#[cfg(not(test))]
|
||||
let mut stream = EventStream::new();
|
||||
|
||||
// In tests, we are not capturing events from the terminal, so we need
|
||||
// to replace the crossterm::EventStream with a dummy pending stream.
|
||||
#[cfg(test)]
|
||||
let mut stream = futures::stream::pending::<Result<TermEvent, ()>>();
|
||||
|
||||
let mut render_interval = tokio::time::interval(Duration::from_secs_f64(1.0 / FRAME_RATE));
|
||||
|
||||
loop {
|
||||
|
@ -149,6 +149,10 @@ pub struct Greeter {
|
||||
|
||||
// Style object for the terminal UI
|
||||
pub theme: Theme,
|
||||
// Display the current time
|
||||
pub time: bool,
|
||||
// Time format
|
||||
pub time_format: Option<String>,
|
||||
// Greeting message (MOTD) to use to welcome the user.
|
||||
pub greeting: Option<String>,
|
||||
// Transaction message to show to the user.
|
||||
@ -193,6 +197,7 @@ impl Greeter {
|
||||
selected: 0,
|
||||
};
|
||||
|
||||
#[cfg(not(test))]
|
||||
greeter.parse_options().await;
|
||||
|
||||
greeter.sessions = Menu {
|
||||
@ -464,11 +469,15 @@ impl Greeter {
|
||||
self.secret_display = SecretDisplay::Character(asterisk);
|
||||
}
|
||||
|
||||
self.time = self.config().opt_present("time");
|
||||
|
||||
if let Some(format) = self.config().opt_str("time-format") {
|
||||
if StrftimeItems::new(&format).any(|item| item == Item::Error) {
|
||||
eprintln!("Invalid strftime format provided in --time-format");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
self.time_format = Some(format);
|
||||
}
|
||||
|
||||
if self.config().opt_present("user-menu") {
|
||||
|
112
src/integration/auth.rs
Normal file
112
src/integration/auth.rs
Normal file
@ -0,0 +1,112 @@
|
||||
use libgreetd_stub::SessionOptions;
|
||||
|
||||
use super::common::IntegrationRunner;
|
||||
|
||||
#[tokio::test]
|
||||
async fn authentication_ok() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_until_buffer_contains("Username:").await;
|
||||
runner.send_text("apognu").await;
|
||||
runner.wait_until_buffer_contains("Password:").await;
|
||||
runner.send_text("password").await;
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_client_exit(events).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authentication_bad_password() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
{
|
||||
async move {
|
||||
runner.wait_until_buffer_contains("Username:").await;
|
||||
runner.send_text("apognu").await;
|
||||
runner.wait_until_buffer_contains("Password:").await;
|
||||
runner.send_text("password2").await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Authentication failed"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_end(events).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authentication_ok_mfa() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: true,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_until_buffer_contains("Username:").await;
|
||||
runner.send_text("apognu").await;
|
||||
runner.wait_until_buffer_contains("Password:").await;
|
||||
runner.send_text("password").await;
|
||||
runner.wait_until_buffer_contains("7 + 2 =").await;
|
||||
runner.send_text("9").await;
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_client_exit(events).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authentication_bad_mfa() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: true,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_until_buffer_contains("Username:").await;
|
||||
runner.send_text("apognu").await;
|
||||
runner.wait_until_buffer_contains("Password:").await;
|
||||
runner.send_text("password").await;
|
||||
runner.wait_until_buffer_contains("7 + 2 = ").await;
|
||||
runner.send_text("10").await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Authentication failed"));
|
||||
assert!(runner.output().await.contains("Password:"));
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_end(events).await;
|
||||
}
|
214
src/integration/common/backend.rs
Normal file
214
src/integration/common/backend.rs
Normal file
@ -0,0 +1,214 @@
|
||||
#![allow(unused_must_use)]
|
||||
|
||||
/*
|
||||
Copied and adapted from the codebase of ratatui.
|
||||
|
||||
Repository: https://github.com/ratatui-org/ratatui
|
||||
License: https://github.com/ratatui-org/ratatui/blob/main/LICENSE
|
||||
File: https://github.com/ratatui-org/ratatui/blob/f4637d40c35e068fd60d17c9a42b9114667c9861/src/backend/test.rs
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2022 Florian Dehau
|
||||
Copyright (c) 2023-2024 The Ratatui Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
use std::{
|
||||
fmt::Write,
|
||||
io,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use tokio::sync::mpsc;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use tui::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::{Buffer, Cell},
|
||||
layout::{Rect, Size},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestBackend {
|
||||
tick: mpsc::Sender<bool>,
|
||||
width: u16,
|
||||
buffer: Arc<Mutex<Buffer>>,
|
||||
height: u16,
|
||||
cursor: bool,
|
||||
pos: (u16, u16),
|
||||
}
|
||||
|
||||
pub fn output(buffer: &Arc<Mutex<Buffer>>) -> String {
|
||||
let buffer = buffer.lock().unwrap();
|
||||
|
||||
let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
|
||||
for cells in buffer.content.chunks(buffer.area.width as usize) {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
view.push('"');
|
||||
for (x, c) in cells.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
view.push_str(c.symbol());
|
||||
} else {
|
||||
overwritten.push((x, c.symbol()));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol().width()).saturating_sub(1);
|
||||
}
|
||||
view.push('"');
|
||||
if !overwritten.is_empty() {
|
||||
write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap();
|
||||
}
|
||||
view.push('\n');
|
||||
}
|
||||
view
|
||||
}
|
||||
|
||||
impl TestBackend {
|
||||
pub fn new(width: u16, height: u16) -> (Self, Arc<Mutex<Buffer>>, mpsc::Receiver<bool>) {
|
||||
let buffer = Arc::new(Mutex::new(Buffer::empty(Rect::new(0, 0, width, height))));
|
||||
let (tx, rx) = mpsc::channel::<bool>(10);
|
||||
|
||||
let backend = Self {
|
||||
tick: tx,
|
||||
width,
|
||||
height,
|
||||
buffer: buffer.clone(),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
};
|
||||
|
||||
(backend, buffer, rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TestBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut buffer = self.buffer.lock().unwrap();
|
||||
|
||||
for (x, y, c) in content {
|
||||
let cell = buffer.get_mut(x, y);
|
||||
*cell = c.clone();
|
||||
}
|
||||
|
||||
let sender = self.tick.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
sender.blocking_send(true);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
Ok(self.pos)
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.buffer.lock().unwrap().reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: tui::backend::ClearType) -> io::Result<()> {
|
||||
let buffer = self.buffer.clone();
|
||||
let mut buffer = buffer.lock().unwrap();
|
||||
|
||||
match clear_type {
|
||||
ClearType::All => self.clear()?,
|
||||
ClearType::AfterCursor => {
|
||||
let index = buffer.index_of(self.pos.0, self.pos.1) + 1;
|
||||
buffer.content[index..].fill(Cell::default());
|
||||
}
|
||||
ClearType::BeforeCursor => {
|
||||
let index = buffer.index_of(self.pos.0, self.pos.1);
|
||||
buffer.content[..index].fill(Cell::default());
|
||||
}
|
||||
ClearType::CurrentLine => {
|
||||
let line_start_index = buffer.index_of(0, self.pos.1);
|
||||
let line_end_index = buffer.index_of(self.width - 1, self.pos.1);
|
||||
buffer.content[line_start_index..=line_end_index].fill(Cell::default());
|
||||
}
|
||||
ClearType::UntilNewLine => {
|
||||
let index = buffer.index_of(self.pos.0, self.pos.1);
|
||||
let line_end_index = buffer.index_of(self.width - 1, self.pos.1);
|
||||
buffer.content[index..=line_end_index].fill(Cell::default());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
let (cur_x, cur_y) = self.get_cursor()?;
|
||||
|
||||
let new_cursor_x = cur_x.saturating_add(1).min(self.width.saturating_sub(1));
|
||||
|
||||
let max_y = self.height.saturating_sub(1);
|
||||
let lines_after_cursor = max_y.saturating_sub(cur_y);
|
||||
if n > lines_after_cursor {
|
||||
let rotate_by = n.saturating_sub(lines_after_cursor).min(max_y);
|
||||
|
||||
if rotate_by == self.height - 1 {
|
||||
self.clear()?;
|
||||
}
|
||||
|
||||
self.set_cursor(0, rotate_by)?;
|
||||
self.clear_region(ClearType::BeforeCursor)?;
|
||||
self.buffer.lock().unwrap().content.rotate_left((self.width * rotate_by).into());
|
||||
}
|
||||
|
||||
let new_cursor_y = cur_y.saturating_add(n).min(max_y);
|
||||
self.set_cursor(new_cursor_x, new_cursor_y)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
static WINDOW_PIXEL_SIZE: Size = Size { width: 640, height: 480 };
|
||||
Ok(WindowSize {
|
||||
columns_rows: (self.width, self.height).into(),
|
||||
pixels: WINDOW_PIXEL_SIZE,
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
173
src/integration/common/mod.rs
Normal file
173
src/integration/common/mod.rs
Normal file
@ -0,0 +1,173 @@
|
||||
mod backend;
|
||||
|
||||
use std::{
|
||||
panic,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use libgreetd_stub::SessionOptions;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::{Receiver, Sender},
|
||||
RwLock,
|
||||
},
|
||||
task::{JoinError, JoinHandle},
|
||||
};
|
||||
use tui::buffer::Buffer;
|
||||
|
||||
use crate::{
|
||||
event::{Event, Events},
|
||||
ui::sessions::SessionSource,
|
||||
Greeter,
|
||||
};
|
||||
|
||||
pub use self::backend::{output, TestBackend};
|
||||
|
||||
pub struct IntegrationRunner(Arc<RwLock<_IntegrationRunner>>);
|
||||
|
||||
struct _IntegrationRunner {
|
||||
server: Option<JoinHandle<()>>,
|
||||
client: Option<JoinHandle<()>>,
|
||||
|
||||
pub buffer: Arc<Mutex<Buffer>>,
|
||||
pub sender: Sender<Event>,
|
||||
pub tick: Receiver<bool>,
|
||||
}
|
||||
|
||||
impl Clone for IntegrationRunner {
|
||||
fn clone(&self) -> Self {
|
||||
IntegrationRunner(Arc::clone(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntegrationRunner {
|
||||
pub async fn new(opts: SessionOptions, builder: Option<fn(&mut Greeter)>) -> IntegrationRunner {
|
||||
let socket = NamedTempFile::new().unwrap().into_temp_path().to_path_buf();
|
||||
|
||||
let (backend, buffer, tick) = TestBackend::new(200, 200);
|
||||
let events = Events::new().await;
|
||||
let sender = events.sender();
|
||||
|
||||
let server = tokio::task::spawn({
|
||||
let socket = socket.clone();
|
||||
|
||||
async move {
|
||||
libgreetd_stub::start(&socket, &opts).await;
|
||||
}
|
||||
});
|
||||
|
||||
let client = tokio::task::spawn(async move {
|
||||
let mut greeter = Greeter::new(events.sender()).await;
|
||||
greeter.session_source = SessionSource::Command("uname".to_string());
|
||||
|
||||
if let Some(builder) = builder {
|
||||
builder(&mut greeter);
|
||||
}
|
||||
|
||||
if greeter.config.is_none() {
|
||||
greeter.config = Greeter::options().parse(&[""]).ok();
|
||||
}
|
||||
|
||||
greeter.logfile = "/tmp/tuigreet.log".to_string();
|
||||
greeter.socket = socket.to_str().unwrap().to_string();
|
||||
greeter.events = Some(events.sender());
|
||||
greeter.connect().await;
|
||||
|
||||
let _ = crate::run(backend, greeter, events).await;
|
||||
});
|
||||
|
||||
IntegrationRunner(Arc::new(RwLock::new(_IntegrationRunner {
|
||||
server: Some(server),
|
||||
client: Some(client),
|
||||
buffer,
|
||||
sender,
|
||||
tick,
|
||||
})))
|
||||
}
|
||||
|
||||
pub async fn join_until_client_exit(&mut self, mut events: JoinHandle<()>) {
|
||||
let (mut server, mut client) = {
|
||||
let mut runner = self.0.write().await;
|
||||
|
||||
(runner.server.take().unwrap(), runner.client.take().unwrap())
|
||||
};
|
||||
|
||||
let mut exited = false;
|
||||
|
||||
while !exited {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => break,
|
||||
_ = (&mut server) => {}
|
||||
_ = (&mut client) => { exited = true; },
|
||||
ret = (&mut events), if !events.is_finished() => rethrow(ret),
|
||||
}
|
||||
}
|
||||
|
||||
assert!(exited, "tuigreet did not exit");
|
||||
}
|
||||
|
||||
pub async fn join_until_end(&mut self, events: JoinHandle<()>) {
|
||||
let (server, client) = {
|
||||
let mut runner = self.0.write().await;
|
||||
|
||||
(runner.server.take().unwrap(), runner.client.take().unwrap())
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => {},
|
||||
_ = server => {}
|
||||
_ = client => {},
|
||||
ret = events => rethrow(ret),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub async fn wait_until_buffer_contains(&mut self, needle: &str) {
|
||||
loop {
|
||||
if output(&self.0.read().await.buffer).contains(needle) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.wait_for_render().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused, unused_must_use)]
|
||||
pub async fn send_key(&self, key: KeyCode) {
|
||||
self.0.write().await.sender.send(Event::Key(KeyEvent::new(key, KeyModifiers::empty()))).await;
|
||||
}
|
||||
|
||||
#[allow(unused, unused_must_use)]
|
||||
pub async fn send_modified_key(&self, key: KeyCode, modifiers: KeyModifiers) {
|
||||
self.0.write().await.sender.send(Event::Key(KeyEvent::new(key, modifiers))).await;
|
||||
}
|
||||
|
||||
#[allow(unused, unused_must_use)]
|
||||
pub async fn send_text(&self, text: &str) {
|
||||
for char in text.chars() {
|
||||
self.0.write().await.sender.send(Event::Key(KeyEvent::new(KeyCode::Char(char), KeyModifiers::empty()))).await;
|
||||
}
|
||||
|
||||
self.0.write().await.sender.send(Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()))).await;
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub async fn wait_for_render(&mut self) {
|
||||
self.0.write().await.tick.recv().await;
|
||||
}
|
||||
|
||||
pub async fn output(&self) -> String {
|
||||
output(&self.0.read().await.buffer)
|
||||
}
|
||||
}
|
||||
|
||||
fn rethrow(result: Result<(), JoinError>) {
|
||||
if let Err(err) = result {
|
||||
if let Ok(panick) = err.try_into_panic() {
|
||||
panic::resume_unwind(panick);
|
||||
}
|
||||
}
|
||||
}
|
79
src/integration/display.rs
Normal file
79
src/integration/display.rs
Normal file
@ -0,0 +1,79 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Local;
|
||||
use libgreetd_stub::SessionOptions;
|
||||
|
||||
use super::common::IntegrationRunner;
|
||||
|
||||
#[tokio::test]
|
||||
async fn show_greet() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(
|
||||
opts,
|
||||
Some(|greeter| {
|
||||
greeter.greeting = Some("Lorem ipsum dolor sit amet".to_string());
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Lorem ipsum dolor sit amet"));
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_end(events).await;
|
||||
}
|
||||
|
||||
const TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S";
|
||||
|
||||
// TODO
|
||||
// This could create a race condition if we do not mock time, because we rely on
|
||||
// being at the same second between the test instantiation and the tasks
|
||||
// running, which is not guaranteed.
|
||||
#[tokio::test]
|
||||
async fn show_time() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let tref = Local::now().format(&TIME_FORMAT).to_string();
|
||||
|
||||
let mut runner = IntegrationRunner::new(
|
||||
opts,
|
||||
Some(|greeter| {
|
||||
greeter.time = true;
|
||||
greeter.time_format = Some(TIME_FORMAT.to_string());
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains(&tref));
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert_eq!(runner.output().await.contains(&tref), false);
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_end(events).await;
|
||||
}
|
26
src/integration/exit.rs
Normal file
26
src/integration/exit.rs
Normal file
@ -0,0 +1,26 @@
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use libgreetd_stub::SessionOptions;
|
||||
|
||||
use super::common::IntegrationRunner;
|
||||
|
||||
#[tokio::test]
|
||||
async fn exit() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.send_modified_key(KeyCode::Char('x'), KeyModifiers::CONTROL).await;
|
||||
runner.wait_for_render().await;
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_client_exit(events).await;
|
||||
}
|
233
src/integration/menus.rs
Normal file
233
src/integration/menus.rs
Normal file
@ -0,0 +1,233 @@
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use libgreetd_stub::SessionOptions;
|
||||
|
||||
use crate::{
|
||||
power::PowerOption,
|
||||
ui::{common::menu::Menu, power::Power, sessions::Session, users::User},
|
||||
};
|
||||
|
||||
use super::common::IntegrationRunner;
|
||||
|
||||
#[tokio::test]
|
||||
async fn change_command() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_until_buffer_contains("Username:").await;
|
||||
runner.send_key(KeyCode::F(3)).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("CMD uname"));
|
||||
|
||||
runner.send_key(KeyCode::F(2)).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Change session command"));
|
||||
assert!(runner.output().await.contains("New command: uname"));
|
||||
|
||||
runner.send_modified_key(KeyCode::Char('u'), KeyModifiers::CONTROL).await;
|
||||
runner.send_text("mynewcommand").await;
|
||||
runner.send_key(KeyCode::Enter).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("CMD mynewcommand"));
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_end(events).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_menu() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(
|
||||
opts,
|
||||
Some(|greeter| {
|
||||
greeter.sessions = Menu::<Session> {
|
||||
title: "List of sessions".to_string(),
|
||||
options: vec![
|
||||
Session {
|
||||
name: "My Session".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Session {
|
||||
name: "Second Session".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
selected: 0,
|
||||
};
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_until_buffer_contains("Username:").await;
|
||||
runner.send_key(KeyCode::F(3)).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("List of sessions"));
|
||||
assert!(runner.output().await.contains("My Session"));
|
||||
assert!(runner.output().await.contains("Second Session"));
|
||||
|
||||
runner.send_key(KeyCode::Down).await;
|
||||
runner.send_key(KeyCode::Down).await;
|
||||
runner.send_key(KeyCode::Enter).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("CMD Second Session"));
|
||||
|
||||
runner.send_key(KeyCode::F(3)).await;
|
||||
runner.wait_for_render().await;
|
||||
runner.send_key(KeyCode::Up).await;
|
||||
runner.send_key(KeyCode::Up).await;
|
||||
runner.send_key(KeyCode::Enter).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("CMD My Session"));
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_end(events).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn power_menu() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(
|
||||
opts,
|
||||
Some(|greeter| {
|
||||
greeter.powers = Menu::<Power> {
|
||||
title: "What to do?".to_string(),
|
||||
options: vec![
|
||||
Power {
|
||||
action: PowerOption::Shutdown,
|
||||
label: "Turn it off".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Power {
|
||||
action: PowerOption::Reboot,
|
||||
label: "And back on again".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
selected: 0,
|
||||
};
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_until_buffer_contains("Username:").await;
|
||||
runner.send_key(KeyCode::F(12)).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("What to do?"));
|
||||
assert!(runner.output().await.contains("Turn it off"));
|
||||
assert!(runner.output().await.contains("And back on again"));
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_end(events).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn users_menu() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(
|
||||
opts,
|
||||
Some(|greeter| {
|
||||
greeter.user_menu = true;
|
||||
greeter.users = Menu::<User> {
|
||||
title: "The users".to_string(),
|
||||
options: vec![
|
||||
User {
|
||||
username: "apognu".to_string(),
|
||||
name: Some("Antoine POPINEAU".to_string()),
|
||||
},
|
||||
User {
|
||||
username: "bob".to_string(),
|
||||
name: Some("Bob JOE".to_string()),
|
||||
},
|
||||
],
|
||||
selected: 0,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_until_buffer_contains("select a user").await;
|
||||
|
||||
runner.send_key(KeyCode::Enter).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Antoine POPINEAU"));
|
||||
assert!(runner.output().await.contains("Bob JOE"));
|
||||
|
||||
runner.send_key(KeyCode::Down).await;
|
||||
runner.send_key(KeyCode::Enter).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Username: Bob JOE"));
|
||||
assert!(runner.output().await.contains("Password:"));
|
||||
|
||||
runner.send_key(KeyCode::Esc).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
runner.wait_until_buffer_contains("select a user").await;
|
||||
|
||||
runner.send_text("otheruser").await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Username: otheruser"));
|
||||
assert!(runner.output().await.contains("Password:"));
|
||||
|
||||
runner.send_key(KeyCode::Esc).await;
|
||||
runner.send_key(KeyCode::Enter).await;
|
||||
runner.send_key(KeyCode::Up).await;
|
||||
runner.send_key(KeyCode::Enter).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Username: Antoine POPINEAU"));
|
||||
assert!(runner.output().await.contains("Password:"));
|
||||
|
||||
runner.send_text("password").await;
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_client_exit(events).await;
|
||||
}
|
8
src/integration/mod.rs
Normal file
8
src/integration/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
mod common;
|
||||
|
||||
mod auth;
|
||||
mod display;
|
||||
mod exit;
|
||||
mod menus;
|
||||
mod movement;
|
||||
mod remember;
|
49
src/integration/movement.rs
Normal file
49
src/integration/movement.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use libgreetd_stub::SessionOptions;
|
||||
|
||||
use super::common::IntegrationRunner;
|
||||
|
||||
#[tokio::test]
|
||||
async fn keyboard_movement() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(opts, None).await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_until_buffer_contains("Username:").await;
|
||||
for char in "apognu".chars() {
|
||||
runner.send_key(KeyCode::Char(char)).await;
|
||||
}
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Username: apognu"));
|
||||
|
||||
runner.send_key(KeyCode::Left).await;
|
||||
runner.send_key(KeyCode::Char('l')).await;
|
||||
runner.send_key(KeyCode::Right).await;
|
||||
runner.send_key(KeyCode::Char('r')).await;
|
||||
runner.send_modified_key(KeyCode::Char('a'), KeyModifiers::CONTROL).await;
|
||||
runner.send_key(KeyCode::Char('a')).await;
|
||||
runner.send_modified_key(KeyCode::Char('e'), KeyModifiers::CONTROL).await;
|
||||
runner.send_key(KeyCode::Char('e')).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Username: aapognlure"));
|
||||
|
||||
runner.send_key(KeyCode::Left).await;
|
||||
runner.send_modified_key(KeyCode::Char('u'), KeyModifiers::CONTROL).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Username: "));
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_end(events).await;
|
||||
}
|
43
src/integration/remember.rs
Normal file
43
src/integration/remember.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use crossterm::event::KeyCode;
|
||||
use libgreetd_stub::SessionOptions;
|
||||
|
||||
use crate::ui::common::masked::MaskedString;
|
||||
|
||||
use super::common::IntegrationRunner;
|
||||
|
||||
#[tokio::test]
|
||||
async fn remember_username() {
|
||||
let opts = SessionOptions {
|
||||
username: "apognu".to_string(),
|
||||
password: "password".to_string(),
|
||||
mfa: false,
|
||||
};
|
||||
|
||||
let mut runner = IntegrationRunner::new(
|
||||
opts,
|
||||
Some(|greeter| {
|
||||
greeter.remember = true;
|
||||
greeter.username = MaskedString::from("apognu".to_string(), None);
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let events = tokio::task::spawn({
|
||||
let mut runner = runner.clone();
|
||||
|
||||
async move {
|
||||
runner.wait_until_buffer_contains("Username:").await;
|
||||
|
||||
assert!(runner.output().await.contains("Username: apognu"));
|
||||
|
||||
runner.wait_until_buffer_contains("Password:").await;
|
||||
runner.send_key(KeyCode::Esc).await;
|
||||
runner.wait_for_render().await;
|
||||
|
||||
assert!(runner.output().await.contains("Username: "));
|
||||
assert!(!runner.output().await.contains("Password:"));
|
||||
}
|
||||
});
|
||||
|
||||
runner.join_until_end(events).await;
|
||||
}
|
37
src/main.rs
37
src/main.rs
@ -12,11 +12,14 @@ mod keyboard;
|
||||
mod power;
|
||||
mod ui;
|
||||
|
||||
#[cfg(test)]
|
||||
mod integration;
|
||||
|
||||
use std::{error::Error, fs::OpenOptions, io, process, sync::Arc};
|
||||
|
||||
use crossterm::{
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
terminal::{disable_raw_mode, LeaveAlternateScreen},
|
||||
};
|
||||
use event::Event;
|
||||
use greetd_ipc::Request;
|
||||
@ -24,12 +27,19 @@ use tokio::sync::RwLock;
|
||||
use tracing_appender::non_blocking::WorkerGuard;
|
||||
use tui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
#[cfg(not(test))]
|
||||
use crossterm::terminal::{enable_raw_mode, EnterAlternateScreen};
|
||||
|
||||
pub use self::greeter::*;
|
||||
use self::{event::Events, ipc::Ipc};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
if let Err(error) = run().await {
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
let events = Events::new().await;
|
||||
let greeter = Greeter::new(events.sender()).await;
|
||||
|
||||
if let Err(error) = run(backend, greeter, events).await {
|
||||
if let Some(AuthStatus::Success) = error.downcast_ref::<AuthStatus>() {
|
||||
return;
|
||||
}
|
||||
@ -38,23 +48,25 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
async fn run() -> Result<(), Box<dyn Error>> {
|
||||
let mut events = Events::new().await;
|
||||
let mut greeter = Greeter::new(events.sender()).await;
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
async fn run<B>(backend: B, mut greeter: Greeter, mut events: Events) -> Result<(), Box<dyn Error>>
|
||||
where
|
||||
B: tui::backend::Backend,
|
||||
{
|
||||
let _guard = init_logger(&greeter);
|
||||
|
||||
tracing::info!("tuigreet started");
|
||||
|
||||
register_panic_handler();
|
||||
|
||||
enable_raw_mode()?;
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
enable_raw_mode()?;
|
||||
execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
}
|
||||
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
#[cfg(not(test))]
|
||||
terminal.clear()?;
|
||||
|
||||
let ipc = Ipc::new();
|
||||
@ -116,7 +128,9 @@ async fn exit(greeter: &mut Greeter, status: AuthStatus) {
|
||||
AuthStatus::Cancel | AuthStatus::Failure => Ipc::cancel(greeter).await,
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
clear_screen();
|
||||
|
||||
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
||||
let _ = disable_raw_mode();
|
||||
|
||||
@ -127,7 +141,9 @@ fn register_panic_handler() {
|
||||
let hook = std::panic::take_hook();
|
||||
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
#[cfg(not(test))]
|
||||
clear_screen();
|
||||
|
||||
let _ = execute!(io::stdout(), LeaveAlternateScreen);
|
||||
let _ = disable_raw_mode();
|
||||
|
||||
@ -135,6 +151,7 @@ fn register_panic_handler() {
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub fn clear_screen() {
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
|
||||
|
@ -9,6 +9,7 @@ pub mod users;
|
||||
mod util;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
error::Error,
|
||||
io::{self, Write},
|
||||
sync::Arc,
|
||||
@ -17,7 +18,6 @@ use std::{
|
||||
use chrono::prelude::*;
|
||||
use tokio::sync::RwLock;
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
layout::{Alignment, Constraint, Direction, Layout},
|
||||
style::Modifier,
|
||||
text::{Line, Span},
|
||||
@ -39,11 +39,12 @@ const STATUSBAR_INDEX: usize = 3;
|
||||
const STATUSBAR_LEFT_INDEX: usize = 1;
|
||||
const STATUSBAR_RIGHT_INDEX: usize = 2;
|
||||
|
||||
pub(super) type Backend = CrosstermBackend<io::Stdout>;
|
||||
pub(super) type Term = Terminal<Backend>;
|
||||
pub(super) type Frame<'a> = CrosstermFrame<'a>;
|
||||
|
||||
pub async fn draw(greeter: Arc<RwLock<Greeter>>, terminal: &mut Term) -> Result<(), Box<dyn Error>> {
|
||||
pub async fn draw<B>(greeter: Arc<RwLock<Greeter>>, terminal: &mut Terminal<B>) -> Result<(), Box<dyn Error>>
|
||||
where
|
||||
B: tui::backend::Backend,
|
||||
{
|
||||
let mut greeter = greeter.write().await;
|
||||
let hide_cursor = should_hide_cursor(&greeter);
|
||||
|
||||
@ -64,7 +65,7 @@ pub async fn draw(greeter: Arc<RwLock<Greeter>>, terminal: &mut Term) -> Result<
|
||||
)
|
||||
.split(size);
|
||||
|
||||
if greeter.config().opt_present("time") {
|
||||
if greeter.time {
|
||||
let time_text = Span::from(get_time(&greeter));
|
||||
let time = Paragraph::new(time_text).alignment(Alignment::Center).style(theme.of(&[Themed::Time]));
|
||||
|
||||
@ -133,9 +134,9 @@ pub async fn draw(greeter: Arc<RwLock<Greeter>>, terminal: &mut Term) -> Result<
|
||||
}
|
||||
|
||||
fn get_time(greeter: &Greeter) -> String {
|
||||
let format = match greeter.config().opt_str("time-format") {
|
||||
Some(format) => format,
|
||||
None => fl!("date"),
|
||||
let format = match &greeter.time_format {
|
||||
Some(format) => Cow::Borrowed(format),
|
||||
None => Cow::Owned(fl!("date")),
|
||||
};
|
||||
|
||||
Local::now().format_localized(&format, greeter.locale).to_string()
|
||||
|
Loading…
Reference in New Issue
Block a user