This is an automated email from the ASF dual-hosted git repository.

piotr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new 3e848424b feat(cli): Add context create/delete CLI commands to 
complete login/logout workflow (#2998)
3e848424b is described below

commit 3e848424bd062a0ca8d1cdaee5dfccf2799c8b9a
Author: Atharva Lade <[email protected]>
AuthorDate: Mon Mar 30 03:14:19 2026 -0500

    feat(cli): Add context create/delete CLI commands to complete login/logout 
workflow (#2998)
    
    Closes #595
---
 Cargo.lock                                         |   1 +
 core/cli/Cargo.toml                                |   4 +
 core/cli/src/args/context.rs                       | 157 +++++++
 core/cli/src/commands/binary_context/common.rs     | 483 ++++++++++++++++++++-
 .../src/commands/binary_context/create_context.rs  |  94 ++++
 .../src/commands/binary_context/delete_context.rs  |  84 ++++
 core/cli/src/commands/binary_context/mod.rs        |   2 +
 core/cli/src/main.rs                               |   9 +
 core/integration/tests/cli/context/common.rs       |   4 +
 core/integration/tests/cli/context/mod.rs          |   2 +
 .../tests/cli/context/test_context_applied.rs      |   4 +-
 .../cli/context/test_context_create_command.rs     | 239 ++++++++++
 .../cli/context/test_context_delete_command.rs     | 224 ++++++++++
 .../tests/cli/context/test_context_list_command.rs |   6 +-
 .../tests/cli/context/test_context_use_command.rs  |   4 +-
 15 files changed, 1305 insertions(+), 12 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index d43b9a2db..d10cb6e5b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5290,6 +5290,7 @@ dependencies = [
  "secrecy",
  "serde",
  "serde_json",
+ "tempfile",
  "thiserror 2.0.18",
  "tokio",
  "toml 1.0.7+spec-1.1.0",
diff --git a/core/cli/Cargo.toml b/core/cli/Cargo.toml
index 2a631cb24..ea3c1fce6 100644
--- a/core/cli/Cargo.toml
+++ b/core/cli/Cargo.toml
@@ -67,3 +67,7 @@ toml = { workspace = true }
 tracing = { workspace = true }
 tracing-appender = { workspace = true }
 tracing-subscriber = { workspace = true, default-features = false }
+
+[dev-dependencies]
+tempfile = { workspace = true }
+tokio = { workspace = true, features = ["macros", "rt"] }
diff --git a/core/cli/src/args/context.rs b/core/cli/src/args/context.rs
index 8de74b76a..1c7d9089c 100644
--- a/core/cli/src/args/context.rs
+++ b/core/cli/src/args/context.rs
@@ -18,6 +18,8 @@
 
 use crate::args::common::ListMode;
 use clap::{Args, Subcommand};
+use iggy_cli::commands::binary_context::common::ContextConfig;
+use iggy_common::ArgsOptional;
 
 #[derive(Debug, Clone, Subcommand)]
 pub(crate) enum ContextAction {
@@ -35,6 +37,29 @@ pub(crate) enum ContextAction {
     ///  iggy context use default
     #[clap(verbatim_doc_comment, visible_alias = "u")]
     Use(ContextUseArgs),
+
+    /// Create a new context
+    ///
+    /// Creates a new named context in the contexts configuration file.
+    /// After creating a context, use 'iggy context use <name>' to activate it.
+    ///
+    /// Examples
+    ///  iggy context create production --transport tcp --tcp-server-address 
10.0.0.1:8090
+    ///  iggy context create dev --transport http --http-api-url 
http://localhost:3000
+    ///  iggy context create local --username iggy --password iggy
+    #[clap(verbatim_doc_comment, visible_alias = "c")]
+    Create(ContextCreateArgs),
+
+    /// Delete an existing context
+    ///
+    /// Removes a named context from the contexts configuration file.
+    /// The 'default' context cannot be deleted. If the deleted context
+    /// was the active context, the active context resets to 'default'.
+    ///
+    /// Examples
+    ///  iggy context delete production
+    #[clap(verbatim_doc_comment, visible_alias = "d")]
+    Delete(ContextDeleteArgs),
 }
 
 #[derive(Debug, Clone, Args)]
@@ -50,3 +75,135 @@ pub(crate) struct ContextUseArgs {
     #[arg(value_parser = clap::value_parser!(String))]
     pub(crate) context_name: String,
 }
+
+#[derive(Debug, Clone, Args)]
+pub(crate) struct ContextCreateArgs {
+    /// Name of the context to create
+    #[arg(value_parser = clap::value_parser!(String))]
+    pub(crate) context_name: String,
+
+    /// The transport to use
+    ///
+    /// Valid values are "quic", "http", "tcp" and "ws".
+    #[clap(verbatim_doc_comment, long)]
+    pub(crate) transport: Option<String>,
+
+    /// The server address for the TCP transport
+    #[clap(long)]
+    pub(crate) tcp_server_address: Option<String>,
+
+    /// The API URL for the HTTP transport
+    #[clap(long)]
+    pub(crate) http_api_url: Option<String>,
+
+    /// The server address for the QUIC transport
+    #[clap(long)]
+    pub(crate) quic_server_address: Option<String>,
+
+    /// Flag to enable TLS for the TCP transport
+    #[clap(long)]
+    pub(crate) tcp_tls_enabled: Option<bool>,
+
+    /// Iggy server username
+    #[clap(short, long)]
+    pub(crate) username: Option<String>,
+
+    /// Iggy server password
+    #[clap(short, long)]
+    pub(crate) password: Option<String>,
+
+    /// Iggy server personal access token
+    #[clap(short, long)]
+    pub(crate) token: Option<String>,
+
+    /// Iggy server personal access token name
+    #[clap(short = 'n', long)]
+    pub(crate) token_name: Option<String>,
+}
+
+impl From<ContextCreateArgs> for ContextConfig {
+    fn from(args: ContextCreateArgs) -> Self {
+        ContextConfig {
+            username: args.username,
+            password: args.password,
+            token: args.token,
+            token_name: args.token_name,
+            iggy: ArgsOptional {
+                transport: args.transport,
+                tcp_server_address: args.tcp_server_address,
+                http_api_url: args.http_api_url,
+                quic_server_address: args.quic_server_address,
+                tcp_tls_enabled: args.tcp_tls_enabled,
+                ..Default::default()
+            },
+            extra: Default::default(),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Args)]
+pub(crate) struct ContextDeleteArgs {
+    /// Name of the context to delete
+    #[arg(value_parser = clap::value_parser!(String))]
+    pub(crate) context_name: String,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn should_convert_create_args_to_context_config() {
+        let args = ContextCreateArgs {
+            context_name: "production".to_string(),
+            transport: Some("tcp".to_string()),
+            tcp_server_address: Some("10.0.0.1:8090".to_string()),
+            http_api_url: None,
+            quic_server_address: None,
+            tcp_tls_enabled: Some(true),
+            username: Some("admin".to_string()),
+            password: Some("secret".to_string()),
+            token: Some("tok123".to_string()),
+            token_name: Some("my-token".to_string()),
+        };
+
+        let config: ContextConfig = args.into();
+
+        assert_eq!(config.username.as_deref(), Some("admin"));
+        assert_eq!(config.password.as_deref(), Some("secret"));
+        assert_eq!(config.token.as_deref(), Some("tok123"));
+        assert_eq!(config.token_name.as_deref(), Some("my-token"));
+        assert_eq!(config.iggy.transport.as_deref(), Some("tcp"));
+        assert_eq!(
+            config.iggy.tcp_server_address.as_deref(),
+            Some("10.0.0.1:8090")
+        );
+        assert!(config.iggy.http_api_url.is_none());
+        assert!(config.iggy.quic_server_address.is_none());
+        assert_eq!(config.iggy.tcp_tls_enabled, Some(true));
+    }
+
+    #[test]
+    fn should_convert_create_args_with_defaults() {
+        let args = ContextCreateArgs {
+            context_name: "minimal".to_string(),
+            transport: None,
+            tcp_server_address: None,
+            http_api_url: None,
+            quic_server_address: None,
+            tcp_tls_enabled: None,
+            username: None,
+            password: None,
+            token: None,
+            token_name: None,
+        };
+
+        let config: ContextConfig = args.into();
+
+        assert!(config.username.is_none());
+        assert!(config.password.is_none());
+        assert!(config.token.is_none());
+        assert!(config.token_name.is_none());
+        assert!(config.iggy.transport.is_none());
+    }
+}
diff --git a/core/cli/src/commands/binary_context/common.rs 
b/core/cli/src/commands/binary_context/common.rs
index 1d96e5ae4..09009b7ef 100644
--- a/core/cli/src/commands/binary_context/common.rs
+++ b/core/cli/src/commands/binary_context/common.rs
@@ -19,8 +19,9 @@
 use anyhow::{Context, Result, bail};
 use dirs::home_dir;
 use serde::{Deserialize, Serialize};
+use std::collections::BTreeMap;
 use std::path::PathBuf;
-use std::{collections::HashMap, env::var, path};
+use std::{env::var, path};
 use tokio::join;
 
 use iggy_common::ArgsOptional;
@@ -31,7 +32,7 @@ static ACTIVE_CONTEXT_FILE_NAME: &str = ".active_context";
 static CONTEXTS_FILE_NAME: &str = "contexts.toml";
 pub(crate) static DEFAULT_CONTEXT_NAME: &str = "default";
 
-pub type ContextsConfigMap = HashMap<String, ContextConfig>;
+pub type ContextsConfigMap = BTreeMap<String, ContextConfig>;
 
 #[derive(Deserialize, Serialize, Clone, Debug, Default)]
 pub struct ContextConfig {
@@ -49,6 +50,9 @@ pub struct ContextConfig {
 
     #[serde(flatten)]
     pub iggy: ArgsOptional,
+
+    #[serde(flatten)]
+    pub extra: BTreeMap<String, toml::Value>,
 }
 
 struct ContextState {
@@ -58,9 +62,11 @@ struct ContextState {
 
 impl Default for ContextState {
     fn default() -> Self {
+        let mut contexts = ContextsConfigMap::new();
+        contexts.insert(DEFAULT_CONTEXT_NAME.to_string(), 
ContextConfig::default());
         Self {
             active_context: DEFAULT_CONTEXT_NAME.to_string(),
-            contexts: HashMap::from([(DEFAULT_CONTEXT_NAME.to_string(), 
ContextConfig::default())]),
+            contexts,
         }
     }
 }
@@ -126,6 +132,75 @@ impl ContextManager {
         Ok(context_state.contexts.clone())
     }
 
+    pub async fn create_context(&mut self, name: &str, config: ContextConfig) 
-> Result<()> {
+        validate_context_name(name)?;
+
+        if name == DEFAULT_CONTEXT_NAME {
+            bail!("cannot create a context named '{DEFAULT_CONTEXT_NAME}' - it 
is reserved")
+        }
+
+        self.get_context_state().await?;
+        let cs = self.context_state.as_ref().unwrap();
+
+        if cs.contexts.contains_key(name) {
+            bail!("context '{name}' already exists in {CONTEXTS_FILE_NAME}")
+        }
+
+        let mut new_contexts = cs.contexts.clone();
+        new_contexts.insert(name.to_string(), config);
+
+        self.context_rw.ensure_iggy_home_exists().await?;
+        self.context_rw
+            .write_contexts(new_contexts.clone())
+            .await
+            .context(format!("failed writing contexts after creating 
'{name}'"))?;
+
+        self.context_state.replace(ContextState {
+            active_context: cs.active_context.clone(),
+            contexts: new_contexts,
+        });
+
+        Ok(())
+    }
+
+    pub async fn delete_context(&mut self, name: &str) -> Result<()> {
+        if name == DEFAULT_CONTEXT_NAME {
+            bail!("cannot delete the '{DEFAULT_CONTEXT_NAME}' context")
+        }
+
+        self.get_context_state().await?;
+        let cs = self.context_state.as_ref().unwrap();
+
+        if !cs.contexts.contains_key(name) {
+            bail!("context '{name}' not found in {CONTEXTS_FILE_NAME}")
+        }
+
+        let mut new_contexts = cs.contexts.clone();
+        new_contexts.remove(name);
+
+        let active_context = if cs.active_context == name {
+            self.context_rw
+                .write_active_context(DEFAULT_CONTEXT_NAME)
+                .await
+                .context("failed resetting active context to default")?;
+            DEFAULT_CONTEXT_NAME.to_string()
+        } else {
+            cs.active_context.clone()
+        };
+
+        self.context_rw
+            .write_contexts(new_contexts.clone())
+            .await
+            .context(format!("failed writing contexts after deleting 
'{name}'"))?;
+
+        self.context_state.replace(ContextState {
+            active_context,
+            contexts: new_contexts,
+        });
+
+        Ok(())
+    }
+
     async fn get_context_state(&mut self) -> Result<&ContextState> {
         if self.context_state.is_none() {
             let (active_context_res, contexts_res) = join!(
@@ -214,7 +289,8 @@ impl ContextReaderWriter {
                 contexts_path.display()
             ))?;
 
-            tokio::fs::write(contexts_path, contents).await?;
+            tokio::fs::write(&contexts_path, contents).await?;
+            Self::set_owner_only_permissions(&contexts_path).await?;
         }
 
         Ok(())
@@ -226,7 +302,7 @@ impl ContextReaderWriter {
         if let Some(active_context_path) = maybe_active_context_path {
             tokio::fs::read_to_string(active_context_path.clone())
                 .await
-                .map(Some)
+                .map(|s| Some(s.trim().to_string()))
                 .or_else(|err| {
                     if err.kind() == std::io::ErrorKind::NotFound {
                         Ok(None)
@@ -258,6 +334,32 @@ impl ContextReaderWriter {
         Ok(())
     }
 
+    pub async fn ensure_iggy_home_exists(&self) -> Result<()> {
+        if let Some(ref iggy_home) = self.iggy_home
+            && !tokio::fs::try_exists(iggy_home).await.unwrap_or(false)
+        {
+            tokio::fs::create_dir_all(iggy_home).await.context(format!(
+                "failed creating iggy home directory {}",
+                iggy_home.display()
+            ))?;
+        }
+        Ok(())
+    }
+
+    #[cfg(unix)]
+    async fn set_owner_only_permissions(path: &PathBuf) -> Result<()> {
+        use std::os::unix::fs::PermissionsExt;
+        let perms = std::fs::Permissions::from_mode(0o600);
+        tokio::fs::set_permissions(path, perms)
+            .await
+            .context(format!("failed setting permissions on {}", 
path.display()))
+    }
+
+    #[cfg(not(unix))]
+    async fn set_owner_only_permissions(_path: &PathBuf) -> Result<()> {
+        Ok(())
+    }
+
     fn active_context_path(&self) -> Option<PathBuf> {
         self.iggy_home
             .clone()
@@ -281,3 +383,374 @@ pub fn iggy_home() -> Option<PathBuf> {
         Err(_) => home_dir().map(|dir| 
dir.join(path::Path::new(DEFAULT_IGGY_HOME_VALUE))),
     }
 }
+
+fn validate_context_name(name: &str) -> Result<()> {
+    if name.trim().is_empty() {
+        bail!("context name cannot be empty or whitespace-only")
+    }
+    if !name
+        .chars()
+        .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
+    {
+        bail!("context name must contain only alphanumeric characters, 
hyphens, and underscores")
+    }
+    Ok(())
+}
+
+pub fn validate_transport(transport: &str) -> Result<()> {
+    use std::str::FromStr;
+    iggy_common::TransportProtocol::from_str(transport).map_err(|_| {
+        anyhow::anyhow!(
+            "invalid transport '{}' - valid values are: tcp, quic, http, ws",
+            transport
+        )
+    })?;
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::tempdir;
+
+    fn test_manager(iggy_home: PathBuf) -> ContextManager {
+        ContextManager::new(ContextReaderWriter::new(Some(iggy_home)))
+    }
+
+    #[tokio::test]
+    async fn should_create_context() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        let config = ContextConfig {
+            username: Some("admin".to_string()),
+            iggy: ArgsOptional {
+                transport: Some("tcp".to_string()),
+                tcp_server_address: Some("10.0.0.1:8090".to_string()),
+                ..Default::default()
+            },
+            ..Default::default()
+        };
+
+        mgr.create_context("production", config).await.unwrap();
+
+        let contexts = mgr.get_contexts().await.unwrap();
+        assert!(contexts.contains_key("production"));
+        assert!(contexts.contains_key("default"));
+        assert_eq!(contexts.len(), 2);
+    }
+
+    #[tokio::test]
+    async fn should_reject_duplicate_context() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        mgr.create_context("test", ContextConfig::default())
+            .await
+            .unwrap();
+
+        let result = mgr.create_context("test", 
ContextConfig::default()).await;
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("already exists"));
+    }
+
+    #[tokio::test]
+    async fn should_reject_creating_default_context() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        let result = mgr
+            .create_context("default", ContextConfig::default())
+            .await;
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("reserved"));
+    }
+
+    #[tokio::test]
+    async fn should_reject_empty_context_name() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        let result = mgr.create_context("", ContextConfig::default()).await;
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("empty"));
+    }
+
+    #[tokio::test]
+    async fn should_reject_whitespace_only_context_name() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        let result = mgr.create_context("  ", ContextConfig::default()).await;
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("empty"));
+    }
+
+    #[tokio::test]
+    async fn should_reject_context_name_with_special_chars() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        let result = mgr
+            .create_context("my context!", ContextConfig::default())
+            .await;
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("alphanumeric"));
+    }
+
+    #[tokio::test]
+    async fn should_accept_context_name_with_hyphens_and_underscores() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        mgr.create_context("my-context_01", ContextConfig::default())
+            .await
+            .unwrap();
+
+        let contexts = mgr.get_contexts().await.unwrap();
+        assert!(contexts.contains_key("my-context_01"));
+    }
+
+    #[tokio::test]
+    async fn should_delete_context() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        mgr.create_context("staging", ContextConfig::default())
+            .await
+            .unwrap();
+
+        mgr.delete_context("staging").await.unwrap();
+
+        let contexts = mgr.get_contexts().await.unwrap();
+        assert!(!contexts.contains_key("staging"));
+    }
+
+    #[tokio::test]
+    async fn should_reject_deleting_default_context() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        let result = mgr.delete_context("default").await;
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("cannot delete"));
+    }
+
+    #[tokio::test]
+    async fn should_reject_deleting_nonexistent_context() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        let result = mgr.delete_context("nope").await;
+        assert!(result.is_err());
+        assert!(result.unwrap_err().to_string().contains("not found"));
+    }
+
+    #[tokio::test]
+    async fn should_reset_active_to_default_when_deleting_active_context() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        mgr.create_context("dev", ContextConfig::default())
+            .await
+            .unwrap();
+        mgr.set_active_context_key("dev").await.unwrap();
+        assert_eq!(mgr.get_active_context_key().await.unwrap(), "dev");
+
+        mgr.delete_context("dev").await.unwrap();
+        assert_eq!(mgr.get_active_context_key().await.unwrap(), "default");
+    }
+
+    #[tokio::test]
+    async fn should_create_iggy_home_if_missing() {
+        let dir = tempdir().unwrap();
+        let nested = dir.path().join("sub").join("dir");
+        let mut mgr = test_manager(nested.clone());
+
+        assert!(!nested.exists());
+        mgr.create_context("test", ContextConfig::default())
+            .await
+            .unwrap();
+        assert!(nested.exists());
+    }
+
+    #[tokio::test]
+    async fn should_persist_context_config_fields() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        let config = ContextConfig {
+            username: Some("user1".to_string()),
+            password: Some("pass1".to_string()),
+            iggy: ArgsOptional {
+                transport: Some("http".to_string()),
+                http_api_url: Some("http://localhost:3000".to_string()),
+                ..Default::default()
+            },
+            ..Default::default()
+        };
+
+        mgr.create_context("myctx", config).await.unwrap();
+
+        let rw = ContextReaderWriter::new(Some(dir.path().to_path_buf()));
+        let saved = rw.read_contexts().await.unwrap().unwrap();
+        let ctx = saved.get("myctx").unwrap();
+        assert_eq!(ctx.username.as_deref(), Some("user1"));
+        assert_eq!(ctx.password.as_deref(), Some("pass1"));
+        assert_eq!(ctx.iggy.transport.as_deref(), Some("http"));
+        assert_eq!(
+            ctx.iggy.http_api_url.as_deref(),
+            Some("http://localhost:3000";)
+        );
+    }
+
+    #[tokio::test]
+    async fn reader_writer_with_none_iggy_home_is_noop_for_paths() {
+        let rw = ContextReaderWriter::new(None);
+        assert!(rw.read_contexts().await.unwrap().is_none());
+        assert!(rw.read_active_context().await.unwrap().is_none());
+        rw.write_contexts(ContextsConfigMap::new()).await.unwrap();
+        rw.write_active_context("any").await.unwrap();
+        rw.ensure_iggy_home_exists().await.unwrap();
+    }
+
+    #[tokio::test]
+    async fn read_contexts_returns_none_when_file_missing() {
+        let dir = tempdir().unwrap();
+        let rw = ContextReaderWriter::new(Some(dir.path().to_path_buf()));
+        assert!(rw.read_contexts().await.unwrap().is_none());
+    }
+
+    #[tokio::test]
+    async fn read_active_context_returns_none_when_file_missing() {
+        let dir = tempdir().unwrap();
+        let rw = ContextReaderWriter::new(Some(dir.path().to_path_buf()));
+        assert!(rw.read_active_context().await.unwrap().is_none());
+    }
+
+    #[tokio::test]
+    async fn ensure_iggy_home_exists_skips_when_directory_already_exists() {
+        let dir = tempdir().unwrap();
+        let rw = ContextReaderWriter::new(Some(dir.path().to_path_buf()));
+        assert!(dir.path().exists());
+        rw.ensure_iggy_home_exists().await.unwrap();
+    }
+
+    #[tokio::test]
+    async fn should_fail_when_active_context_file_points_to_unknown_context() {
+        let dir = tempdir().unwrap();
+        let iggy_home = dir.path().to_path_buf();
+        tokio::fs::create_dir_all(&iggy_home).await.unwrap();
+        tokio::fs::write(iggy_home.join(ACTIVE_CONTEXT_FILE_NAME), "ghost")
+            .await
+            .unwrap();
+        let mut only_default = ContextsConfigMap::new();
+        only_default.insert(DEFAULT_CONTEXT_NAME.to_string(), 
ContextConfig::default());
+        let contents = toml::to_string(&only_default).unwrap();
+        tokio::fs::write(iggy_home.join(CONTEXTS_FILE_NAME), contents)
+            .await
+            .unwrap();
+
+        let mut mgr = test_manager(iggy_home);
+        let err = mgr.get_contexts().await.unwrap_err();
+        assert!(err.to_string().contains("missing"));
+    }
+
+    #[tokio::test]
+    async fn should_reject_set_active_for_unknown_context() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+        let err = mgr.set_active_context_key("nonexistent").await.unwrap_err();
+        assert!(err.to_string().contains("missing"));
+    }
+
+    #[tokio::test]
+    async fn should_get_active_context_config() {
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+        let ctx = mgr.get_active_context().await.unwrap();
+        assert!(ctx.username.is_none());
+        assert!(ctx.password.is_none());
+        assert_eq!(
+            mgr.get_active_context_key().await.unwrap(),
+            DEFAULT_CONTEXT_NAME
+        );
+    }
+
+    #[tokio::test]
+    async fn should_trim_whitespace_from_active_context() {
+        let dir = tempdir().unwrap();
+        let iggy_home = dir.path().to_path_buf();
+        let rw = ContextReaderWriter::new(Some(iggy_home.clone()));
+        rw.write_active_context("dev").await.unwrap();
+        tokio::fs::write(iggy_home.join(ACTIVE_CONTEXT_FILE_NAME), "dev\n")
+            .await
+            .unwrap();
+        let result = rw.read_active_context().await.unwrap();
+        assert_eq!(result.as_deref(), Some("dev"));
+    }
+
+    #[cfg(unix)]
+    #[tokio::test]
+    async fn should_set_restrictive_permissions_on_contexts_file() {
+        use std::os::unix::fs::PermissionsExt;
+
+        let dir = tempdir().unwrap();
+        let mut mgr = test_manager(dir.path().to_path_buf());
+
+        mgr.create_context("secure-ctx", ContextConfig::default())
+            .await
+            .unwrap();
+
+        let contexts_path = dir.path().join(CONTEXTS_FILE_NAME);
+        let metadata = tokio::fs::metadata(&contexts_path).await.unwrap();
+        let mode = metadata.permissions().mode() & 0o777;
+        assert_eq!(mode, 0o600);
+    }
+
+    #[test]
+    fn should_validate_transport() {
+        assert!(validate_transport("tcp").is_ok());
+        assert!(validate_transport("quic").is_ok());
+        assert!(validate_transport("http").is_ok());
+        assert!(validate_transport("ws").is_ok());
+        assert!(validate_transport("foobar").is_err());
+        assert!(validate_transport("websocket").is_err());
+    }
+
+    #[test]
+    fn should_validate_context_name() {
+        assert!(validate_context_name("production").is_ok());
+        assert!(validate_context_name("my-ctx").is_ok());
+        assert!(validate_context_name("my_ctx_01").is_ok());
+        assert!(validate_context_name("").is_err());
+        assert!(validate_context_name("  ").is_err());
+        assert!(validate_context_name("my ctx").is_err());
+        assert!(validate_context_name("ctx!").is_err());
+        assert!(validate_context_name("a/b").is_err());
+    }
+
+    #[tokio::test]
+    async fn should_preserve_unknown_fields_through_round_trip() {
+        let dir = tempdir().unwrap();
+        let iggy_home = dir.path().to_path_buf();
+
+        let toml_with_extra = r#"[myctx]
+username = "admin"
+future_field = "preserved"
+"#;
+        tokio::fs::write(iggy_home.join(CONTEXTS_FILE_NAME), toml_with_extra)
+            .await
+            .unwrap();
+
+        let rw = ContextReaderWriter::new(Some(iggy_home.clone()));
+        let mut contexts = rw.read_contexts().await.unwrap().unwrap();
+        contexts.insert("newctx".to_string(), ContextConfig::default());
+        rw.write_contexts(contexts).await.unwrap();
+
+        let reloaded = rw.read_contexts().await.unwrap().unwrap();
+        let myctx = reloaded.get("myctx").unwrap();
+        assert_eq!(myctx.username.as_deref(), Some("admin"));
+        assert!(myctx.extra.contains_key("future_field"));
+    }
+}
diff --git a/core/cli/src/commands/binary_context/create_context.rs 
b/core/cli/src/commands/binary_context/create_context.rs
new file mode 100644
index 000000000..76f3ad0e0
--- /dev/null
+++ b/core/cli/src/commands/binary_context/create_context.rs
@@ -0,0 +1,94 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use async_trait::async_trait;
+use tracing::{Level, event};
+
+use crate::commands::cli_command::{CliCommand, PRINT_TARGET};
+use iggy_common::Client;
+
+use super::common::{ContextConfig, ContextManager, validate_transport};
+
+pub struct CreateContextCmd {
+    context_name: String,
+    context_config: ContextConfig,
+}
+
+impl CreateContextCmd {
+    pub fn new(context_name: String, context_config: ContextConfig) -> Self {
+        Self {
+            context_name,
+            context_config,
+        }
+    }
+}
+
+#[async_trait]
+impl CliCommand for CreateContextCmd {
+    fn explain(&self) -> String {
+        let context_name = &self.context_name;
+        format!("create context {context_name}")
+    }
+
+    fn login_required(&self) -> bool {
+        false
+    }
+
+    fn connection_required(&self) -> bool {
+        false
+    }
+
+    async fn execute_cmd(&mut self, _client: &dyn Client) -> 
anyhow::Result<(), anyhow::Error> {
+        if let Some(ref transport) = self.context_config.iggy.transport {
+            validate_transport(transport)?;
+        }
+
+        let mut context_mgr = ContextManager::default();
+
+        context_mgr
+            .create_context(&self.context_name, self.context_config.clone())
+            .await?;
+
+        event!(target: PRINT_TARGET, Level::INFO, "context '{}' created 
successfully", self.context_name);
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn should_return_explain_message() {
+        let cmd = CreateContextCmd::new("production".to_string(), 
ContextConfig::default());
+        assert_eq!(cmd.explain(), "create context production");
+    }
+
+    #[test]
+    fn should_not_require_login() {
+        let cmd = CreateContextCmd::new("test".to_string(), 
ContextConfig::default());
+        assert!(!cmd.login_required());
+    }
+
+    #[test]
+    fn should_not_require_connection() {
+        let cmd = CreateContextCmd::new("test".to_string(), 
ContextConfig::default());
+        assert!(!cmd.connection_required());
+    }
+}
diff --git a/core/cli/src/commands/binary_context/delete_context.rs 
b/core/cli/src/commands/binary_context/delete_context.rs
new file mode 100644
index 000000000..0593f32d4
--- /dev/null
+++ b/core/cli/src/commands/binary_context/delete_context.rs
@@ -0,0 +1,84 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use async_trait::async_trait;
+use tracing::{Level, event};
+
+use crate::commands::cli_command::{CliCommand, PRINT_TARGET};
+use iggy_common::Client;
+
+use super::common::ContextManager;
+
+pub struct DeleteContextCmd {
+    context_name: String,
+}
+
+impl DeleteContextCmd {
+    pub fn new(context_name: String) -> Self {
+        Self { context_name }
+    }
+}
+
+#[async_trait]
+impl CliCommand for DeleteContextCmd {
+    fn explain(&self) -> String {
+        let context_name = &self.context_name;
+        format!("delete context {context_name}")
+    }
+
+    fn login_required(&self) -> bool {
+        false
+    }
+
+    fn connection_required(&self) -> bool {
+        false
+    }
+
+    async fn execute_cmd(&mut self, _client: &dyn Client) -> 
anyhow::Result<(), anyhow::Error> {
+        let mut context_mgr = ContextManager::default();
+
+        context_mgr.delete_context(&self.context_name).await?;
+
+        event!(target: PRINT_TARGET, Level::INFO, "context '{}' deleted 
successfully", self.context_name);
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn should_return_explain_message() {
+        let cmd = DeleteContextCmd::new("production".to_string());
+        assert_eq!(cmd.explain(), "delete context production");
+    }
+
+    #[test]
+    fn should_not_require_login() {
+        let cmd = DeleteContextCmd::new("test".to_string());
+        assert!(!cmd.login_required());
+    }
+
+    #[test]
+    fn should_not_require_connection() {
+        let cmd = DeleteContextCmd::new("test".to_string());
+        assert!(!cmd.connection_required());
+    }
+}
diff --git a/core/cli/src/commands/binary_context/mod.rs 
b/core/cli/src/commands/binary_context/mod.rs
index c319b3ffd..bad021e21 100644
--- a/core/cli/src/commands/binary_context/mod.rs
+++ b/core/cli/src/commands/binary_context/mod.rs
@@ -18,5 +18,7 @@
 
 pub mod common;
 
+pub mod create_context;
+pub mod delete_context;
 pub mod get_contexts;
 pub mod use_context;
diff --git a/core/cli/src/main.rs b/core/cli/src/main.rs
index 657f2e3c9..98c300ada 100644
--- a/core/cli/src/main.rs
+++ b/core/cli/src/main.rs
@@ -41,6 +41,8 @@ use iggy::client_provider::{self, ClientProviderConfig};
 use iggy::clients::client::IggyClient;
 use iggy::prelude::{Aes256GcmEncryptor, Args, EncryptorKind, 
PersonalAccessTokenExpiry};
 use iggy_cli::commands::binary_context::common::ContextManager;
+use iggy_cli::commands::binary_context::create_context::CreateContextCmd;
+use iggy_cli::commands::binary_context::delete_context::DeleteContextCmd;
 use iggy_cli::commands::binary_context::use_context::UseContextCmd;
 use iggy_cli::commands::binary_segments::delete_segments::DeleteSegmentsCmd;
 use iggy_cli::commands::binary_system::snapshot::GetSnapshotCmd;
@@ -328,6 +330,13 @@ fn get_command(
             ContextAction::Use(use_args) => {
                 Box::new(UseContextCmd::new(use_args.context_name.clone()))
             }
+            ContextAction::Create(create_args) => {
+                let context_name = create_args.context_name.clone();
+                Box::new(CreateContextCmd::new(context_name, 
create_args.into()))
+            }
+            ContextAction::Delete(delete_args) => {
+                
Box::new(DeleteContextCmd::new(delete_args.context_name.clone()))
+            }
         },
         #[cfg(feature = "login-session")]
         Command::Login(login_args) => Box::new(LoginCmd::new(
diff --git a/core/integration/tests/cli/context/common.rs 
b/core/integration/tests/cli/context/common.rs
index 19c02184f..4ca2ca500 100644
--- a/core/integration/tests/cli/context/common.rs
+++ b/core/integration/tests/cli/context/common.rs
@@ -64,6 +64,10 @@ impl TestIggyContext {
         self.context_manager.read_active_context().await.unwrap()
     }
 
+    pub async fn read_saved_contexts(&self) -> Option<ContextsConfigMap> {
+        self.context_manager.read_contexts().await.unwrap()
+    }
+
     pub fn get_contexts(&self) -> Option<ContextsConfigMap> {
         self.maybe_contexts.clone()
     }
diff --git a/core/integration/tests/cli/context/mod.rs 
b/core/integration/tests/cli/context/mod.rs
index 974cd77ad..1bc857c6c 100644
--- a/core/integration/tests/cli/context/mod.rs
+++ b/core/integration/tests/cli/context/mod.rs
@@ -19,5 +19,7 @@
 mod common;
 
 mod test_context_applied;
+mod test_context_create_command;
+mod test_context_delete_command;
 mod test_context_list_command;
 mod test_context_use_command;
diff --git a/core/integration/tests/cli/context/test_context_applied.rs 
b/core/integration/tests/cli/context/test_context_applied.rs
index 30a9457b3..99ccc93c1 100644
--- a/core/integration/tests/cli/context/test_context_applied.rs
+++ b/core/integration/tests/cli/context/test_context_applied.rs
@@ -26,7 +26,7 @@ use iggy_cli::commands::binary_context::common::ContextConfig;
 use integration::harness::ServerHandle;
 use predicates::str::{contains, starts_with};
 use serial_test::parallel;
-use std::collections::HashMap;
+use std::collections::BTreeMap;
 
 struct TestContextApplied {
     set_transport_context: Option<String>,
@@ -37,7 +37,7 @@ struct TestContextApplied {
 impl TestContextApplied {
     fn new(set_transport_context: Option<String>, set_transport_arg: 
Option<String>) -> Self {
         let test_iggy_context = TestIggyContext::new(
-            Some(HashMap::from([
+            Some(BTreeMap::from([
                 (
                     "default".to_string(),
                     ContextConfig {
diff --git a/core/integration/tests/cli/context/test_context_create_command.rs 
b/core/integration/tests/cli/context/test_context_create_command.rs
new file mode 100644
index 000000000..fc1910bc5
--- /dev/null
+++ b/core/integration/tests/cli/context/test_context_create_command.rs
@@ -0,0 +1,239 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use std::collections::BTreeMap;
+
+use crate::cli::common::{
+    CLAP_INDENT, IggyCmdCommand, IggyCmdTest, IggyCmdTestCase, TestHelpCmd, 
USAGE_PREFIX,
+};
+use assert_cmd::assert::Assert;
+use async_trait::async_trait;
+use iggy::prelude::Client;
+use iggy_cli::commands::binary_context::common::ContextConfig;
+use predicates::str::contains;
+use serial_test::parallel;
+
+use super::common::TestIggyContext;
+
+struct TestContextCreateCmd {
+    test_iggy_context: TestIggyContext,
+    new_context_name: String,
+    transport: Option<String>,
+    tcp_server_address: Option<String>,
+}
+
+impl TestContextCreateCmd {
+    fn new(
+        test_iggy_context: TestIggyContext,
+        new_context_name: String,
+        transport: Option<String>,
+        tcp_server_address: Option<String>,
+    ) -> Self {
+        Self {
+            test_iggy_context,
+            new_context_name,
+            transport,
+            tcp_server_address,
+        }
+    }
+}
+
+#[async_trait]
+impl IggyCmdTestCase for TestContextCreateCmd {
+    async fn prepare_server_state(&mut self, _client: &dyn Client) {
+        self.test_iggy_context.prepare().await;
+    }
+
+    fn get_command(&self) -> IggyCmdCommand {
+        let mut cmd = IggyCmdCommand::new()
+            .env(
+                "IGGY_HOME",
+                self.test_iggy_context.get_iggy_home().to_str().unwrap(),
+            )
+            .arg("context")
+            .arg("create")
+            .arg(self.new_context_name.clone())
+            .with_env_credentials();
+
+        if let Some(transport) = &self.transport {
+            cmd = cmd.arg("--transport").arg(transport.clone());
+        }
+
+        if let Some(addr) = &self.tcp_server_address {
+            cmd = cmd.arg("--tcp-server-address").arg(addr.clone());
+        }
+
+        cmd
+    }
+
+    fn verify_command(&self, command_state: Assert) {
+        command_state.success().stdout(contains(format!(
+            "context '{}' created successfully",
+            self.new_context_name
+        )));
+    }
+
+    async fn verify_server_state(&self, _client: &dyn Client) {
+        let saved_contexts = 
self.test_iggy_context.read_saved_contexts().await;
+        assert!(saved_contexts.is_some());
+        let contexts = saved_contexts.unwrap();
+        assert!(contexts.contains_key(&self.new_context_name));
+    }
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_be_successful() {
+    let mut iggy_cmd_test = IggyCmdTest::default();
+    iggy_cmd_test.setup().await;
+
+    iggy_cmd_test
+        .execute_test(TestContextCreateCmd::new(
+            TestIggyContext::new(
+                Some(BTreeMap::from([(
+                    "default".to_string(),
+                    ContextConfig::default(),
+                )])),
+                None,
+            ),
+            "production".to_string(),
+            Some("tcp".to_string()),
+            Some("10.0.0.1:8090".to_string()),
+        ))
+        .await;
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_create_minimal_context() {
+    let mut iggy_cmd_test = IggyCmdTest::default();
+    iggy_cmd_test.setup().await;
+
+    iggy_cmd_test
+        .execute_test(TestContextCreateCmd::new(
+            TestIggyContext::new(
+                Some(BTreeMap::from([(
+                    "default".to_string(),
+                    ContextConfig::default(),
+                )])),
+                None,
+            ),
+            "staging".to_string(),
+            None,
+            None,
+        ))
+        .await;
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_help_match() {
+    let mut iggy_cmd_test = IggyCmdTest::default();
+
+    iggy_cmd_test
+        .execute_test_for_help_command(TestHelpCmd::new(
+            vec!["context", "create", "--help"],
+            format!(
+                r#"Create a new context
+
+Creates a new named context in the contexts configuration file.
+After creating a context, use 'iggy context use <name>' to activate it.
+
+Examples
+ iggy context create production --transport tcp --tcp-server-address 
10.0.0.1:8090
+ iggy context create dev --transport http --http-api-url http://localhost:3000
+ iggy context create local --username iggy --password iggy
+
+{USAGE_PREFIX} context create [OPTIONS] <CONTEXT_NAME>
+
+Arguments:
+  <CONTEXT_NAME>
+{CLAP_INDENT}Name of the context to create
+
+Options:
+      --transport <TRANSPORT>
+{CLAP_INDENT}The transport to use
+{CLAP_INDENT}
+{CLAP_INDENT}Valid values are "quic", "http", "tcp" and "ws".
+
+      --tcp-server-address <TCP_SERVER_ADDRESS>
+{CLAP_INDENT}The server address for the TCP transport
+
+      --http-api-url <HTTP_API_URL>
+{CLAP_INDENT}The API URL for the HTTP transport
+
+      --quic-server-address <QUIC_SERVER_ADDRESS>
+{CLAP_INDENT}The server address for the QUIC transport
+
+      --tcp-tls-enabled <TCP_TLS_ENABLED>
+{CLAP_INDENT}Flag to enable TLS for the TCP transport
+{CLAP_INDENT}
+{CLAP_INDENT}[possible values: true, false]
+
+  -u, --username <USERNAME>
+{CLAP_INDENT}Iggy server username
+
+  -p, --password <PASSWORD>
+{CLAP_INDENT}Iggy server password
+
+  -t, --token <TOKEN>
+{CLAP_INDENT}Iggy server personal access token
+
+  -n, --token-name <TOKEN_NAME>
+{CLAP_INDENT}Iggy server personal access token name
+
+  -h, --help
+{CLAP_INDENT}Print help (see a summary with '-h')
+"#,
+            ),
+        ))
+        .await;
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_short_help_match() {
+    let mut iggy_cmd_test = IggyCmdTest::default();
+
+    iggy_cmd_test
+        .execute_test_for_help_command(TestHelpCmd::new(
+            vec!["context", "create", "-h"],
+            format!(
+                r#"Create a new context
+
+{USAGE_PREFIX} context create [OPTIONS] <CONTEXT_NAME>
+
+Arguments:
+  <CONTEXT_NAME>  Name of the context to create
+
+Options:
+      --transport <TRANSPORT>                      The transport to use
+      --tcp-server-address <TCP_SERVER_ADDRESS>    The server address for the 
TCP transport
+      --http-api-url <HTTP_API_URL>                The API URL for the HTTP 
transport
+      --quic-server-address <QUIC_SERVER_ADDRESS>  The server address for the 
QUIC transport
+      --tcp-tls-enabled <TCP_TLS_ENABLED>          Flag to enable TLS for the 
TCP transport [possible values: true, false]
+  -u, --username <USERNAME>                        Iggy server username
+  -p, --password <PASSWORD>                        Iggy server password
+  -t, --token <TOKEN>                              Iggy server personal access 
token
+  -n, --token-name <TOKEN_NAME>                    Iggy server personal access 
token name
+  -h, --help                                       Print help (see more with 
'--help')
+"#,
+            ),
+        ))
+        .await;
+}
diff --git a/core/integration/tests/cli/context/test_context_delete_command.rs 
b/core/integration/tests/cli/context/test_context_delete_command.rs
new file mode 100644
index 000000000..c21d04d2e
--- /dev/null
+++ b/core/integration/tests/cli/context/test_context_delete_command.rs
@@ -0,0 +1,224 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use std::collections::BTreeMap;
+
+use crate::cli::common::{
+    CLAP_INDENT, IggyCmdCommand, IggyCmdTest, IggyCmdTestCase, TestHelpCmd, 
USAGE_PREFIX,
+};
+use assert_cmd::assert::Assert;
+use async_trait::async_trait;
+use iggy::prelude::Client;
+use iggy_cli::commands::binary_context::common::ContextConfig;
+use predicates::str::contains;
+use serial_test::parallel;
+
+use super::common::TestIggyContext;
+
+struct TestContextDeleteCmd {
+    test_iggy_context: TestIggyContext,
+    context_to_delete: String,
+}
+
+impl TestContextDeleteCmd {
+    fn new(test_iggy_context: TestIggyContext, context_to_delete: String) -> 
Self {
+        Self {
+            test_iggy_context,
+            context_to_delete,
+        }
+    }
+}
+
+#[async_trait]
+impl IggyCmdTestCase for TestContextDeleteCmd {
+    async fn prepare_server_state(&mut self, _client: &dyn Client) {
+        self.test_iggy_context.prepare().await;
+    }
+
+    fn get_command(&self) -> IggyCmdCommand {
+        IggyCmdCommand::new()
+            .env(
+                "IGGY_HOME",
+                self.test_iggy_context.get_iggy_home().to_str().unwrap(),
+            )
+            .arg("context")
+            .arg("delete")
+            .arg(self.context_to_delete.clone())
+            .with_env_credentials()
+    }
+
+    fn verify_command(&self, command_state: Assert) {
+        command_state.success().stdout(contains(format!(
+            "context '{}' deleted successfully",
+            self.context_to_delete
+        )));
+    }
+
+    async fn verify_server_state(&self, _client: &dyn Client) {
+        let saved_contexts = 
self.test_iggy_context.read_saved_contexts().await;
+        assert!(saved_contexts.is_some());
+        let contexts = saved_contexts.unwrap();
+        assert!(!contexts.contains_key(&self.context_to_delete));
+    }
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_be_successful() {
+    let mut iggy_cmd_test = IggyCmdTest::default();
+    iggy_cmd_test.setup().await;
+
+    iggy_cmd_test
+        .execute_test(TestContextDeleteCmd::new(
+            TestIggyContext::new(
+                Some(BTreeMap::from([
+                    ("default".to_string(), ContextConfig::default()),
+                    ("production".to_string(), ContextConfig::default()),
+                ])),
+                None,
+            ),
+            "production".to_string(),
+        ))
+        .await;
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_reset_active_context_on_delete() {
+    let mut iggy_cmd_test = IggyCmdTest::default();
+    iggy_cmd_test.setup().await;
+
+    let test_context = TestIggyContext::new(
+        Some(BTreeMap::from([
+            ("default".to_string(), ContextConfig::default()),
+            ("staging".to_string(), ContextConfig::default()),
+        ])),
+        Some("staging".to_string()),
+    );
+
+    let test_cmd = TestContextDeleteActiveCmd::new(test_context, 
"staging".to_string());
+
+    iggy_cmd_test.execute_test(test_cmd).await;
+}
+
+struct TestContextDeleteActiveCmd {
+    test_iggy_context: TestIggyContext,
+    context_to_delete: String,
+}
+
+impl TestContextDeleteActiveCmd {
+    fn new(test_iggy_context: TestIggyContext, context_to_delete: String) -> 
Self {
+        Self {
+            test_iggy_context,
+            context_to_delete,
+        }
+    }
+}
+
+#[async_trait]
+impl IggyCmdTestCase for TestContextDeleteActiveCmd {
+    async fn prepare_server_state(&mut self, _client: &dyn Client) {
+        self.test_iggy_context.prepare().await;
+    }
+
+    fn get_command(&self) -> IggyCmdCommand {
+        IggyCmdCommand::new()
+            .env(
+                "IGGY_HOME",
+                self.test_iggy_context.get_iggy_home().to_str().unwrap(),
+            )
+            .arg("context")
+            .arg("delete")
+            .arg(self.context_to_delete.clone())
+            .with_env_credentials()
+    }
+
+    fn verify_command(&self, command_state: Assert) {
+        command_state.success().stdout(contains(format!(
+            "context '{}' deleted successfully",
+            self.context_to_delete
+        )));
+    }
+
+    async fn verify_server_state(&self, _client: &dyn Client) {
+        let saved_contexts = 
self.test_iggy_context.read_saved_contexts().await;
+        assert!(saved_contexts.is_some());
+        let contexts = saved_contexts.unwrap();
+        assert!(!contexts.contains_key(&self.context_to_delete));
+
+        let active_key = self.test_iggy_context.read_saved_context_key().await;
+        assert_eq!(active_key, Some("default".to_string()));
+    }
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_help_match() {
+    let mut iggy_cmd_test = IggyCmdTest::default();
+
+    iggy_cmd_test
+        .execute_test_for_help_command(TestHelpCmd::new(
+            vec!["context", "delete", "--help"],
+            format!(
+                r#"Delete an existing context
+
+Removes a named context from the contexts configuration file.
+The 'default' context cannot be deleted. If the deleted context
+was the active context, the active context resets to 'default'.
+
+Examples
+ iggy context delete production
+
+{USAGE_PREFIX} context delete <CONTEXT_NAME>
+
+Arguments:
+  <CONTEXT_NAME>
+{CLAP_INDENT}Name of the context to delete
+
+Options:
+  -h, --help
+{CLAP_INDENT}Print help (see a summary with '-h')
+"#,
+            ),
+        ))
+        .await;
+}
+
+#[tokio::test]
+#[parallel]
+pub async fn should_short_help_match() {
+    let mut iggy_cmd_test = IggyCmdTest::default();
+
+    iggy_cmd_test
+        .execute_test_for_help_command(TestHelpCmd::new(
+            vec!["context", "delete", "-h"],
+            format!(
+                r#"Delete an existing context
+
+{USAGE_PREFIX} context delete <CONTEXT_NAME>
+
+Arguments:
+  <CONTEXT_NAME>  Name of the context to delete
+
+Options:
+  -h, --help  Print help (see more with '--help')
+"#,
+            ),
+        ))
+        .await;
+}
diff --git a/core/integration/tests/cli/context/test_context_list_command.rs 
b/core/integration/tests/cli/context/test_context_list_command.rs
index 0656ce0e4..e428368bd 100644
--- a/core/integration/tests/cli/context/test_context_list_command.rs
+++ b/core/integration/tests/cli/context/test_context_list_command.rs
@@ -16,7 +16,7 @@
  * under the License.
  */
 
-use std::collections::HashMap;
+use std::collections::BTreeMap;
 
 use crate::cli::common::{
     CLAP_INDENT, IggyCmdCommand, IggyCmdTest, IggyCmdTestCase, TestHelpCmd, 
USAGE_PREFIX,
@@ -98,7 +98,7 @@ pub async fn should_be_successful() {
 
     iggy_cmd_test
         .execute_test(TestContextListCmd::new(TestIggyContext::new(
-            Some(HashMap::from([
+            Some(BTreeMap::from([
                 ("default".to_string(), ContextConfig::default()),
                 ("second".to_string(), ContextConfig::default()),
             ])),
@@ -115,7 +115,7 @@ pub async fn should_display_active_context() {
 
     iggy_cmd_test
         .execute_test(TestContextListCmd::new(TestIggyContext::new(
-            Some(HashMap::from([
+            Some(BTreeMap::from([
                 (
                     "default".to_string(),
                     ContextConfig {
diff --git a/core/integration/tests/cli/context/test_context_use_command.rs 
b/core/integration/tests/cli/context/test_context_use_command.rs
index 9be8be0ea..4cad1a0c9 100644
--- a/core/integration/tests/cli/context/test_context_use_command.rs
+++ b/core/integration/tests/cli/context/test_context_use_command.rs
@@ -16,7 +16,7 @@
  * under the License.
  */
 
-use std::collections::HashMap;
+use std::collections::BTreeMap;
 
 use crate::cli::common::{
     CLAP_INDENT, IggyCmdCommand, IggyCmdTest, IggyCmdTestCase, TestHelpCmd, 
USAGE_PREFIX,
@@ -87,7 +87,7 @@ pub async fn should_be_successful() {
     iggy_cmd_test
         .execute_test(TestContextUseCmd::new(
             TestIggyContext::new(
-                Some(HashMap::from([
+                Some(BTreeMap::from([
                     ("default".to_string(), ContextConfig::default()),
                     ("second".to_string(), ContextConfig::default()),
                 ])),

Reply via email to