use crate::auth::crypto::{self, KdfParams};
use crate::auth::format::{self, ExportPayload, ExportProfile};
use crate::profile::{
default_config_path, get_oauth_client_secret, load_config, make_token_key, save_config,
store_oauth_client_secret, Profile, TokenStore, TokenStoreError,
};
use serde::{Deserialize, Serialize};
use std::fs;
use std::io;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ExportImportError {
#[error("Profile not found: {0}")]
ProfileNotFound(String),
#[error("Token not found for profile: {0}")]
TokenNotFound(String),
#[error("No profiles to export")]
NoProfiles,
#[error("Export requires --yes flag for confirmation")]
ConfirmationRequired,
#[error("Profile already exists: {0} (use --force to overwrite)")]
ProfileExists(String),
#[error("Empty passphrase not allowed")]
EmptyPassphrase,
#[cfg(unix)]
#[error("File permission error: {0}")]
PermissionError(String),
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Crypto error: {0}")]
Crypto(#[from] crypto::CryptoError),
#[error("Format error: {0}")]
Format(#[from] format::FormatError),
#[error("Storage error: {0}")]
Storage(String),
#[error("Token store error: {0}")]
TokenStore(#[from] TokenStoreError),
}
pub type Result<T> = std::result::Result<T, ExportImportError>;
#[derive(Debug, Clone)]
pub struct ExportOptions {
pub profile_name: Option<String>,
pub all: bool,
pub output_path: String,
pub passphrase: String,
pub yes: bool,
}
#[derive(Debug, Clone)]
pub struct ImportOptions {
pub input_path: String,
pub passphrase: String,
pub yes: bool,
pub force: bool,
pub dry_run: bool,
pub json: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImportAction {
Updated,
Skipped,
Overwritten,
}
impl std::fmt::Display for ImportAction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImportAction::Updated => write!(f, "updated"),
ImportAction::Skipped => write!(f, "skipped"),
ImportAction::Overwritten => write!(f, "overwritten"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileImportResult {
pub profile_name: String,
pub action: ImportAction,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportResult {
pub profiles: Vec<ProfileImportResult>,
pub summary: ImportSummary,
pub dry_run: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportSummary {
pub updated: usize,
pub skipped: usize,
pub overwritten: usize,
pub total: usize,
}
#[derive(Debug, Clone)]
pub struct ExportResult {
pub exported_count: usize,
pub skipped_profiles: Vec<String>,
}
pub fn export_profiles(
token_store: &dyn TokenStore,
options: &ExportOptions,
) -> Result<ExportResult> {
if !options.yes {
return Err(ExportImportError::ConfirmationRequired);
}
if options.passphrase.is_empty() {
return Err(ExportImportError::EmptyPassphrase);
}
let config_path =
default_config_path().map_err(|e| ExportImportError::Storage(e.to_string()))?;
let config =
load_config(&config_path).map_err(|e| ExportImportError::Storage(e.to_string()))?;
let profiles_to_export: Vec<(String, &Profile)> = if options.all {
config
.profiles
.iter()
.map(|(k, v)| (k.clone(), v))
.collect()
} else if let Some(ref profile_name) = options.profile_name {
let profile = config
.get(profile_name)
.ok_or_else(|| ExportImportError::ProfileNotFound(profile_name.clone()))?;
vec![(profile_name.clone(), profile)]
} else {
let profile = config
.get("default")
.ok_or_else(|| ExportImportError::ProfileNotFound("default".to_string()))?;
vec![("default".to_string(), profile)]
};
if profiles_to_export.is_empty() {
return Err(ExportImportError::NoProfiles);
}
let mut payload = ExportPayload::new();
let mut skipped_profiles = Vec::new();
for (name, profile) in profiles_to_export {
let bot_token_key = make_token_key(&profile.team_id, &profile.user_id);
let user_token_key = format!("{}:{}:user", &profile.team_id, &profile.user_id);
let bot_token = token_store.get(&bot_token_key).ok();
let user_token = token_store.get(&user_token_key).ok();
if bot_token.is_none() && user_token.is_none() {
if options.all {
skipped_profiles.push(name);
} else {
return Err(ExportImportError::TokenNotFound(name));
}
continue;
}
let client_id = profile.client_id.clone();
let client_secret = get_oauth_client_secret(token_store, &name).ok();
payload.profiles.insert(
name,
ExportProfile {
team_id: profile.team_id.clone(),
user_id: profile.user_id.clone(),
team_name: profile.team_name.clone(),
user_name: profile.user_name.clone(),
token: bot_token.unwrap_or_default(),
client_id,
client_secret,
user_token,
},
);
}
if payload.profiles.is_empty() {
return Err(ExportImportError::NoProfiles);
}
let kdf_params = KdfParams {
salt: crypto::generate_salt(),
..Default::default()
};
let key = crypto::derive_key(&options.passphrase, &kdf_params)?;
let payload_json = serde_json::to_vec(&payload)
.map_err(|e| ExportImportError::Format(format::FormatError::Json(e)))?;
let encrypted = crypto::encrypt(&payload_json, &key)?;
let encoded = format::encode_export(&payload, &encrypted, &kdf_params)?;
let output_path = Path::new(&options.output_path);
if output_path.exists() {
check_file_permissions(output_path)?;
}
write_secure_file(output_path, &encoded)?;
Ok(ExportResult {
exported_count: payload.profiles.len(),
skipped_profiles,
})
}
pub fn import_profiles(
token_store: &dyn TokenStore,
options: &ImportOptions,
) -> Result<ImportResult> {
if options.passphrase.is_empty() {
return Err(ExportImportError::EmptyPassphrase);
}
let input_path = Path::new(&options.input_path);
check_file_permissions(input_path)?;
let encoded_data = fs::read(input_path)?;
let decoded = format::decode_export(&encoded_data)?;
let key = crypto::derive_key(&options.passphrase, &decoded.kdf_params)?;
let payload_json = crypto::decrypt(&decoded.encrypted_data, &key)?;
let payload: ExportPayload = serde_json::from_slice(&payload_json)
.map_err(|e| ExportImportError::Format(format::FormatError::Json(e)))?;
let config_path =
default_config_path().map_err(|e| ExportImportError::Storage(e.to_string()))?;
let mut config =
load_config(&config_path).map_err(|e| ExportImportError::Storage(e.to_string()))?;
if options.force && !options.yes && !options.dry_run {
return Err(ExportImportError::Storage(
"--force requires --yes to confirm overwrite".to_string(),
));
}
let mut profile_results = Vec::new();
for (name, export_profile) in payload.profiles {
let find_conflicting_name = || -> Option<String> {
config
.profiles
.iter()
.find(|(n, p)| *n != &name && p.team_id == export_profile.team_id)
.map(|(n, _)| n.clone())
};
let (action, reason, should_import) = if let Some(existing) = config.get(&name) {
if existing.team_id == export_profile.team_id {
if options.force {
(
ImportAction::Overwritten,
format!(
"Overwritten existing profile (same team_id: {})",
existing.team_id
),
true,
)
} else {
(
ImportAction::Updated,
format!(
"Updated existing profile (same team_id: {})",
existing.team_id
),
true,
)
}
} else {
if options.force {
(
ImportAction::Overwritten,
format!(
"Overwritten conflicting profile (team_id {} -> {})",
existing.team_id, export_profile.team_id
),
true,
)
} else {
(
ImportAction::Skipped,
format!(
"Skipped due to team_id conflict ({} vs {})",
existing.team_id, export_profile.team_id
),
false,
)
}
}
} else if let Some(conflicting_name) = find_conflicting_name() {
if options.force {
config.remove(&conflicting_name);
(
ImportAction::Overwritten,
format!(
"Overwritten profile '{}' with conflicting team_id {}",
conflicting_name, export_profile.team_id
),
true,
)
} else {
(
ImportAction::Skipped,
format!(
"Skipped due to existing team_id {} under different name '{}'",
export_profile.team_id, conflicting_name
),
false,
)
}
} else {
(
ImportAction::Updated,
"New profile imported".to_string(),
true,
)
};
if should_import && !options.dry_run {
let profile = Profile {
team_id: export_profile.team_id.clone(),
user_id: export_profile.user_id.clone(),
team_name: export_profile.team_name,
user_name: export_profile.user_name,
client_id: export_profile.client_id.clone(),
redirect_uri: None, scopes: None, bot_scopes: None, user_scopes: None, default_token_type: None,
};
config.set(name.clone(), profile);
let bot_token_key = make_token_key(&export_profile.team_id, &export_profile.user_id);
token_store.set(&bot_token_key, &export_profile.token)?;
if let Some(user_token) = &export_profile.user_token {
let user_token_key = format!(
"{}:{}:user",
&export_profile.team_id, &export_profile.user_id
);
token_store.set(&user_token_key, user_token)?;
}
if let Some(client_secret) = export_profile.client_secret {
store_oauth_client_secret(token_store, &name, &client_secret)?;
}
}
profile_results.push(ProfileImportResult {
profile_name: name,
action,
reason,
});
}
if !options.dry_run {
save_config(&config_path, &config)
.map_err(|e| ExportImportError::Storage(e.to_string()))?;
}
let updated = profile_results
.iter()
.filter(|r| r.action == ImportAction::Updated)
.count();
let skipped = profile_results
.iter()
.filter(|r| r.action == ImportAction::Skipped)
.count();
let overwritten = profile_results
.iter()
.filter(|r| r.action == ImportAction::Overwritten)
.count();
let total = profile_results.len();
Ok(ImportResult {
profiles: profile_results,
summary: ImportSummary {
updated,
skipped,
overwritten,
total,
},
dry_run: options.dry_run,
})
}
#[cfg(unix)]
fn check_file_permissions(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
if !path.exists() {
return Ok(());
}
let metadata = fs::metadata(path)?;
let permissions = metadata.permissions();
let mode = permissions.mode();
if mode & 0o777 != 0o600 {
return Err(ExportImportError::PermissionError(format!(
"File must have 0600 permissions, found: {:o}",
mode & 0o777
)));
}
Ok(())
}
#[cfg(not(unix))]
fn check_file_permissions(_path: &Path) -> Result<()> {
Ok(())
}
#[cfg(unix)]
fn write_secure_file(path: &Path, data: &[u8]) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
fs::write(path, data)?;
let mut permissions = fs::metadata(path)?.permissions();
permissions.set_mode(0o600);
fs::set_permissions(path, permissions)?;
Ok(())
}
#[cfg(not(unix))]
fn write_secure_file(path: &Path, data: &[u8]) -> Result<()> {
fs::write(path, data)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::profile::{InMemoryTokenStore, ProfilesConfig};
use tempfile::TempDir;
#[test]
fn test_export_requires_yes_flag() {
let token_store = InMemoryTokenStore::new();
let options = ExportOptions {
profile_name: None,
all: false,
output_path: "/tmp/test.export".to_string(),
passphrase: "password".to_string(),
yes: false,
};
let result = export_profiles(&token_store, &options);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ExportImportError::ConfirmationRequired
));
}
#[test]
fn test_export_empty_passphrase() {
let token_store = InMemoryTokenStore::new();
let options = ExportOptions {
profile_name: None,
all: false,
output_path: "/tmp/test.export".to_string(),
passphrase: "".to_string(),
yes: true,
};
let result = export_profiles(&token_store, &options);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ExportImportError::EmptyPassphrase
));
}
#[test]
fn test_import_empty_passphrase() {
let token_store = InMemoryTokenStore::new();
let options = ImportOptions {
input_path: "/tmp/test.export".to_string(),
passphrase: "".to_string(),
yes: false,
force: false,
dry_run: false,
json: false,
};
let result = import_profiles(&token_store, &options);
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ExportImportError::EmptyPassphrase
));
}
#[test]
fn test_export_import_round_trip() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("profiles.json");
let _export_path = temp_dir.path().join("export.dat");
let mut config = ProfilesConfig::new();
config.set(
"test".to_string(),
Profile {
team_id: "T123".to_string(),
user_id: "U456".to_string(),
team_name: Some("Test Team".to_string()),
user_name: Some("Test User".to_string()),
client_id: None,
redirect_uri: None,
scopes: None,
bot_scopes: None,
user_scopes: None,
default_token_type: None,
},
);
save_config(&config_path, &config).unwrap();
let token_store = InMemoryTokenStore::new();
let token_key = make_token_key("T123", "U456");
token_store.set(&token_key, "xoxb-test-token").unwrap();
}
#[cfg(unix)]
#[test]
fn test_write_secure_file_permissions() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("secure.dat");
write_secure_file(&file_path, b"test data").unwrap();
let metadata = fs::metadata(&file_path).unwrap();
let mode = metadata.permissions().mode();
assert_eq!(mode & 0o777, 0o600, "File should have 0600 permissions");
}
#[test]
#[serial_test::serial]
fn test_import_dry_run_no_changes() {
use crate::auth::crypto::KdfParams;
use crate::auth::format::ExportProfile;
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("profiles.json");
let import_path = temp_dir.path().join("import.dat");
let tokens_path = temp_dir.path().join("tokens.json");
std::env::set_var("SLACK_RS_TOKENS_PATH", tokens_path.to_str().unwrap());
let mut config = ProfilesConfig::new();
config.set(
"existing".to_string(),
Profile {
team_id: "T123".to_string(),
user_id: "U456".to_string(),
team_name: Some("Existing Team".to_string()),
user_name: None,
client_id: None,
redirect_uri: None,
scopes: None,
bot_scopes: None,
user_scopes: None,
default_token_type: None,
},
);
save_config(&config_path, &config).unwrap();
let mut payload = crate::auth::format::ExportPayload::new();
payload.profiles.insert(
"new_profile".to_string(),
ExportProfile {
team_id: "T789".to_string(),
user_id: "U101".to_string(),
team_name: Some("New Team".to_string()),
user_name: None,
token: "xoxb-new-token".to_string(),
client_id: None,
client_secret: None,
user_token: None,
},
);
let passphrase = "test-password";
let kdf_params = KdfParams {
salt: crypto::generate_salt(),
..Default::default()
};
let key = crypto::derive_key(passphrase, &kdf_params).unwrap();
let payload_json = serde_json::to_vec(&payload).unwrap();
let encrypted = crypto::encrypt(&payload_json, &key).unwrap();
let encoded = format::encode_export(&payload, &encrypted, &kdf_params).unwrap();
#[cfg(unix)]
write_secure_file(&import_path, &encoded).unwrap();
#[cfg(not(unix))]
std::fs::write(&import_path, &encoded).unwrap();
std::env::set_var("SLACK_RS_CONFIG_PATH", config_path.to_str().unwrap());
let token_store = crate::profile::FileTokenStore::with_path(tokens_path.clone()).unwrap();
let options = ImportOptions {
input_path: import_path.to_str().unwrap().to_string(),
passphrase: passphrase.to_string(),
yes: true,
force: false,
dry_run: true,
json: false,
};
let result = import_profiles(&token_store, &options).unwrap();
assert!(result.dry_run);
assert_eq!(result.profiles.len(), 1);
assert_eq!(result.profiles[0].profile_name, "new_profile");
assert_eq!(result.profiles[0].action, ImportAction::Updated);
let config_after = load_config(&config_path).unwrap();
assert_eq!(config_after.profiles.len(), 1);
assert!(config_after.get("new_profile").is_none());
assert!(config_after.get("existing").is_some());
let token_key = make_token_key("T789", "U101");
assert!(!token_store.exists(&token_key));
std::env::remove_var("SLACK_RS_TOKENS_PATH");
std::env::remove_var("SLACK_RS_CONFIG_PATH");
}
#[test]
#[serial_test::serial]
fn test_export_all_with_partial_skip() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("profiles.json");
let export_path = temp_dir.path().join("export.dat");
let tokens_path = temp_dir.path().join("tokens.json");
std::env::set_var("SLACK_RS_TOKENS_PATH", tokens_path.to_str().unwrap());
std::env::set_var("SLACK_RS_CONFIG_PATH", config_path.to_str().unwrap());
let mut config = ProfilesConfig::new();
config.set(
"profile1".to_string(),
Profile {
team_id: "T123".to_string(),
user_id: "U456".to_string(),
team_name: Some("Team 1".to_string()),
user_name: Some("User 1".to_string()),
client_id: None,
redirect_uri: None,
scopes: None,
bot_scopes: None,
user_scopes: None,
default_token_type: None,
},
);
config.set(
"profile2".to_string(),
Profile {
team_id: "T789".to_string(),
user_id: "U101".to_string(),
team_name: Some("Team 2".to_string()),
user_name: Some("User 2".to_string()),
client_id: None,
redirect_uri: None,
scopes: None,
bot_scopes: None,
user_scopes: None,
default_token_type: None,
},
);
save_config(&config_path, &config).unwrap();
let token_store = crate::profile::FileTokenStore::with_path(tokens_path.clone()).unwrap();
let token_key1 = make_token_key("T123", "U456");
token_store.set(&token_key1, "xoxb-token-1").unwrap();
let options = ExportOptions {
profile_name: None,
all: true,
output_path: export_path.to_str().unwrap().to_string(),
passphrase: "test-password".to_string(),
yes: true,
};
let result = export_profiles(&token_store, &options).unwrap();
assert_eq!(result.exported_count, 1);
assert_eq!(result.skipped_profiles.len(), 1);
assert!(result.skipped_profiles.contains(&"profile2".to_string()));
assert!(export_path.exists());
std::env::remove_var("SLACK_RS_TOKENS_PATH");
std::env::remove_var("SLACK_RS_CONFIG_PATH");
}
#[test]
#[serial_test::serial]
fn test_export_all_with_all_skipped() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("profiles.json");
let export_path = temp_dir.path().join("export.dat");
let tokens_path = temp_dir.path().join("tokens.json");
std::env::set_var("SLACK_RS_TOKENS_PATH", tokens_path.to_str().unwrap());
std::env::set_var("SLACK_RS_CONFIG_PATH", config_path.to_str().unwrap());
let mut config = ProfilesConfig::new();
config.set(
"profile1".to_string(),
Profile {
team_id: "T123".to_string(),
user_id: "U456".to_string(),
team_name: Some("Team 1".to_string()),
user_name: Some("User 1".to_string()),
client_id: None,
redirect_uri: None,
scopes: None,
bot_scopes: None,
user_scopes: None,
default_token_type: None,
},
);
config.set(
"profile2".to_string(),
Profile {
team_id: "T789".to_string(),
user_id: "U101".to_string(),
team_name: Some("Team 2".to_string()),
user_name: Some("User 2".to_string()),
client_id: None,
redirect_uri: None,
scopes: None,
bot_scopes: None,
user_scopes: None,
default_token_type: None,
},
);
save_config(&config_path, &config).unwrap();
let token_store = crate::profile::FileTokenStore::with_path(tokens_path.clone()).unwrap();
let options = ExportOptions {
profile_name: None,
all: true,
output_path: export_path.to_str().unwrap().to_string(),
passphrase: "test-password".to_string(),
yes: true,
};
let result = export_profiles(&token_store, &options);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ExportImportError::NoProfiles));
std::env::remove_var("SLACK_RS_TOKENS_PATH");
std::env::remove_var("SLACK_RS_CONFIG_PATH");
}
#[test]
#[serial_test::serial]
fn test_export_single_profile_with_missing_token_fails() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("profiles.json");
let export_path = temp_dir.path().join("export.dat");
let tokens_path = temp_dir.path().join("tokens.json");
std::env::set_var("SLACK_RS_TOKENS_PATH", tokens_path.to_str().unwrap());
std::env::set_var("SLACK_RS_CONFIG_PATH", config_path.to_str().unwrap());
let mut config = ProfilesConfig::new();
config.set(
"profile1".to_string(),
Profile {
team_id: "T123".to_string(),
user_id: "U456".to_string(),
team_name: Some("Team 1".to_string()),
user_name: Some("User 1".to_string()),
client_id: None,
redirect_uri: None,
scopes: None,
bot_scopes: None,
user_scopes: None,
default_token_type: None,
},
);
save_config(&config_path, &config).unwrap();
let token_store = crate::profile::FileTokenStore::with_path(tokens_path.clone()).unwrap();
let options = ExportOptions {
profile_name: Some("profile1".to_string()),
all: false,
output_path: export_path.to_str().unwrap().to_string(),
passphrase: "test-password".to_string(),
yes: true,
};
let result = export_profiles(&token_store, &options);
assert!(result.is_err());
match result.unwrap_err() {
ExportImportError::TokenNotFound(name) => {
assert_eq!(name, "profile1");
}
_ => panic!("Expected TokenNotFound error"),
}
std::env::remove_var("SLACK_RS_TOKENS_PATH");
std::env::remove_var("SLACK_RS_CONFIG_PATH");
}
#[test]
#[serial_test::serial]
fn test_export_import_with_user_token() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("profiles.json");
let export_path = temp_dir.path().join("export.dat");
let tokens_path = temp_dir.path().join("tokens.json");
std::env::set_var("SLACK_RS_TOKENS_PATH", tokens_path.to_str().unwrap());
std::env::set_var("SLACK_RS_CONFIG_PATH", config_path.to_str().unwrap());
let mut config = ProfilesConfig::new();
config.set(
"test".to_string(),
Profile {
team_id: "T123".to_string(),
user_id: "U456".to_string(),
team_name: Some("Test Team".to_string()),
user_name: Some("Test User".to_string()),
client_id: None,
redirect_uri: None,
scopes: None,
bot_scopes: None,
user_scopes: None,
default_token_type: None,
},
);
save_config(&config_path, &config).unwrap();
let token_store = crate::profile::FileTokenStore::with_path(tokens_path.clone()).unwrap();
let bot_token_key = make_token_key("T123", "U456");
let user_token_key = "T123:U456:user".to_string();
token_store.set(&bot_token_key, "xoxb-bot-token").unwrap();
token_store.set(&user_token_key, "xoxp-user-token").unwrap();
let export_options = ExportOptions {
profile_name: Some("test".to_string()),
all: false,
output_path: export_path.to_str().unwrap().to_string(),
passphrase: "test-password".to_string(),
yes: true,
};
let export_result = export_profiles(&token_store, &export_options).unwrap();
assert_eq!(export_result.exported_count, 1);
assert_eq!(export_result.skipped_profiles.len(), 0);
token_store.delete(&bot_token_key).ok();
token_store.delete(&user_token_key).ok();
let import_options = ImportOptions {
input_path: export_path.to_str().unwrap().to_string(),
passphrase: "test-password".to_string(),
yes: true,
force: false,
dry_run: false,
json: false,
};
let import_result = import_profiles(&token_store, &import_options).unwrap();
assert_eq!(import_result.summary.updated, 1);
assert_eq!(import_result.summary.skipped, 0);
let bot_token = token_store.get(&bot_token_key).unwrap();
assert_eq!(bot_token, "xoxb-bot-token");
let user_token = token_store.get(&user_token_key).unwrap();
assert_eq!(user_token, "xoxp-user-token");
std::env::remove_var("SLACK_RS_TOKENS_PATH");
std::env::remove_var("SLACK_RS_CONFIG_PATH");
}
#[test]
#[serial_test::serial]
fn test_export_user_token_only() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("profiles.json");
let export_path = temp_dir.path().join("export.dat");
let tokens_path = temp_dir.path().join("tokens.json");
std::env::set_var("SLACK_RS_TOKENS_PATH", tokens_path.to_str().unwrap());
std::env::set_var("SLACK_RS_CONFIG_PATH", config_path.to_str().unwrap());
let mut config = ProfilesConfig::new();
config.set(
"test".to_string(),
Profile {
team_id: "T123".to_string(),
user_id: "U456".to_string(),
team_name: Some("Test Team".to_string()),
user_name: Some("Test User".to_string()),
client_id: None,
redirect_uri: None,
scopes: None,
bot_scopes: None,
user_scopes: None,
default_token_type: None,
},
);
save_config(&config_path, &config).unwrap();
let token_store = crate::profile::FileTokenStore::with_path(tokens_path.clone()).unwrap();
let user_token_key = "T123:U456:user".to_string();
token_store.set(&user_token_key, "xoxp-user-token").unwrap();
let export_options = ExportOptions {
profile_name: Some("test".to_string()),
all: false,
output_path: export_path.to_str().unwrap().to_string(),
passphrase: "test-password".to_string(),
yes: true,
};
let export_result = export_profiles(&token_store, &export_options).unwrap();
assert_eq!(export_result.exported_count, 1);
assert_eq!(export_result.skipped_profiles.len(), 0);
token_store.delete(&user_token_key).ok();
let import_options = ImportOptions {
input_path: export_path.to_str().unwrap().to_string(),
passphrase: "test-password".to_string(),
yes: true,
force: false,
dry_run: false,
json: false,
};
let import_result = import_profiles(&token_store, &import_options).unwrap();
assert_eq!(import_result.summary.updated, 1);
let user_token = token_store.get(&user_token_key).unwrap();
assert_eq!(user_token, "xoxp-user-token");
std::env::remove_var("SLACK_RS_TOKENS_PATH");
std::env::remove_var("SLACK_RS_CONFIG_PATH");
}
}