diff --git a/Cargo.lock b/Cargo.lock index 646c468d..34c6db15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2046,6 +2046,7 @@ dependencies = [ "rand", "reqwest", "serde", + "serde_json", "sha2", "snapd", "sqlx", diff --git a/Cargo.toml b/Cargo.toml index 020b11d9..22c61b5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ tonic-build = { version = "0.11", features = ["prost"] } [dev-dependencies] cucumber = { version = "0.20.2", features = ["libtest", "tracing"] } +serde_json = "1.0.114" [[test]] @@ -63,3 +64,7 @@ harness = false [[test]] name = "chart" harness = false + +[[test]] +name = "log_level" +harness = false diff --git a/src/features/admin/log_level/interface.rs b/src/features/admin/log_level/interface.rs index 6abc56e8..ddfa6c5b 100644 --- a/src/features/admin/log_level/interface.rs +++ b/src/features/admin/log_level/interface.rs @@ -4,7 +4,8 @@ use log::Level; use serde::{Deserialize, Serialize}; /// The request for setting the log level -#[derive(Copy, Clone, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize)] +#[serde(transparent)] pub struct SetLogLevelRequest { /// The current log level, [`tracing`] doesn't implement [`serde`] traits so // we convert between the two internally. @@ -16,7 +17,7 @@ pub struct SetLogLevelRequest { pub struct SetLogLevelResponse; /// Returns the log level to the caller -#[derive(Copy, Clone, Serialize)] +#[derive(Copy, Clone, Serialize, Deserialize)] pub struct GetLogLevelResponse { /// The current log level, [`tracing`] doesn't implement [`serde`] traits so // we convert between the two internally. diff --git a/tests/features/admin/log-level.feature b/tests/features/admin/log-level.feature new file mode 100644 index 00000000..7f5b597a --- /dev/null +++ b/tests/features/admin/log-level.feature @@ -0,0 +1,19 @@ +Feature: Can retrieve and set the internal log level via the REST endpoint + + Scenario: Espio wants to find out the current log level + Given Espio doesn't know the log level + When Espio asks for the log level + Then Espio gets an answer + + Scenario Outline: Espio wants to set the log level + Given the service's current log level + When Espio requests it changes to + Then the log level is set to + + Examples: + | level | + | error | + | info | + | debug | + | warn | + | trace | diff --git a/tests/helpers/client/log_level.rs b/tests/helpers/client/log_level.rs new file mode 100644 index 00000000..3a49f809 --- /dev/null +++ b/tests/helpers/client/log_level.rs @@ -0,0 +1,45 @@ +use std::str::FromStr; + +use axum::async_trait; +use log::Level; +use ratings::features::admin::log_level::interface::{GetLogLevelResponse, SetLogLevelRequest}; +use reqwest::Url; + +use super::Client; + +#[async_trait] +pub trait LogClient: Client { + fn rest_url(&self) -> Url { + Url::from_str(self.url()) + .unwrap() + .join("/v1/admin/log-level") + .unwrap() + } + + async fn get_log_level( + &self, + ) -> Result> { + Ok(serde_json::from_str( + &reqwest::get(self.rest_url()) + .await? + .error_for_status()? + .text() + .await?, + )?) + } + + async fn set_log_level( + &self, + level: Level, + ) -> Result<(), Box> { + reqwest::Client::new() + .post(self.rest_url()) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&SetLogLevelRequest { level }).unwrap()) + .send() + .await? + .error_for_status_ref()?; + + Ok(()) + } +} diff --git a/tests/helpers/client/mod.rs b/tests/helpers/client/mod.rs index bd9ee928..7dfbbaa1 100644 --- a/tests/helpers/client/mod.rs +++ b/tests/helpers/client/mod.rs @@ -1,10 +1,11 @@ pub mod app; pub mod chart; +pub mod log_level; pub mod user; use std::fmt::Display; -pub use self::{app::AppClient, chart::ChartClient, user::UserClient}; +pub use self::{app::AppClient, chart::ChartClient, log_level::LogClient, user::UserClient}; pub trait Client { fn url(&self) -> &str; @@ -33,3 +34,4 @@ impl Client for TestClient { impl AppClient for TestClient {} impl ChartClient for TestClient {} impl UserClient for TestClient {} +impl LogClient for TestClient {} diff --git a/tests/helpers/mod.rs b/tests/helpers/mod.rs index 7db13580..54fe8144 100644 --- a/tests/helpers/mod.rs +++ b/tests/helpers/mod.rs @@ -1,4 +1,5 @@ #![allow(dead_code)] +#![cfg(test)] pub mod assert; pub mod client; diff --git a/tests/log_level.rs b/tests/log_level.rs new file mode 100644 index 00000000..38f7d907 --- /dev/null +++ b/tests/log_level.rs @@ -0,0 +1,110 @@ +use std::str::FromStr; + +use cucumber::{given, then, when, Parameter, World}; + +use helpers::client::*; +use ratings::utils::Config; + +mod helpers; + +#[derive(Copy, Clone, Eq, PartialEq, Parameter, Debug)] +#[param(name = "level", regex = "info|warn|debug|trace|error")] +pub struct Level(log::Level); + +impl FromStr for Level { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + Ok(Level(log::Level::from_str(s)?)) + } +} + +impl From for Level { + fn from(value: log::Level) -> Self { + Self(value) + } +} + +impl From for log::Level { + fn from(value: Level) -> Self { + value.0 + } +} + +#[derive(Clone, Debug, World)] +#[world(init = Self::new)] +struct LogWorld { + client: TestClient, + current_level: Option, +} + +impl LogWorld { + fn new() -> Self { + let config = Config::load().expect("could not load config"); + let client = TestClient::new(config.socket()); + Self { + client, + current_level: None, + } + } +} + +#[given(expr = "Espio doesn't know the log level")] +fn unknown_level(world: &mut LogWorld) { + world.current_level = None +} + +#[when(expr = "Espio asks for the log level")] +#[given(expr = "the service's current log level")] +async fn get_log_level(world: &mut LogWorld) { + world.current_level = Some( + world + .client + .get_log_level() + .await + .expect("could not get log level") + .level + .into(), + ) +} + +#[when(expr = "Espio requests it changes to {level}")] +async fn set_log_level(world: &mut LogWorld, level: Level) { + world + .client + .set_log_level(level.into()) + .await + .expect("problem setting log level"); +} + +#[then(expr = "Espio gets an answer")] +fn got_any_level(world: &mut LogWorld) { + assert!( + world.current_level.is_some(), + "did not get a valid level from the endpoint" + ); +} + +#[then(expr = "the log level is set to {level}")] +async fn got_expected_level(world: &mut LogWorld, level: Level) { + let post_set_level = world + .client + .get_log_level() + .await + .expect("could not get log level") + .level; + + assert_eq!(level.0, post_set_level) +} + +#[tokio::main] +async fn main() { + dotenvy::from_filename(".env_files/test.env").ok(); + + LogWorld::cucumber() + .repeat_skipped() + .init_tracing() + .max_concurrent_scenarios(1) + .run_and_exit("tests/features/admin/log-level.feature") + .await +}