use std::ffi::OsString;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use reqwest::{
StatusCode,
header::{HeaderMap, HeaderValue},
};
use super::{
OutputFormat, output_write_error, render_api_response_human, render_error, render_success,
structured_api_response,
};
use crate::{
command_result::{
AuthStatusEmail, AuthStatusSuccess, AuthStatusUser, AuthSuccess, BoardCountSuccess,
BoardCreateSuccess, BoardDeleteSuccess, BoardListScope, BoardListSuccess,
BoardRenameSuccess, BoardSummary, CancellationSuccess, CardCollectionSuccess,
CardCreateSuccess, CardDeleteMode, CardDeleteSuccess, CardSubmittedField, CardSummary,
CardUpdateSuccess, CommandSuccess, CommentCollectionSuccess, CommentCreateSuccess,
CommentDeleteMode, CommentDeleteSuccess, CommentDetail, CommentSummary,
DestructiveOperation, ListCollectionSuccess, ListCreateSuccess, ListDeleteMode,
ListDeleteSuccess, ListDetail, ListSummary, ListUpdateSuccess, ListUpdatedField,
ListWipLimitDetail, LogoutScope, LogoutSuccess, ProfileItem, ProfileListSuccess,
ProfileRemoveSuccess, SwimlaneCollectionSuccess, SwimlaneCreateSuccess, SwimlaneDeleteMode,
SwimlaneDeleteSuccess, SwimlaneDetail, SwimlaneSummary, SwimlaneUpdateSuccess,
SwimlaneUpdatedField, UserBoardSummary, UserBoardsSuccess, UserCard, UserCardsSuccess,
UserCreateSuccess, UserDeleteSuccess, UserDetail, UserEmail, UserListSuccess,
UserOwnershipSuccess, UserSummary,
},
error::AppError,
exit_code::StableExitCode,
};
#[test]
fn output_write_failures_use_the_transport_exit_status() {
let error = output_write_error(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"closed output",
));
assert_eq!(error.exit_code(), StableExitCode::Transport);
}
#[test]
fn bot_run_results_preserve_public_names_and_status_values() {
use crate::client::cards::bot_runs::BotRunReport;
use serde_json::json;
for status in [
"queued",
"running",
"awaiting_input",
"review",
"done",
"failed",
"stopped",
] {
let wire: BotRunReport = serde_json::from_value(json!({
"_id": "run\u{1b}[2J", "status": status,
}))
.unwrap();
let success = CommandSuccess::BotRunReported(wire.into());
let rendered: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &success)).unwrap();
assert_eq!(
rendered,
json!({
"ok": true,
"data": {"run_id": "run\u{1b}[2J", "status": status},
})
);
let human = render_success(OutputFormat::Human, &success);
assert!(human.contains("run_id:"));
assert!(human.contains(status));
assert!(!human.contains('\u{1b}'));
}
}
#[test]
fn inbound_email_results_preserve_nullable_ids_and_bounce_counts() {
use crate::client::integrations::inbound_email::{BounceReceipt, InboundEmailReceipt};
use serde_json::json;
for resource_id in [Some("card-1"), None] {
let wire: InboundEmailReceipt =
serde_json::from_value(json!({"_id": resource_id})).unwrap();
let success = CommandSuccess::InboundEmailSubmitted(wire.into());
let rendered: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &success)).unwrap();
assert_eq!(
rendered,
json!({
"ok": true, "data": {"resource_id": resource_id},
})
);
}
let wire: BounceReceipt = serde_json::from_value(json!({"ok": true, "recorded": 2})).unwrap();
let success = CommandSuccess::EmailBounceSubmitted(wire.into());
let rendered: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &success)).unwrap();
assert_eq!(
rendered,
json!({
"ok": true, "data": {"ok": true, "recorded": 2},
})
);
assert!(render_success(OutputFormat::Human, &success).contains("recorded: 2"));
}
fn success() -> CommandSuccess {
CommandSuccess::Registration(AuthSuccess {
server: "https://wekan.example/".to_owned(),
profile: "default".to_owned(),
user_id: "user-1".to_owned(),
token_expires: "2030-01-02T03:04:05Z".to_owned(),
credential_stored: true,
profile_created: true,
profile_active: true,
})
}
#[test]
fn renders_the_stable_json_success_envelope() {
let value: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &success())).unwrap();
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["user_id"], "user-1");
assert_eq!(value["data"]["credential_stored"], true);
assert!(value["data"].get("token").is_none());
}
#[test]
fn raw_output_detection_accepts_both_global_flag_forms() {
assert_eq!(
OutputFormat::detect_from_args(&[
OsString::from("wekan"),
OsString::from("--output"),
OsString::from("raw"),
]),
OutputFormat::Raw
);
assert_eq!(
OutputFormat::detect_from_args(&[OsString::from("wekan"), OsString::from("--output=raw"),]),
OutputFormat::Raw
);
}
#[test]
fn raw_api_structured_values_preserve_json_text_binary_and_header_bytes() {
let mut headers = HeaderMap::new();
headers.append("x-repeat", HeaderValue::from_static("one"));
headers.append("x-repeat", HeaderValue::from_static("two"));
headers.append("x-binary", HeaderValue::from_bytes(&[0x80, 0x81]).unwrap());
let json = structured_api_response(
StatusCode::OK,
&headers,
br#"{"unknown":{"nested":true}}"#.to_vec(),
);
assert_eq!(json.headers.len(), 3);
assert_eq!(json.http_status, 200);
assert_eq!(json.headers[2].value, BASE64.encode([0x80, 0x81]));
assert!(matches!(
json.body,
crate::command_result::RawResponseBody::Json { .. }
));
let text = structured_api_response(StatusCode::OK, &HeaderMap::new(), b"hello".to_vec());
assert!(matches!(
text.body,
crate::command_result::RawResponseBody::Text { ref value } if value == "hello"
));
let binary = structured_api_response(StatusCode::OK, &HeaderMap::new(), vec![0, 255]);
assert!(matches!(
binary.body,
crate::command_result::RawResponseBody::Base64 { ref value }
if value == &BASE64.encode([0, 255])
));
assert!(render_api_response_human(&binary).contains("2 binary bytes"));
}
#[test]
fn raw_api_structured_json_preserves_arbitrary_precision_numbers() {
let response = structured_api_response(
StatusCode::OK,
&HeaderMap::new(),
br#"{"integer":18446744073709551616,"decimal":0.123456789012345678901234567890}"#.to_vec(),
);
let crate::command_result::RawResponseBody::Json { value } = response.body else {
panic!("valid JSON should remain structured")
};
assert_eq!(value["integer"].to_string(), "18446744073709551616");
assert_eq!(
value["decimal"].to_string(),
"0.123456789012345678901234567890"
);
}
#[test]
fn raw_api_structured_response_renders_http_success() {
let response = structured_api_response(
StatusCode::ACCEPTED,
&HeaderMap::new(),
br#"{"accepted":true}"#.to_vec(),
);
let human = render_api_response_human(&response);
assert!(human.starts_with("HTTP 202 Accepted"));
assert!(!human.contains("Mutation"));
let json = serde_json::to_value(&response).unwrap();
assert!(json.get("mutation_confirmed").is_none());
assert!(json.get("mutation_attempted").is_none());
}
#[test]
fn cancellation_has_stable_human_and_json_output() {
let cancellation = CommandSuccess::Cancelled(CancellationSuccess::new(
DestructiveOperation::ProfileRemove,
));
assert_eq!(
render_success(OutputFormat::Human, &cancellation),
"Cancelled; no changes made."
);
let value: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &cancellation)).unwrap();
assert_eq!(value["ok"], true);
assert_eq!(value["data"]["cancelled"], true);
assert_eq!(value["data"]["operation"], "profile_remove");
}
#[test]
fn human_success_reports_metadata_without_a_token() {
let rendered = render_success(OutputFormat::Human, &success());
assert!(rendered.contains("user-1"));
assert!(rendered.contains("https://wekan.example/"));
assert!(rendered.contains("2030-01-02T03:04:05Z"));
assert!(rendered.contains("Credentials stored securely."));
assert!(!rendered.contains("token\":\""));
}
#[test]
fn human_success_escapes_terminal_control_sequences() {
let success = CommandSuccess::Registration(AuthSuccess {
server: "https://wekan.example/".to_owned(),
profile: "default".to_owned(),
user_id: "user\u{1b}]52;c;clipboard\u{7}\nnext-line".to_owned(),
token_expires: "2030-01-02T03:04:05Z".to_owned(),
credential_stored: true,
profile_created: false,
profile_active: true,
});
let rendered = render_success(OutputFormat::Human, &success);
assert!(!rendered.contains('\u{1b}'));
assert!(!rendered.contains('\u{7}'));
assert!(rendered.contains(r"user\u{1b}]52;c;clipboard\u{7}\nnext-line"));
}
#[test]
fn login_success_uses_the_same_secret_free_json_shape() {
let success = CommandSuccess::Login(AuthSuccess {
server: "https://wekan.example/".to_owned(),
profile: "default".to_owned(),
user_id: "user-1".to_owned(),
token_expires: "2030-01-02T03:04:05Z".to_owned(),
credential_stored: true,
profile_created: false,
profile_active: true,
});
let value: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &success)).unwrap();
assert_eq!(value["data"]["user_id"], "user-1");
assert!(value["data"].get("token").is_none());
assert!(render_success(OutputFormat::Human, &success).starts_with("Logged in user"));
}
#[test]
fn logout_success_reports_scope_and_local_credential_state() {
let success = CommandSuccess::Logout(LogoutSuccess {
server: "https://wekan.example/".to_owned(),
profile: "default".to_owned(),
logout_scope: LogoutScope::AllTokens,
remote_logout_completed: true,
credential_stored: false,
local_credential_removed: true,
});
let value: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &success)).unwrap();
assert_eq!(value["data"]["logout_scope"], "all_tokens");
assert_eq!(value["data"]["remote_logout_completed"], true);
assert_eq!(value["data"]["credential_stored"], false);
assert_eq!(value["data"]["local_credential_removed"], true);
assert!(value["data"].get("token").is_none());
assert!(render_success(OutputFormat::Human, &success).contains("All login tokens revoked"));
}
#[test]
fn local_only_output_warns_that_remote_tokens_were_not_revoked() {
let success = CommandSuccess::Logout(LogoutSuccess {
server: "https://wekan.example/\u{1b}]52;c;clipboard\u{7}".to_owned(),
profile: "default".to_owned(),
logout_scope: LogoutScope::LocalOnly,
remote_logout_completed: false,
credential_stored: false,
local_credential_removed: true,
});
let rendered = render_success(OutputFormat::Human, &success);
assert!(rendered.contains("No Wekan login tokens were revoked"));
assert!(!rendered.contains('\u{1b}'));
assert!(!rendered.contains('\u{7}'));
}
#[test]
fn local_only_output_distinguishes_an_already_absent_credential() {
let success = CommandSuccess::Logout(LogoutSuccess {
server: "https://wekan.example/".to_owned(),
profile: "default".to_owned(),
logout_scope: LogoutScope::LocalOnly,
remote_logout_completed: false,
credential_stored: false,
local_credential_removed: false,
});
let rendered = render_success(OutputFormat::Human, &success);
assert!(rendered.contains("No stored credential was present"));
assert!(rendered.contains("No Wekan login tokens were revoked"));
}
#[test]
fn remote_output_reports_that_a_newer_credential_was_preserved() {
let success = CommandSuccess::Logout(LogoutSuccess {
server: "https://wekan.example/".to_owned(),
profile: "default".to_owned(),
logout_scope: LogoutScope::CurrentToken,
remote_logout_completed: true,
credential_stored: true,
local_credential_removed: false,
});
assert!(
render_success(OutputFormat::Human, &success)
.contains("A newer stored credential was preserved")
);
}
#[test]
fn all_tokens_output_requires_verification_of_a_changed_credential() {
let success = CommandSuccess::Logout(LogoutSuccess {
server: "https://wekan.example/".to_owned(),
profile: "default".to_owned(),
logout_scope: LogoutScope::AllTokens,
remote_logout_completed: true,
credential_stored: true,
local_credential_removed: false,
});
assert!(render_success(OutputFormat::Human, &success).contains("verify it before use"));
}
#[test]
fn status_success_uses_the_stable_nested_profile_shape() {
let success = CommandSuccess::AuthStatus(AuthStatusSuccess {
server: "https://wekan.example/".to_owned(),
profile: "default".to_owned(),
authenticated: true,
token_expires: "2030-01-02T03:04:05Z".to_owned(),
credential_stored: true,
user: AuthStatusUser {
user_id: "user-1".to_owned(),
username: Some("alice".to_owned()),
full_name: None,
is_admin: Some(false),
emails: vec![AuthStatusEmail {
address: Some("alice@example.com".to_owned()),
verified: None,
}],
},
});
let value: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &success)).unwrap();
assert_eq!(value["data"]["authenticated"], true);
assert_eq!(value["data"]["user"]["user_id"], "user-1");
assert_eq!(value["data"]["user"]["full_name"], serde_json::Value::Null);
assert_eq!(
value["data"]["user"]["emails"][0]["verified"],
serde_json::Value::Null
);
assert!(value["data"].get("token").is_none());
}
#[test]
fn human_status_omits_absent_profile_lines_and_escapes_controls() {
let success = CommandSuccess::AuthStatus(AuthStatusSuccess {
server: "https://wekan.example/".to_owned(),
profile: "default".to_owned(),
authenticated: true,
token_expires: "2030-01-02T03:04:05Z".to_owned(),
credential_stored: true,
user: AuthStatusUser {
user_id: "user-1".to_owned(),
username: Some("alice\u{1b}]52;c;clipboard\u{7}".to_owned()),
full_name: None,
is_admin: None,
emails: Vec::new(),
},
});
let rendered = render_success(OutputFormat::Human, &success);
assert!(!rendered.contains('\u{1b}'));
assert!(!rendered.contains("Full name:"));
assert!(!rendered.contains("Administrator:"));
assert!(!rendered.contains("Emails:"));
assert!(rendered.contains("Credentials stored securely."));
}
#[test]
fn profile_list_output_is_stable_and_human_readable() {
let success = CommandSuccess::ProfileList(ProfileListSuccess {
active_profile: Some("local".to_owned()),
profiles: vec![
ProfileItem {
name: "local".to_owned(),
server: "http://localhost:3000/".to_owned(),
active: true,
},
ProfileItem {
name: "work".to_owned(),
server: "https://wekan.example/".to_owned(),
active: false,
},
],
});
let value: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &success)).unwrap();
assert_eq!(value["data"]["active_profile"], "local");
assert_eq!(value["data"]["profiles"][0]["active"], true);
let human = render_success(OutputFormat::Human, &success);
assert_eq!(
human
.lines()
.next()
.unwrap()
.split_whitespace()
.collect::<Vec<_>>(),
["NAME", "SERVER", "ACTIVE"]
);
assert!(human.contains("http://localhost:3000/"));
}
#[test]
fn empty_profile_list_and_removal_report_the_result() {
let empty = CommandSuccess::ProfileList(ProfileListSuccess {
active_profile: None,
profiles: Vec::new(),
});
assert_eq!(
render_success(OutputFormat::Human, &empty),
"No profiles configured."
);
let removed = CommandSuccess::ProfileRemoved(ProfileRemoveSuccess {
name: "local".to_owned(),
server: "http://localhost:3000/".to_owned(),
removed: true,
active_profile: None,
});
let value: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &removed)).unwrap();
assert_eq!(value["data"]["removed"], true);
assert_eq!(value["data"]["active_profile"], serde_json::Value::Null);
assert!(render_success(OutputFormat::Human, &removed).contains("No profile is active"));
}
#[test]
fn auth_output_identifies_profile_and_initialization_state() {
let named = CommandSuccess::Login(AuthSuccess {
server: "https://wekan.example/".to_owned(),
profile: "work".to_owned(),
user_id: "user-1".to_owned(),
token_expires: "2030-01-02T03:04:05Z".to_owned(),
credential_stored: true,
profile_created: true,
profile_active: false,
});
let value: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &named)).unwrap();
assert_eq!(value["data"]["profile"], "work");
assert_eq!(value["data"]["profile_created"], true);
assert_eq!(value["data"]["profile_active"], false);
assert!(render_success(OutputFormat::Human, &named).contains("Profile: work"));
}
#[test]
fn renders_the_stable_json_error_envelope() {
let value: serde_json::Value = serde_json::from_str(&render_error(
OutputFormat::Json,
&AppError::configuration("missing server"),
))
.unwrap();
assert_eq!(value["ok"], false);
assert_eq!(value["error"]["code"], "configuration_error");
assert_eq!(value["error"]["details"], serde_json::json!({}));
}
#[test]
fn user_detail_output_is_typed_secret_free_and_terminal_safe() {
let profile_map = serde_json::json!({
"object\u{1b}]52;c;clipboard\u{7}": {
"board_id": "value\u{1b}[2J",
"swimlane_id": "lane\r\n",
"list_id": "Café",
"card_id": null
}
});
let success = CommandSuccess::UserCurrent(UserDetail {
passkeys: None,
sqrl: None,
web_push_subscriptions: None,
ntfy: None,
notification_delivery_pending: None,
notification_delivery_revision: None,
feature_preview: None,
inactivity_clock_start: None,
login_disabled_reason: None,
anonymized: None,
anonymized_at: None,
profile: Box::new(crate::command_result::UserProfileDetail {
chat_view_layout: None,
email_to_card: None,
notification_delivery: None,
my_dependencies: None,
notify_muted_activities: None,
notify_override_tray: None,
notify_override_email: None,
webhook_hide_identity: None,
unread_changes_since: None,
keep_title_tags: None,
checklist_ding_sound: None,
dismissed_board_announcements: None,
card_label_text_override: None,
show_label_text_override: None,
board_labels: None,
hidden_boards: None,
starred_swimlanes: None,
starred_lists: None,
starred_cards: None,
default_board_template_id: None,
default_board_template_board_id: None,
board_views: None,
show_board_dependencies: None,
show_my_dependencies: None,
board_visits: None,
dismissed_rule_suggestions: None,
date_format_override: None,
move_and_copy_dialog: Some(std::collections::BTreeMap::from([(
"object\u{1b}]52;c;clipboard\u{7}".to_owned(),
crate::command_result::UserDialogOptionsDetail {
board_id: "value\u{1b}[2J".to_owned(),
swimlane_id: "lane\r\n".to_owned(),
list_id: "Café".to_owned(),
card_id: None,
},
)])),
..Default::default()
}),
created_through_api: None,
heartbeat: None,
session_data: None,
import_usernames: vec![],
user_id: "user-1".to_owned(),
username: Some("alice\u{1b}]52;c;clipboard\u{7}".to_owned()),
full_name: Some("Alice".to_owned()),
emails: vec![UserEmail {
address: Some("alice@example.com".to_owned()),
verified: Some(true),
}],
is_admin: Some(false),
login_disabled: Some(false),
authentication_method: Some("password".to_owned()),
created_at: None,
modified_at: None,
last_connection_date: None,
organizations: Vec::new(),
teams: Vec::new(),
boards: Vec::new(),
});
let json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &success)).unwrap();
assert_eq!(json["data"]["user_id"], "user-1");
assert_eq!(json["data"]["profile"]["move_and_copy_dialog"], profile_map);
for forbidden in [
"services",
"sessionData",
"password",
"token",
"preferences",
] {
assert!(json["data"].get(forbidden).is_none());
}
let human = render_success(OutputFormat::Human, &success);
assert!(!human.contains('\u{1b}'));
assert!(!human.contains('\u{7}'));
assert!(human.contains(r"alice\u{1b}]52;c;clipboard\u{7}"));
for escaped in [
r"object\u{1b}]52;c;clipboard\u{7}:",
r"board_id: value\u{1b}[2J",
r"swimlane_id: lane\r\n",
"card_id: null",
"list_id: Café",
] {
assert!(
human.contains(escaped),
"missing escaped profile field: {escaped}"
);
}
assert!(
human
.chars()
.all(|character| !character.is_control() || character == '\n')
);
let CommandSuccess::UserCurrent(mut user) = success else {
unreachable!()
};
user.login_disabled = None;
let disabled =
CommandSuccess::UserLoginChanged(crate::command_result::UserLoginChangeSuccess {
action: crate::command_result::UserLoginAction::Disabled,
user,
});
let human = render_success(OutputFormat::Human, &disabled);
assert!(!human.contains("Disabled login for"));
assert!(human.contains("Login-disable request left unchanged"));
assert!(!human.contains("still permits new REST logins"));
let json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &disabled)).unwrap();
assert_eq!(
json["data"]["user"]["login_disabled"],
serde_json::Value::Null
);
assert!(json.get("warnings").is_none());
}
#[test]
fn user_lists_and_cards_have_stable_empty_and_escaped_output() {
let users = CommandSuccess::UserList(UserListSuccess { users: Vec::new() });
assert_eq!(
render_success(OutputFormat::Human, &users),
"No users found."
);
let user_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &users)).unwrap();
assert_eq!(user_json["data"]["users"], serde_json::json!([]));
let cards = CommandSuccess::UserCards(UserCardsSuccess {
cards: vec![UserCard {
card_id: "card-1".to_owned(),
title: Some("Task\nnext".to_owned()),
board_id: Some("board-1".to_owned()),
swimlane_id: None,
list_id: Some("list-1".to_owned()),
due_at: None,
start_at: None,
end_at: None,
members: Vec::new(),
assignees: Vec::new(),
}],
});
let human = render_success(OutputFormat::Human, &cards);
assert!(human.starts_with("ID TITLE BOARD LIST DUE"));
assert!(human.contains(r"Task\nnext"));
assert_eq!(human.lines().count(), 2);
}
#[test]
fn user_creation_and_mutation_results_are_stable() {
let created = CommandSuccess::UserCreated(UserCreateSuccess {
username: "bob".to_owned(),
email: "bob@example.com".to_owned(),
user_id: "user-1".into(),
});
let json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &created)).unwrap();
assert!(json["data"].get("creation_requested").is_none());
assert!(json["data"].get("mutation_confirmed").is_none());
assert!(json["data"].get("created").is_none());
assert_eq!(json["data"]["user_id"], "user-1");
assert_eq!(
json["data"],
serde_json::json!({"username":"bob", "email":"bob@example.com", "user_id":"user-1"})
);
assert_eq!(
render_success(OutputFormat::Human, &created),
"Created user bob (bob@example.com).\nUser ID: user-1"
);
let ownership = CommandSuccess::UserOwnershipTaken(UserOwnershipSuccess {
from_user_id: "owner-1".to_owned(),
to_user_id: "admin-1".to_owned(),
boards: vec![UserBoardSummary {
board_id: "board-1".to_owned(),
title: "Board".to_owned(),
}],
});
let json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &ownership)).unwrap();
assert_eq!(json["data"]["boards"][0]["board_id"], "board-1");
assert!(json.get("warnings").is_none());
assert!(
render_success(OutputFormat::Human, &ownership)
.contains("Ownership transfer requested for 1 board")
);
let boards = CommandSuccess::UserBoards(UserBoardsSuccess {
user_id: "user-1".to_owned(),
boards: vec![UserBoardSummary {
board_id: "board-1".to_owned(),
title: "Board\u{1b}".to_owned(),
}],
});
assert!(!render_success(OutputFormat::Human, &boards).contains('\u{1b}'));
let deleted = CommandSuccess::UserDeleted(UserDeleteSuccess {
user_id: "user-1".to_owned(),
deleted: true,
deleted_current_user: true,
credential_stored: false,
local_credential_removed: true,
});
let json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &deleted)).unwrap();
assert_eq!(json["data"]["deleted"], true);
assert!(json["data"].get("deletion_requested").is_none());
assert_eq!(json["data"]["deleted_current_user"], true);
assert_eq!(json["data"]["local_credential_removed"], true);
let rendered = render_success(OutputFormat::Human, &deleted);
assert!(rendered.contains("Deleted user user-1"));
assert!(rendered.contains("local credential"));
let list = CommandSuccess::UserList(UserListSuccess {
users: vec![UserSummary {
user_id: "user-1".to_owned(),
username: Some("alice".to_owned()),
}],
});
assert!(render_success(OutputFormat::Human, &list).starts_with("ID USERNAME"));
let email_only = CommandSuccess::UserList(UserListSuccess {
users: vec![UserSummary {
user_id: "email-only".to_owned(),
username: None,
}],
});
assert!(render_success(OutputFormat::Human, &email_only).contains("<none>"));
}
#[test]
fn defective_endpoints_report_requests_without_completion_claims() {
use crate::command_result::*;
let cases = [
(
CommandSuccess::UserTokenDeleted(UserTokenDeleteSuccess {
user_id: "u".into(),
scope: UserTokenDeleteScope::Single,
message: "Server says deleted".into(),
}),
"Single-token deletion request accepted",
),
(
CommandSuccess::DependencyUpdated(DependencyUpdateSuccess {
board_id: "b".into(),
card_id: "c".into(),
target_card_id: "absent".into(),
submitted_fields: vec![DependencySubmittedField::Color],
}),
"Dependency update request accepted",
),
(
CommandSuccess::IntegrationDeleted(IntegrationDeleteSuccess {
board_id: "b".into(),
integration_id: "absent".into(),
}),
"Integration deletion request accepted",
),
(
CommandSuccess::CustomFieldItemsCreated(CustomFieldItemCreateSuccess {
board_id: "b".into(),
custom_field_id: "absent".into(),
}),
"Dropdown item creation request accepted",
),
(
CommandSuccess::ListMoved(ListTransferSuccess {
source_board_id: "a".into(),
source_list_id: "l".into(),
target_board_id: "b".into(),
target_swimlane_id: None,
returned_list_id: "l".into(),
}),
"Cross-board list move request accepted (source list retained; position ignored)",
),
];
for (success, heading) in cases {
let human = render_success(OutputFormat::Human, &success);
assert!(human.starts_with(heading), "{human}");
let json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &success)).unwrap();
assert_eq!(json["ok"], true);
assert!(json.get("warnings").is_none());
assert!(!human.contains("unconfirmed"));
assert!(!human.contains("pending confirmation"));
}
let copied = CommandSuccess::BoardCopied(BoardCopySuccess {
source_board_id: "a".into(),
board_id: "b".into(),
});
let human = render_success(OutputFormat::Human, &copied);
assert!(human.starts_with("Board copied:"));
assert!(!human.contains("omits attachments"));
let json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &copied)).unwrap();
assert!(json.get("warnings").is_none());
let delete_all = CommandSuccess::UserTokenDeleted(UserTokenDeleteSuccess {
user_id: "u".into(),
scope: UserTokenDeleteScope::All,
message: "done".into(),
});
assert!(render_success(OutputFormat::Human, &delete_all).starts_with("Deleted all tokens"));
let json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &delete_all)).unwrap();
assert!(json.get("warnings").is_none());
}
#[test]
fn detects_json_output_before_clap_parsing() {
assert_eq!(
OutputFormat::detect_from_args(&[
OsString::from("wekan"),
OsString::from("auth"),
OsString::from("--output=json"),
OsString::from("register"),
]),
OutputFormat::Json
);
}
#[test]
fn board_outputs_are_stable_comprehensive_and_terminal_safe() {
let list = CommandSuccess::BoardList(BoardListSuccess {
scope: BoardListScope::Public,
boards: vec![BoardSummary {
board_id: "board-1".to_owned(),
title: "Board\nnext".to_owned(),
}],
});
let list_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &list)).unwrap();
assert_eq!(list_json["data"]["scope"], "public");
assert_eq!(list_json["data"]["boards"][0]["board_id"], "board-1");
assert!(render_success(OutputFormat::Human, &list).contains(r"Board\nnext"));
let board = serde_json::from_value(serde_json::json!({
"board_id": "board-1",
"title": "Board\u{1b}]52;c;clipboard\u{7}",
"permission": "private",
"allows_comments": true,
"members": [{"user_id": "user-1", "is_admin": true, "is_active": true}],
"watchers": [{"user_id": "watcher-1", "level": "tracking"}],
"unknownSecret": "must-not-render"
}))
.unwrap();
let shown = CommandSuccess::BoardShown(Box::new(board));
let shown_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &shown)).unwrap();
assert_eq!(shown_json["data"]["board_id"], "board-1");
assert_eq!(shown_json["data"]["allows_comments"], true);
assert_eq!(shown_json["data"]["watchers"][0]["level"], "tracking");
assert!(shown_json["data"].get("unknownSecret").is_none());
let shown_human = render_success(OutputFormat::Human, &shown);
assert!(!shown_human.contains('\u{1b}'));
assert!(!shown_human.contains('\u{7}'));
assert!(!shown_human.contains("must-not-render"));
assert!(shown_human.contains("Settings:"));
assert!(shown_human.contains("watchers: [1]"));
let count = CommandSuccess::BoardCount(BoardCountSuccess {
instance: 0,
private: 4,
public: 2,
});
assert!(render_success(OutputFormat::Human, &count).contains("Private boards: 4"));
let created = CommandSuccess::BoardCreated(BoardCreateSuccess {
board_id: "board-1".to_owned(),
default_swimlane_id: "swimlane-1".to_owned(),
});
let renamed = CommandSuccess::BoardRenamed(BoardRenameSuccess {
board_id: "board-1".to_owned(),
title: "Renamed".to_owned(),
});
let deleted = CommandSuccess::BoardDeleted(BoardDeleteSuccess {
board_id: "board-1".to_owned(),
deleted: true,
});
assert!(render_success(OutputFormat::Human, &created).contains("swimlane-1"));
assert!(render_success(OutputFormat::Human, &renamed).contains("Renamed"));
assert!(render_success(OutputFormat::Human, &deleted).contains("Deleted board"));
}
#[test]
fn list_outputs_are_stable_comprehensive_and_terminal_safe() {
let collection = CommandSuccess::ListCollection(ListCollectionSuccess {
board_id: "board-1".to_owned(),
lists: vec![ListSummary {
list_id: "list-1".to_owned(),
title: "Todo\nnext".to_owned(),
modified_at: Some("2026-08-28T00:00:00Z".to_owned()),
cards_modified_at: None,
}],
});
let collection_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &collection)).unwrap();
assert_eq!(collection_json["data"]["board_id"], "board-1");
assert_eq!(
collection_json["data"]["lists"][0]["cards_modified_at"],
serde_json::Value::Null
);
assert!(render_success(OutputFormat::Human, &collection).contains(r"Todo\nnext"));
let shown = CommandSuccess::ListShown(ListDetail {
scrum: None,
role_exceptions: None,
sync_source: None,
scrum_revision: None,
cards_use_list_color: None,
linked_board_id: None,
linked_list_id: None,
sync_revision: None,
sync_credential_generation: None,
sync_credential_fence: None,
sync_write_back_error: None,
sync_credential_incarnation: None,
watchers: None,
list_id: "list-1".to_owned(),
title: "Todo\u{1b}]52;c;x\u{7}".to_owned(),
starred: Some(true),
archived: false,
archived_at: None,
deleted_at: None,
deleted_by: None,
delete_batch_id: None,
board_id: "board-1".to_owned(),
swimlane_id: Some("swimlane-1".to_owned()),
created_at: "2026-08-28T00:00:00Z".to_owned(),
sort: Some(1.into()),
updated_at: None,
modified_at: "2026-08-28T00:00:00Z".to_owned(),
position_updated_at: None,
wip_limit: Some(ListWipLimitDetail {
value: 2.into(),
enabled: true,
soft: false,
}),
color: Some("silver".to_owned()),
list_type: "list".to_owned(),
width: Some(220.into()),
});
let shown_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &shown)).unwrap();
assert_eq!(shown_json["data"]["wip_limit"]["value"], 2);
assert_eq!(shown_json["data"]["list_type"], "list");
let shown_human = render_success(OutputFormat::Human, &shown);
assert!(!shown_human.contains('\u{1b}'));
assert!(!shown_human.contains('\u{7}'));
let created = CommandSuccess::ListCreated(ListCreateSuccess {
board_id: "board-1".to_owned(),
list_id: "list-1".to_owned(),
});
let updated = CommandSuccess::ListUpdated(ListUpdateSuccess {
board_id: "board-1".to_owned(),
list_id: "list-1".to_owned(),
updated_fields: vec![ListUpdatedField::Title, ListUpdatedField::WipLimit],
});
let deleted = CommandSuccess::ListDeleted(ListDeleteSuccess {
board_id: "board-1".to_owned(),
list_id: "list-1".to_owned(),
deletion_requested: true,
delete_mode: ListDeleteMode::Soft,
});
let updated_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &updated)).unwrap();
assert_eq!(
updated_json["data"]["updated_fields"],
serde_json::json!(["title", "wip_limit"])
);
let deleted_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &deleted)).unwrap();
assert_eq!(deleted_json["data"]["delete_mode"], "soft");
assert!(render_success(OutputFormat::Human, &created).contains("Created list"));
assert!(render_success(OutputFormat::Human, &updated).contains("title, wip_limit"));
assert!(
render_success(OutputFormat::Human, &deleted).contains("List deletion request accepted")
);
}
#[test]
fn card_outputs_are_stable_comprehensive_and_terminal_safe() {
let collection = CommandSuccess::CardCollection(CardCollectionSuccess {
board_id: "board-1".to_owned(),
list_id: "list-1".to_owned(),
cards: vec![CardSummary {
card_id: "card-1".to_owned(),
title: Some("Todo\nnext".to_owned()),
description: Some("Details".to_owned()),
swimlane_id: Some("swimlane-1".to_owned()),
received_at: None,
start_at: None,
due_at: Some("2030-01-02T03:04:05Z".to_owned()),
end_at: None,
assignees: vec!["user-1".to_owned()],
sort: Some(2.into()),
}],
});
let collection_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &collection)).unwrap();
assert_eq!(collection_json["data"]["list_id"], "list-1");
assert_eq!(
collection_json["data"]["cards"][0]["received_at"],
serde_json::Value::Null
);
let collection_human = render_success(OutputFormat::Human, &collection);
assert!(collection_human.contains(r"Todo\nnext"));
assert!(collection_human.contains("Details"));
assert!(collection_human.contains("user-1"));
let card = crate::command_result::CardDetail {
scrum: None,
drawings: None,
custom_field_links: None,
sync_last_source: None,
email: None,
scrum_revision: None,
parent_ids: None,
map_x: None,
map_y: None,
column_set_values: None,
card_kind: None,
last_recurrence_at: None,
sync_source_key: None,
sync_label_ids: None,
sync_issue_state: None,
sync_closed_by_we_kan: None,
sync_external_id: None,
sync_source_type: None,
attached_card_ids: None,
flow_start_at: None,
flow_user_id: None,
pomodoro_start_at: None,
pomodoro_phase: None,
pomodoro_user_id: None,
show_list_on_card: None,
checklists: None,
list_entered_at: None,
recurrence_interval: None,
last_move_reason: None,
flow_interruptions: None,
pomodoro_count: None,
pomodoro_work_minutes: None,
watchers: None,
card_id: "card-1".to_owned(),
title: Some("Todo\u{001b}]52;c;x\u{0007}".to_owned()),
archived: false,
archived_at: None,
deleted_at: None,
deleted_by: None,
delete_batch_id: None,
parent_id: None,
list_id: None,
swimlane_id: "swimlane-1".to_owned(),
board_id: None,
cover_id: None,
color: None,
created_at: "2030-01-02T03:04:05Z".to_owned(),
modified_at: "2030-01-02T03:04:05Z".to_owned(),
custom_fields: vec![],
date_last_activity: "2030-01-02T03:04:05Z".to_owned(),
description: None,
requested_by: None,
assigned_by: None,
label_ids: vec![],
members: vec![],
assignees: vec![],
requesters: vec![],
assigners: vec![],
received_at: None,
start_at: None,
due_at: None,
end_at: None,
due_complete: None,
stickers: vec![],
location_name: None,
location_address: None,
location_latitude: None,
location_longitude: None,
locations: vec![],
spent_time: None,
is_overtime: None,
user_id: "user-1".to_owned(),
sort: None,
subtask_sort: None,
card_type: "cardType-card".to_owned(),
linked_id: None,
card_dependencies: vec![],
vote: None,
poker: None,
target_id_gantt: vec![],
link_type_gantt: vec![],
link_id_gantt: vec![],
card_number: None,
show_activities: false,
show_list_on_minicard: None,
show_checklist_at_minicard: None,
hide_finished_checklist_if_items_are_hidden: None,
};
let shown = CommandSuccess::CardShown(Box::new(card));
let shown_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &shown)).unwrap();
assert_eq!(shown_json["data"]["card_type"], "cardType-card");
assert_eq!(shown_json["data"]["members"], serde_json::json!([]));
assert_eq!(shown_json["data"]["parent_id"], serde_json::Value::Null);
let shown_human = render_success(OutputFormat::Human, &shown);
assert!(!shown_human.contains('\u{1b}'));
assert!(!shown_human.contains('\u{7}'));
assert!(shown_human.contains("parent_id: null"));
assert!(shown_human.contains("members: [0]"));
let created = CommandSuccess::CardCreated(CardCreateSuccess {
board_id: "board-1".to_owned(),
list_id: "list-1".to_owned(),
card_id: "card-1".to_owned(),
});
let updated = CommandSuccess::CardUpdated(CardUpdateSuccess {
board_id: "board-1".to_owned(),
list_id: "list-1".to_owned(),
card_id: "card-1".to_owned(),
submitted_fields: vec![
CardSubmittedField::Title,
CardSubmittedField::Sort,
CardSubmittedField::DueAt,
CardSubmittedField::IsOverTime,
],
});
let deleted = CommandSuccess::CardDeleted(CardDeleteSuccess {
board_id: "board-1".to_owned(),
list_id: "list-1".to_owned(),
card_id: "card-1".to_owned(),
deletion_requested: true,
delete_mode: CardDeleteMode::Hard,
});
assert!(render_success(OutputFormat::Human, &created).contains("Created card"));
let updated_human = render_success(OutputFormat::Human, &updated);
assert!(updated_human.contains("Card update request accepted"));
assert!(updated_human.contains("fields: title, sort, due_at, is_over_time"));
let updated_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &updated)).unwrap();
assert_eq!(
updated_json["data"]["submitted_fields"],
serde_json::json!(["title", "sort", "due_at", "is_over_time"])
);
assert!(updated_json["data"].get("updated_fields").is_none());
assert!(render_success(OutputFormat::Human, &deleted).contains("deletion request accepted"));
let deleted_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &deleted)).unwrap();
assert_eq!(deleted_json["data"]["delete_mode"], "hard");
}
#[test]
fn comment_outputs_are_stable_comprehensive_and_terminal_safe() {
let collection = CommandSuccess::CommentCollection(CommentCollectionSuccess {
board_id: "board-1".to_owned(),
card_id: "card-1".to_owned(),
comments: vec![CommentSummary {
comment_id: "comment-1".to_owned(),
text: "Hello\nnext".to_owned(),
author_id: "user-1".to_owned(),
}],
});
let collection_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &collection)).unwrap();
assert_eq!(collection_json["data"]["card_id"], "card-1");
assert_eq!(
collection_json["data"]["comments"][0]["comment_id"],
"comment-1"
);
let collection_human = render_success(OutputFormat::Human, &collection);
assert!(collection_human.contains(r"Hello\nnext"));
assert!(collection_human.contains("user-1"));
let empty = CommandSuccess::CommentCollection(CommentCollectionSuccess {
board_id: "board-1".to_owned(),
card_id: "card-1".to_owned(),
comments: vec![],
});
assert_eq!(
render_success(OutputFormat::Human, &empty),
"No comments found on card card-1 on board board-1."
);
let shown = CommandSuccess::CommentShown(CommentDetail {
comment_id: "comment-1".to_owned(),
board_id: "board-1".to_owned(),
card_id: "card-1".to_owned(),
text: "Hello\u{1b}]52;c;x\u{7}".to_owned(),
parent_id: None,
created_at: "2030-01-02T03:04:05Z".to_owned(),
modified_at: "2030-01-02T03:05:05Z".to_owned(),
author_id: "user-1".to_owned(),
sync_comment_id: None,
});
let shown_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &shown)).unwrap();
assert_eq!(shown_json["data"]["parent_id"], serde_json::Value::Null);
assert_eq!(shown_json["data"]["author_id"], "user-1");
let shown_human = render_success(OutputFormat::Human, &shown);
assert!(!shown_human.contains('\u{1b}'));
assert!(!shown_human.contains('\u{7}'));
assert!(!shown_human.contains("parent_id"));
assert!(shown_human.contains("created_at: 2030-01-02T03:04:05Z"));
let created = CommandSuccess::CommentCreated(CommentCreateSuccess {
board_id: "board-1".to_owned(),
card_id: "card-1".to_owned(),
comment_id: "comment-1".to_owned(),
});
let deleted = CommandSuccess::CommentDeleted(CommentDeleteSuccess {
board_id: "board-1".to_owned(),
card_id: "card-1".to_owned(),
comment_id: "comment-1".to_owned(),
deleted: true,
delete_mode: CommentDeleteMode::Hard,
});
assert!(render_success(OutputFormat::Human, &created).contains("Created comment"));
assert!(render_success(OutputFormat::Human, &deleted).contains("Permanently deleted comment"));
let deleted_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &deleted)).unwrap();
assert_eq!(deleted_json["data"]["delete_mode"], "hard");
}
#[test]
fn archive_restore_outputs_have_exact_envelopes_and_escape_terminal_controls() {
use crate::command_result::{CardArchiveSuccess, CardRestoreSuccess};
let archive = CommandSuccess::CardArchived(CardArchiveSuccess {
board_id: "board\n1".to_owned(),
list_id: "list\t1".to_owned(),
card_id: "card\u{1b}]52;c;x\u{7}".to_owned(),
archived: true,
archived_at: "2030-01-02T03:04:05.000Z".to_owned(),
});
let restore = CommandSuccess::CardRestored(CardRestoreSuccess {
board_id: "board\n1".to_owned(),
list_id: "list\t1".to_owned(),
card_id: "card\u{1b}]52;c;x\u{7}".to_owned(),
archived: false,
});
for (result, expected) in [
(
&archive,
serde_json::json!({"ok":true,"data":{"board_id":"board\n1","list_id":"list\t1","card_id":"card\u{1b}]52;c;x\u{7}","archived":true,"archived_at":"2030-01-02T03:04:05.000Z"}}),
),
(
&restore,
serde_json::json!({"ok":true,"data":{"board_id":"board\n1","list_id":"list\t1","card_id":"card\u{1b}]52;c;x\u{7}","archived":false}}),
),
] {
let actual: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, result)).unwrap();
assert_eq!(actual, expected);
let human = render_success(OutputFormat::Human, result);
for control in ['\n', '\t', '\u{1b}', '\u{7}'] {
assert!(!human.contains(control));
}
assert!(human.contains(r"board\n1"));
assert!(human.contains(r"list\t1"));
}
assert!(render_success(OutputFormat::Human, &archive).contains("Archived card"));
assert!(render_success(OutputFormat::Human, &archive).contains("2030-01-02T03:04:05.000Z"));
assert!(render_success(OutputFormat::Human, &restore).contains("Restored card"));
}
#[test]
fn swimlane_outputs_are_stable_comprehensive_and_terminal_safe() {
let collection = CommandSuccess::SwimlaneCollection(SwimlaneCollectionSuccess {
board_id: "board-1".to_owned(),
swimlanes: vec![SwimlaneSummary {
swimlane_id: "swimlane-1".to_owned(),
title: "Delivery\nnext".to_owned(),
}],
});
let collection_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &collection)).unwrap();
assert_eq!(collection_json["data"]["board_id"], "board-1");
assert_eq!(
collection_json["data"]["swimlanes"][0]["swimlane_id"],
"swimlane-1"
);
assert!(render_success(OutputFormat::Human, &collection).contains(r"Delivery\nnext"));
let shown = CommandSuccess::SwimlaneShown(SwimlaneDetail {
scrum: None,
scrum_revision: None,
swimlane_id: "swimlane-1".to_owned(),
title: "Delivery\u{1b}]52;c;x\u{7}".to_owned(),
archived: false,
archived_at: None,
board_id: "board-1".to_owned(),
created_at: "2026-08-28T00:00:00Z".to_owned(),
sort: Some(2.into()),
color: Some("silver".to_owned()),
updated_at: Some("2026-08-28T00:30:00Z".to_owned()),
modified_at: "2026-08-28T01:00:00Z".to_owned(),
swimlane_type: "swimlane".to_owned(),
height: Some((-1).into()),
});
let shown_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &shown)).unwrap();
assert_eq!(shown_json["data"]["swimlane_type"], "swimlane");
assert_eq!(shown_json["data"]["height"], -1);
let shown_human = render_success(OutputFormat::Human, &shown);
assert!(!shown_human.contains('\u{1b}'));
assert!(!shown_human.contains('\u{7}'));
let created = CommandSuccess::SwimlaneCreated(SwimlaneCreateSuccess {
board_id: "board-1".to_owned(),
swimlane_id: "swimlane-1".to_owned(),
});
let updated = CommandSuccess::SwimlaneUpdated(SwimlaneUpdateSuccess {
board_id: "board-1".to_owned(),
swimlane_id: "swimlane-1".to_owned(),
updated_fields: vec![SwimlaneUpdatedField::Title],
});
let deleted = CommandSuccess::SwimlaneDeleted(SwimlaneDeleteSuccess {
board_id: "board-1".to_owned(),
swimlane_id: "swimlane-1".to_owned(),
deletion_requested: true,
delete_mode: SwimlaneDeleteMode::Hard,
});
let updated_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &updated)).unwrap();
assert_eq!(
updated_json["data"]["updated_fields"],
serde_json::json!(["title"])
);
let deleted_json: serde_json::Value =
serde_json::from_str(&render_success(OutputFormat::Json, &deleted)).unwrap();
assert_eq!(deleted_json["data"]["delete_mode"], "hard");
assert!(render_success(OutputFormat::Human, &created).contains("Created swimlane"));
assert!(render_success(OutputFormat::Human, &updated).contains("title"));
assert!(render_success(OutputFormat::Human, &deleted).contains("deletion request accepted"));
}
#[tokio::test]
async fn staged_export_cleanup_and_saved_file_diagnostics_survive_output_errors() {
use crate::{
client::exports::{ExportFormat, ExportMetadata, ExportTarget},
command_result::{ExportSuccess, StagedExport},
};
struct Fails;
impl std::io::Write for Fails {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("broken pipe"))
}
fn flush(&mut self) -> std::io::Result<()> {
Err(std::io::Error::other("broken pipe"))
}
}
let make = || {
ExportSuccess::new(
ExportTarget::Board {
board_id: "b".into(),
},
ExportFormat::Json,
"export.json".into(),
ExportMetadata {
bytes_written: 2,
http_status: 200,
content_type: Some("application/json".into()),
},
)
};
let mut saved = make();
saved.file_saved = true;
let error = super::write_success(
OutputFormat::Json,
CommandSuccess::Exported(saved),
&mut Fails,
&mut Vec::new(),
)
.await
.unwrap_err();
assert_eq!(error.details().file_saved, Some(true));
assert_eq!(error.details().destination.as_deref(), Some("export.json"));
let mut file = tempfile::NamedTempFile::new().unwrap();
std::io::Write::write_all(&mut file, b"{}").unwrap();
let path = file.path().to_owned();
let mut raw = make();
raw.staged = Some(StagedExport(file));
raw.destination = "-".into();
super::write_success(
OutputFormat::Raw,
CommandSuccess::Exported(raw),
&mut Fails,
&mut Vec::new(),
)
.await
.unwrap_err();
assert!(!path.exists());
let mut human = make();
human.destination = "unsafe\u{1b}]52;c;payload\u{7}".into();
assert!(
!render_success(OutputFormat::Human, &CommandSuccess::Exported(human)).contains('\u{1b}')
);
}