1use crate::apig::api::Column as ApigColumn;
2use crate::apig::resource::Column as ResourceColumn;
3use crate::apig::route::Column as RouteColumn;
4pub use crate::aws::{filter_profiles, Profile as AwsProfile, Region as AwsRegion};
5use crate::cfn::{Column as CfnColumn, Stack as CfnStack};
6use crate::clipboard::ClipboardState;
7use crate::cloudtrail::{CloudTrailEvent, CloudTrailEventColumn, EventResourceColumn};
8use crate::common::{ColumnId, CyclicEnum, InputFocus, PageSize, SortDirection};
9pub use crate::cw::insights::InsightsFocus;
10use crate::cw::insights::InsightsState;
11pub use crate::cw::{Alarm, AlarmColumn};
12use crate::dynamodb::table::Column as DynamoDbColumn;
13use crate::dynamodb::{actions as ddb_actions, items as ddb_items};
14pub use crate::ec2::{Column as Ec2Column, Instance as Ec2Instance};
15use crate::ecr::image::{Column as EcrImageColumn, Image as EcrImage};
16use crate::ecr::repo::{Column as EcrColumn, Repository as EcrRepository};
17use crate::efs::fs::{Column as EfsColumn, FileSystem as EfsFileSystem};
18use crate::fsx::fs::Column as FsxColumn;
19use crate::iam::{
20 GroupUser as IamGroupUser, Policy as IamPolicy, RoleColumn, RoleTag as IamRoleTag, UserColumn,
21 UserTag as IamUserTag,
22};
23#[cfg(test)]
24use crate::iam::{IamRole, IamUser, LastAccessedService};
25use crate::keymap::{Action, Mode};
26use crate::kms::key::Key as KmsKey;
27pub use crate::lambda::{
28 Alias as LambdaAlias, Application as LambdaApplication,
29 ApplicationColumn as LambdaApplicationColumn, Deployment, DeploymentColumn,
30 Function as LambdaFunction, FunctionColumn as LambdaColumn, Layer as LambdaLayer, Resource,
31 ResourceColumn as LambdaResourceColumn, Version as LambdaVersion,
32};
33use crate::rds::actions as rds_actions;
34pub use crate::s3::{Bucket as S3Bucket, BucketColumn as S3BucketColumn, Object as S3Object};
35use crate::session::{Session, SessionTab};
36pub use crate::sqs::queue::Column as SqsColumn;
37pub use crate::sqs::trigger::Column as SqsTriggerColumn;
38#[cfg(test)]
39use crate::sqs::{
40 EventBridgePipe, LambdaTrigger, Queue as SqsQueue, QueueTag as SqsQueueTag, SnsSubscription,
41};
42use crate::table::TableState;
43pub use crate::ui::apig::{
44 filtered_apis, State as ApigState, FILTER_CONTROLS as APIG_FILTER_CONTROLS,
45};
46pub use crate::ui::cfn::{
47 change_set_column_ids, change_set_visible_column_ids, event_column_ids,
48 event_visible_column_ids, filtered_change_sets, filtered_cloudformation_stacks,
49 filtered_events, filtered_outputs, filtered_parameters, filtered_resources, output_column_ids,
50 parameter_column_ids, resource_column_ids, DetailTab as CfnDetailTab,
51 EventsView as CfnEventsView, State as CfnState, StatusFilter as CfnStatusFilter,
52};
53pub use crate::ui::cw::alarms::{
54 AlarmDetailTab, AlarmTab, AlarmViewMode, FILTER_CONTROLS as ALARM_FILTER_CONTROLS,
55};
56pub use crate::ui::cw::logs::{
57 filtered_log_events, filtered_log_groups, filtered_log_streams, selected_log_group,
58 DetailTab as CwLogsDetailTab, EventFilterFocus, FILTER_CONTROLS as LOG_FILTER_CONTROLS,
59};
60pub use crate::ui::dynamodb::State as DynamoDbState;
61use crate::ui::dynamodb::{ItemOperation, ItemProjection, ITEM_LIMIT};
62pub use crate::ui::ec2::{
63 DetailTab as Ec2DetailTab, State as Ec2State, StateFilter as Ec2StateFilter,
64 STATE_FILTER as EC2_STATE_FILTER,
65};
66pub use crate::ui::ecr::{
67 filtered_ecr_images, filtered_ecr_repositories, State as EcrState, Tab as EcrTab,
68 FILTER_CONTROLS as ECR_FILTER_CONTROLS,
69};
70pub use crate::ui::efs::State as EfsState;
71pub use crate::ui::fsx::State as FsxState;
72use crate::ui::iam::{GroupTab, RoleTab, State as IamState, UserTab};
73pub use crate::ui::kms::{State as KmsState, Tab as KmsTab};
74pub use crate::ui::lambda::{
75 filtered_lambda_applications, filtered_lambda_functions,
76 ApplicationDetailTab as LambdaApplicationDetailTab, ApplicationState as LambdaApplicationState,
77 DetailTab as LambdaDetailTab, State as LambdaState, FILTER_CONTROLS as LAMBDA_FILTER_CONTROLS,
78};
79use crate::ui::monitoring::MonitoringState;
80use crate::ui::rds::State as RdsState;
81pub use crate::ui::s3::{
82 calculate_total_bucket_rows, calculate_total_object_rows, BucketType as S3BucketType,
83 ObjectTab as S3ObjectTab, State as S3State,
84};
85pub use crate::ui::sqs::{
86 extract_account_id, extract_region, filtered_eventbridge_pipes, filtered_lambda_triggers,
87 filtered_queues, filtered_subscriptions, filtered_tags, QueueDetailTab as SqsQueueDetailTab,
88 State as SqsState, FILTER_CONTROLS as SQS_FILTER_CONTROLS,
89 SUBSCRIPTION_FILTER_CONTROLS as SQS_SUBSCRIPTION_FILTER_CONTROLS, SUBSCRIPTION_REGION,
90};
91use crate::ui::vpc::State as VpcState;
92pub use crate::ui::{
93 CloudWatchLogGroupsState, DateRangeType, DetailTab, EventColumn, LogGroupColumn, Preferences,
94 StreamColumn, StreamSort, TimeUnit,
95};
96use crate::vpc::actions as vpc_actions;
97use rusticity_core::dynamodb::DdbItemsPage;
98#[cfg(test)]
99use rusticity_core::LogStream;
100use rusticity_core::{
101 AlarmsClient, ApiGatewayClient, AwsConfig, CloudFormationClient, CloudTrailClient,
102 CloudWatchClient, DynamoDbClient, Ec2Client, EcrClient, EfsClient, FsxClient, IamClient,
103 KmsClient, LambdaClient, S3Client, SqsClient,
104};
105use rusticity_core::{RdsClient, VpcClient};
106
107#[derive(Clone)]
108pub struct Tab {
109 pub service: Service,
110 pub title: String,
111 pub breadcrumb: String,
112}
113
114pub struct App {
115 pub running: bool,
116 pub mode: Mode,
117 pub config: AwsConfig,
118 pub cloudwatch_client: CloudWatchClient,
119 pub cloudtrail_client: CloudTrailClient,
120 pub s3_client: S3Client,
121 pub sqs_client: SqsClient,
122 pub alarms_client: AlarmsClient,
123 pub ec2_client: Ec2Client,
124 pub ecr_client: EcrClient,
125 pub kms_client: KmsClient,
126 pub efs_client: EfsClient,
127 pub fsx_client: FsxClient,
128 pub rds_client: RdsClient,
129 pub vpc_client: VpcClient,
130 pub dynamodb_client: DynamoDbClient,
131 pub apig_client: ApiGatewayClient,
132 pub iam_client: IamClient,
133 pub lambda_client: LambdaClient,
134 pub cloudformation_client: CloudFormationClient,
135 pub current_service: Service,
136 pub tabs: Vec<Tab>,
137 pub current_tab: usize,
138 pub tab_picker_selected: usize,
139 pub tab_filter: String,
140 pub pending_key: Option<char>,
141 pub log_groups_state: CloudWatchLogGroupsState,
142 pub insights_state: CloudWatchInsightsState,
143 pub alarms_state: CloudWatchAlarmsState,
144 pub cloudtrail_state: CloudTrailState,
145 pub s3_state: S3State,
146 pub sqs_state: SqsState,
147 pub ec2_state: Ec2State,
148 pub ecr_state: EcrState,
149 pub kms_state: KmsState,
150 pub efs_state: EfsState,
151 pub fsx_state: FsxState,
152 pub rds_state: RdsState,
153 pub vpc_state: VpcState,
154 pub resource_finder: crate::finder::State,
155 pub dynamodb_state: DynamoDbState,
156 pub apig_state: ApigState,
157 pub lambda_state: LambdaState,
158 pub lambda_application_state: LambdaApplicationState,
159 pub cfn_state: CfnState,
160 pub iam_state: IamState,
161 pub service_picker: ServicePickerState,
162 pub service_selected: bool,
163 pub profile: String,
164 pub region: String,
165 pub region_selector_index: usize,
166 pub cw_log_group_visible_column_ids: Vec<ColumnId>,
167 pub cw_log_group_column_ids: Vec<ColumnId>,
168 pub column_selector_index: usize,
169 pub preference_section: Preferences,
170 pub cw_log_stream_visible_column_ids: Vec<ColumnId>,
171 pub cw_log_stream_column_ids: Vec<ColumnId>,
172 pub cw_log_event_visible_column_ids: Vec<ColumnId>,
173 pub cw_log_event_column_ids: Vec<ColumnId>,
174 pub cw_log_tag_visible_column_ids: Vec<ColumnId>,
175 pub cw_log_tag_column_ids: Vec<ColumnId>,
176 pub cw_alarm_visible_column_ids: Vec<ColumnId>,
177 pub cw_alarm_column_ids: Vec<ColumnId>,
178 pub cloudtrail_event_visible_column_ids: Vec<ColumnId>,
179 pub cloudtrail_event_column_ids: Vec<ColumnId>,
180 pub cloudtrail_resource_visible_column_ids: Vec<ColumnId>,
181 pub cloudtrail_resource_column_ids: Vec<ColumnId>,
182 pub s3_bucket_visible_column_ids: Vec<ColumnId>,
183 pub s3_bucket_column_ids: Vec<ColumnId>,
184 pub sqs_visible_column_ids: Vec<ColumnId>,
185 pub sqs_column_ids: Vec<ColumnId>,
186 pub ec2_visible_column_ids: Vec<ColumnId>,
187 pub ec2_column_ids: Vec<ColumnId>,
188 pub ecr_repo_visible_column_ids: Vec<ColumnId>,
189 pub ecr_repo_column_ids: Vec<ColumnId>,
190 pub ecr_image_visible_column_ids: Vec<ColumnId>,
191 pub ecr_image_column_ids: Vec<ColumnId>,
192 pub efs_visible_column_ids: Vec<ColumnId>,
193 pub efs_column_ids: Vec<ColumnId>,
194 pub fsx_visible_column_ids: Vec<ColumnId>,
195 pub fsx_column_ids: Vec<ColumnId>,
196 pub dynamodb_visible_column_ids: Vec<ColumnId>,
197 pub dynamodb_column_ids: Vec<ColumnId>,
198 pub apig_api_visible_column_ids: Vec<ColumnId>,
199 pub apig_api_column_ids: Vec<ColumnId>,
200 pub apig_route_visible_column_ids: Vec<ColumnId>,
201 pub apig_route_column_ids: Vec<ColumnId>,
202 pub apig_resource_visible_column_ids: Vec<ColumnId>,
203 pub apig_resource_column_ids: Vec<ColumnId>,
204 pub lambda_application_visible_column_ids: Vec<ColumnId>,
205 pub lambda_application_column_ids: Vec<ColumnId>,
206 pub lambda_deployment_visible_column_ids: Vec<ColumnId>,
207 pub lambda_deployment_column_ids: Vec<ColumnId>,
208 pub lambda_resource_visible_column_ids: Vec<ColumnId>,
209 pub lambda_resource_column_ids: Vec<ColumnId>,
210 pub cfn_visible_column_ids: Vec<ColumnId>,
211 pub cfn_column_ids: Vec<ColumnId>,
212 pub cfn_parameter_visible_column_ids: Vec<ColumnId>,
213 pub cfn_parameter_column_ids: Vec<ColumnId>,
214 pub cfn_output_visible_column_ids: Vec<ColumnId>,
215 pub cfn_output_column_ids: Vec<ColumnId>,
216 pub cfn_resource_visible_column_ids: Vec<ColumnId>,
217 pub cfn_resource_column_ids: Vec<ColumnId>,
218 pub cfn_event_visible_column_ids: Vec<ColumnId>,
219 pub cfn_event_column_ids: Vec<ColumnId>,
220 pub cfn_change_set_visible_column_ids: Vec<ColumnId>,
221 pub cfn_change_set_column_ids: Vec<ColumnId>,
222 pub iam_user_visible_column_ids: Vec<ColumnId>,
223 pub iam_user_column_ids: Vec<ColumnId>,
224 pub iam_role_visible_column_ids: Vec<ColumnId>,
225 pub iam_role_column_ids: Vec<ColumnId>,
226 pub iam_group_visible_column_ids: Vec<String>,
227 pub iam_group_column_ids: Vec<String>,
228 pub iam_policy_visible_column_ids: Vec<String>,
229 pub iam_policy_column_ids: Vec<String>,
230 pub view_mode: ViewMode,
231 pub error_message: Option<String>,
232 pub error_scroll: usize,
233 pub page_input: String,
234 pub calendar_date: Option<time::Date>,
235 pub calendar_selecting: CalendarField,
236 pub cursor_pos: usize,
237 pub current_session: Option<Session>,
238 pub sessions: Vec<Session>,
239 pub session_picker_selected: usize,
240 pub session_filter: String,
241 pub session_filter_active: bool,
242 pub region_filter: String,
243 pub region_picker_selected: usize,
244 pub region_filter_active: bool,
245 pub region_latencies: std::collections::HashMap<String, u64>,
246 pub profile_filter: String,
247 pub profile_picker_selected: usize,
248 pub profile_filter_active: bool,
249 pub available_profiles: Vec<AwsProfile>,
250 pub clipboard: ClipboardState,
251}
252
253#[derive(Debug, Clone, Copy, PartialEq)]
254pub enum CalendarField {
255 StartDate,
256 EndDate,
257}
258
259pub struct CloudWatchInsightsState {
260 pub insights: InsightsState,
261 pub loading: bool,
262}
263
264pub struct CloudWatchAlarmsState {
265 pub table: TableState<Alarm>,
266 pub current_alarm: Option<String>,
267 pub alarm_tab: AlarmTab,
268 pub detail_tab: AlarmDetailTab,
269 pub view_as: AlarmViewMode,
270 pub wrap_lines: bool,
271 pub sort_column: String,
272 pub sort_direction: SortDirection,
273 pub input_focus: InputFocus,
274 pub metric_data: Vec<(i64, f64)>,
275 pub metrics_loading: bool,
276}
277
278#[derive(Debug, Clone)]
279pub struct CloudTrailState {
280 pub table: TableState<CloudTrailEvent>,
281 pub input_focus: InputFocus,
282 pub current_event: Option<CloudTrailEvent>,
283 pub event_json_scroll: usize,
284 pub detail_focus: CloudTrailDetailFocus,
285 pub resources_expanded_index: Option<usize>,
286 pub active_event_name_filter: Option<String>,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq)]
291pub enum CloudTrailDetailFocus {
292 Resources,
293 EventRecord,
294}
295
296impl CyclicEnum for CloudTrailDetailFocus {
297 const ALL: &'static [Self] = &[Self::Resources, Self::EventRecord];
298}
299
300impl PageSize {
301 pub fn value(&self) -> usize {
302 match self {
303 PageSize::Ten => 10,
304 PageSize::TwentyFive => 25,
305 PageSize::Fifty => 50,
306 PageSize::OneHundred => 100,
307 }
308 }
309
310 pub fn next(&self) -> Self {
311 match self {
312 PageSize::Ten => PageSize::TwentyFive,
313 PageSize::TwentyFive => PageSize::Fifty,
314 PageSize::Fifty => PageSize::OneHundred,
315 PageSize::OneHundred => PageSize::Ten,
316 }
317 }
318}
319
320pub struct ServicePickerState {
321 pub filter: String,
322 pub filter_active: bool,
323 pub selected: usize,
324 pub services: Vec<&'static str>,
325}
326
327#[derive(Debug, Clone, Copy, PartialEq)]
328pub enum ViewMode {
329 List,
330 Detail,
331 Events,
332 InsightsResults,
333 PolicyView,
334}
335
336#[derive(Debug, Clone, Copy, PartialEq)]
337pub enum Service {
338 ApiGatewayApis,
339 CloudWatchLogGroups,
340 CloudWatchInsights,
341 CloudWatchAlarms,
342 CloudTrailEvents,
343 RdsInstances,
344 S3Buckets,
345 SqsQueues,
346 VpcVpcs,
347 Ec2Instances,
348 EcrRepositories,
349 KmsKeys,
350 EfsFileSystems,
351 FsxFileSystems,
352 DynamoDbTables,
353 LambdaFunctions,
354 LambdaApplications,
355 CloudFormationStacks,
356 IamUsers,
357 IamRoles,
358 IamUserGroups,
359}
360
361impl Service {
362 pub fn name(&self) -> &str {
363 match self {
364 Service::ApiGatewayApis => "API Gateway › APIs",
365 Service::CloudWatchLogGroups => "CloudWatch › Log Groups",
366 Service::CloudWatchInsights => "CloudWatch › Logs Insights",
367 Service::CloudWatchAlarms => "CloudWatch › Alarms",
368 Service::CloudTrailEvents => "CloudTrail › Event History",
369 Service::RdsInstances => "RDS › Databases",
370 Service::S3Buckets => "S3 › Buckets",
371 Service::SqsQueues => "SQS › Queues",
372 Service::VpcVpcs => "VPC › VPCs",
373 Service::Ec2Instances => "EC2 › Instances",
374 Service::EcrRepositories => "ECR › Repositories",
375 Service::KmsKeys => "KMS › Managed Keys",
376 Service::EfsFileSystems => "EFS › File Systems",
377 Service::FsxFileSystems => "FSx › File Systems",
378 Service::DynamoDbTables => "DynamoDB › Tables",
379 Service::LambdaFunctions => "Lambda › Functions",
380 Service::LambdaApplications => "Lambda › Applications",
381 Service::CloudFormationStacks => "CloudFormation › Stacks",
382 Service::IamUsers => "IAM › Users",
383 Service::IamRoles => "IAM › Roles",
384 Service::IamUserGroups => "IAM › User Groups",
385 }
386 }
387}
388
389pub(crate) fn nav_page_down(selected: &mut usize, max: usize, page_size: usize) {
390 if max > 0 {
391 *selected = (*selected + page_size).min(max - 1);
392 }
393}
394
395impl App {
396 pub fn get_input_focus(&self) -> InputFocus {
397 InputFocus::Filter
398 }
399
400 fn get_active_filter_mut(&mut self) -> Option<&mut String> {
401 if crate::tags::Source::active(self).is_some() {
402 return crate::tags::active_filter_mut(self);
403 }
404 if self.current_service == Service::VpcVpcs {
405 return Some(&mut self.vpc_state.vpcs.filter);
406 }
407 if self.current_service == Service::RdsInstances {
408 return Some(&mut self.rds_state.instances.filter);
409 }
410 if self.current_service == Service::ApiGatewayApis {
411 crate::apig::actions::get_active_filter_mut(self)
412 } else if self.current_service == Service::CloudWatchAlarms {
413 Some(&mut self.alarms_state.table.filter)
414 } else if self.current_service == Service::CloudTrailEvents {
415 Some(&mut self.cloudtrail_state.table.filter)
416 } else if self.current_service == Service::Ec2Instances {
417 crate::ec2::actions::get_active_filter_mut(self)
418 } else if self.current_service == Service::S3Buckets {
419 if self.s3_state.current_bucket.is_some() {
420 Some(&mut self.s3_state.object_filter)
421 } else {
422 Some(&mut self.s3_state.buckets.filter)
423 }
424 } else if self.current_service == Service::EcrRepositories {
425 if self.ecr_state.current_repository.is_some() {
426 Some(&mut self.ecr_state.images.filter)
427 } else {
428 Some(&mut self.ecr_state.repositories.filter)
429 }
430 } else if self.current_service == Service::SqsQueues {
431 crate::sqs::actions::get_active_filter_mut(self)
432 } else if self.current_service == Service::LambdaFunctions {
433 crate::lambda::functions::get_active_filter_mut(self)
434 } else if self.current_service == Service::LambdaApplications {
435 crate::lambda::applications::get_active_filter_mut(self)
436 } else if self.current_service == Service::KmsKeys {
437 crate::kms::actions::get_active_filter_mut(self)
438 } else if self.current_service == Service::EfsFileSystems {
439 crate::efs::actions::get_active_filter_mut(self)
440 } else if self.current_service == Service::DynamoDbTables {
441 ddb_actions::get_active_filter_mut(self)
442 } else if self.current_service == Service::FsxFileSystems {
443 crate::fsx::actions::get_active_filter_mut(self)
444 } else if self.current_service == Service::CloudFormationStacks {
445 if self.cfn_state.current_stack.is_some()
446 && self.cfn_state.detail_tab == CfnDetailTab::Resources
447 {
448 Some(&mut self.cfn_state.resources.filter)
449 } else {
450 Some(&mut self.cfn_state.table.filter)
451 }
452 } else if self.current_service == Service::IamUsers {
453 if self.iam_state.current_user.is_some() {
454 if self.iam_state.user_tab == UserTab::Tags {
455 Some(&mut self.iam_state.user_tags.filter)
456 } else {
457 Some(&mut self.iam_state.policies.filter)
458 }
459 } else {
460 Some(&mut self.iam_state.users.filter)
461 }
462 } else if self.current_service == Service::IamRoles {
463 if self.iam_state.current_role.is_some() {
464 if self.iam_state.role_tab == RoleTab::Tags {
465 Some(&mut self.iam_state.tags.filter)
466 } else if self.iam_state.role_tab == RoleTab::LastAccessed {
467 Some(&mut self.iam_state.last_accessed_filter)
468 } else {
469 Some(&mut self.iam_state.policies.filter)
470 }
471 } else {
472 Some(&mut self.iam_state.roles.filter)
473 }
474 } else if self.current_service == Service::IamUserGroups {
475 if self.iam_state.current_group.is_some() {
476 if self.iam_state.group_tab == GroupTab::Permissions {
477 Some(&mut self.iam_state.policies.filter)
478 } else if self.iam_state.group_tab == GroupTab::Users {
479 Some(&mut self.iam_state.group_users.filter)
480 } else {
481 None
482 }
483 } else {
484 Some(&mut self.iam_state.groups.filter)
485 }
486 } else if self.view_mode == ViewMode::List {
487 Some(&mut self.log_groups_state.log_groups.filter)
488 } else if self.view_mode == ViewMode::Detail
489 && self.log_groups_state.detail_tab == DetailTab::LogStreams
490 {
491 Some(&mut self.log_groups_state.stream_filter)
492 } else {
493 None
494 }
495 }
496
497 fn apply_filter_operation<F>(&mut self, op: F)
498 where
499 F: FnOnce(&mut String),
500 {
501 if let Some(filter) = self.get_active_filter_mut() {
502 op(filter);
503 if crate::tags::reset_active(self) {
504 return;
505 }
506 if self.current_service == Service::RdsInstances {
507 self.rds_state.instances.reset();
508 } else if self.current_service == Service::VpcVpcs {
509 self.vpc_state.vpcs.reset();
510 }
511 if self.current_service == Service::CloudWatchAlarms {
513 crate::cw::actions::alarms_apply_filter_reset(self);
514 } else if self.current_service == Service::CloudTrailEvents {
515 crate::cloudtrail::actions::apply_filter_reset(self);
516 } else if self.current_service == Service::Ec2Instances {
517 crate::ec2::actions::apply_filter_reset(self);
518 } else if self.current_service == Service::S3Buckets {
519 crate::s3::actions::apply_filter_reset(self);
520 } else if self.current_service == Service::EcrRepositories {
521 crate::ecr::actions::apply_filter_reset(self);
522 } else if self.current_service == Service::KmsKeys {
523 crate::kms::actions::apply_filter_reset(self);
524 } else if self.current_service == Service::EfsFileSystems {
525 crate::efs::actions::apply_filter_reset(self);
526 } else if self.current_service == Service::DynamoDbTables {
527 ddb_actions::apply_filter_reset(self)
528 } else if self.current_service == Service::FsxFileSystems {
529 crate::fsx::actions::apply_filter_reset(self);
530 } else if self.current_service == Service::SqsQueues {
531 crate::sqs::actions::apply_filter_reset(self);
532 } else if self.current_service == Service::LambdaFunctions {
533 crate::lambda::functions::apply_filter_reset(self);
534 } else if self.current_service == Service::LambdaApplications {
535 crate::lambda::applications::apply_filter_reset(self);
536 } else if self.current_service == Service::CloudFormationStacks {
537 crate::cfn::actions::apply_filter_reset(self);
538 } else if self.current_service == Service::IamUsers {
539 crate::iam::actions::apply_filter_reset_users(self);
540 } else if self.current_service == Service::IamRoles {
541 crate::iam::actions::apply_filter_reset_roles(self);
542 } else if self.current_service == Service::IamUserGroups {
543 crate::iam::actions::apply_filter_reset_groups(self);
544 } else if self.current_service == Service::CloudWatchLogGroups {
545 if self.view_mode == ViewMode::List {
546 self.log_groups_state.log_groups.reset();
547 } else if self.log_groups_state.detail_tab == DetailTab::LogStreams {
548 self.log_groups_state.selected_stream = 0;
549 }
550 } else if self.current_service == Service::ApiGatewayApis {
551 crate::apig::actions::apply_filter_reset(self);
552 }
553 }
554 }
555
556 pub async fn new(profile: Option<String>, region: Option<String>) -> anyhow::Result<Self> {
557 let profile_name = profile.or_else(|| std::env::var("AWS_PROFILE").ok())
558 .ok_or_else(|| anyhow::anyhow!("No AWS profile specified. Set AWS_PROFILE environment variable or select a profile."))?;
559
560 std::env::set_var("AWS_PROFILE", &profile_name);
561
562 let config = AwsConfig::new(region).await?;
563 let cloudwatch_client = CloudWatchClient::new(config.clone()).await?;
564 let cloudtrail_client = CloudTrailClient::new(config.clone());
565 let s3_client = S3Client::new(config.clone());
566 let sqs_client = SqsClient::new(config.clone());
567 let alarms_client = AlarmsClient::new(config.clone());
568 let ec2_client = Ec2Client::new(config.clone());
569 let ecr_client = EcrClient::new(config.clone());
570 let apig_client = ApiGatewayClient::new(config.clone());
571 let iam_client = IamClient::new(config.clone());
572 let kms_client = KmsClient::new(config.clone());
573 let efs_client = EfsClient::new(config.clone());
574 let fsx_client = FsxClient::new(config.clone());
575 let rds_client = RdsClient::new(config.clone());
576 let vpc_client = VpcClient::new(config.clone());
577 let dynamodb_client = DynamoDbClient::new(config.clone());
578 let lambda_client = LambdaClient::new(config.clone());
579 let cloudformation_client = CloudFormationClient::new(config.clone());
580 let region_name = config.region.clone();
581
582 Ok(Self {
583 running: true,
584 mode: Mode::ServicePicker,
585 config,
586 cloudwatch_client,
587 cloudtrail_client,
588 s3_client,
589 sqs_client,
590 alarms_client,
591 ec2_client,
592 ecr_client,
593 kms_client,
594 efs_client,
595 fsx_client,
596 rds_client,
597 vpc_client,
598 dynamodb_client,
599 apig_client,
600 iam_client,
601 lambda_client,
602 cloudformation_client,
603 current_service: Service::CloudWatchLogGroups,
604 tabs: Vec::new(),
605 current_tab: 0,
606 tab_picker_selected: 0,
607 tab_filter: String::new(),
608 pending_key: None,
609 log_groups_state: CloudWatchLogGroupsState::new(),
610 insights_state: CloudWatchInsightsState::new(),
611 alarms_state: CloudWatchAlarmsState::new(),
612 cloudtrail_state: CloudTrailState {
613 table: TableState::new(),
614 input_focus: InputFocus::Filter,
615 current_event: None,
616 event_json_scroll: 0,
617 detail_focus: CloudTrailDetailFocus::Resources,
618 resources_expanded_index: None,
619 active_event_name_filter: None,
620 },
621 s3_state: S3State::new(),
622 sqs_state: SqsState::new(),
623 ec2_state: Ec2State::default(),
624 ecr_state: EcrState::new(),
625 kms_state: KmsState::new(),
626 efs_state: EfsState::new(),
627 fsx_state: FsxState::new(),
628 rds_state: RdsState::default(),
629 vpc_state: VpcState::default(),
630 resource_finder: crate::finder::State::default(),
631 dynamodb_state: DynamoDbState::new(),
632 apig_state: ApigState::new(),
633 lambda_state: LambdaState::new(),
634 lambda_application_state: LambdaApplicationState::new(),
635 cfn_state: CfnState::new(),
636 iam_state: IamState::new(),
637 service_picker: ServicePickerState::new(),
638 service_selected: false,
639 profile: profile_name,
640 region: region_name,
641 region_selector_index: 0,
642 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
643 cw_log_group_column_ids: LogGroupColumn::ids(),
644 column_selector_index: 0,
645 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
646 cw_log_stream_column_ids: StreamColumn::ids(),
647 cw_log_event_visible_column_ids: EventColumn::default_visible(),
648 cw_log_event_column_ids: EventColumn::ids(),
649 cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
650 cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
651 cw_alarm_visible_column_ids: [
652 AlarmColumn::Name,
653 AlarmColumn::State,
654 AlarmColumn::LastStateUpdate,
655 AlarmColumn::Conditions,
656 AlarmColumn::Actions,
657 ]
658 .iter()
659 .map(|c| c.id())
660 .collect(),
661 cw_alarm_column_ids: AlarmColumn::ids(),
662 cloudtrail_event_visible_column_ids: [
663 CloudTrailEventColumn::EventName,
664 CloudTrailEventColumn::EventTime,
665 CloudTrailEventColumn::Username,
666 CloudTrailEventColumn::EventSource,
667 CloudTrailEventColumn::ResourceType,
668 CloudTrailEventColumn::ResourceName,
669 ]
670 .iter()
671 .map(|c| c.id())
672 .collect(),
673 cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
674 cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
675 cloudtrail_resource_column_ids: EventResourceColumn::ids(),
676 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
677 s3_bucket_column_ids: S3BucketColumn::ids(),
678 sqs_visible_column_ids: [
679 SqsColumn::Name,
680 SqsColumn::Type,
681 SqsColumn::Created,
682 SqsColumn::MessagesAvailable,
683 SqsColumn::MessagesInFlight,
684 SqsColumn::Encryption,
685 SqsColumn::ContentBasedDeduplication,
686 ]
687 .iter()
688 .map(|c| c.id())
689 .collect(),
690 sqs_column_ids: SqsColumn::ids(),
691 ec2_visible_column_ids: [
692 Ec2Column::Name,
693 Ec2Column::InstanceId,
694 Ec2Column::InstanceState,
695 Ec2Column::InstanceType,
696 Ec2Column::StatusCheck,
697 Ec2Column::AlarmStatus,
698 Ec2Column::AvailabilityZone,
699 Ec2Column::PublicIpv4Dns,
700 Ec2Column::PublicIpv4Address,
701 Ec2Column::ElasticIp,
702 Ec2Column::Ipv6Ips,
703 Ec2Column::Monitoring,
704 Ec2Column::SecurityGroupName,
705 Ec2Column::KeyName,
706 Ec2Column::LaunchTime,
707 Ec2Column::PlatformDetails,
708 ]
709 .iter()
710 .map(|c| c.id())
711 .collect(),
712 ec2_column_ids: Ec2Column::ids(),
713 ecr_repo_visible_column_ids: EcrColumn::ids(),
714 ecr_repo_column_ids: EcrColumn::ids(),
715 ecr_image_visible_column_ids: EcrImageColumn::ids(),
716 ecr_image_column_ids: EcrImageColumn::ids(),
717 efs_visible_column_ids: EfsColumn::default_visible_ids(),
718 efs_column_ids: EfsColumn::ids(),
719 fsx_visible_column_ids: FsxColumn::default_visible_ids(),
720 fsx_column_ids: FsxColumn::ids(),
721 dynamodb_visible_column_ids: DynamoDbColumn::default_visible_ids(),
722 dynamodb_column_ids: DynamoDbColumn::ids(),
723 apig_api_visible_column_ids: ApigColumn::ids(),
724 apig_api_column_ids: ApigColumn::ids(),
725 apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
726 apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
727 apig_resource_visible_column_ids: ResourceColumn::all()
728 .iter()
729 .map(|c| c.id())
730 .collect(),
731 apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
732 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
733 lambda_application_column_ids: LambdaApplicationColumn::ids(),
734 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
735 lambda_deployment_column_ids: DeploymentColumn::ids(),
736 lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
737 lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
738 cfn_visible_column_ids: [
739 CfnColumn::Name,
740 CfnColumn::Status,
741 CfnColumn::CreatedTime,
742 CfnColumn::Description,
743 ]
744 .iter()
745 .map(|c| c.id())
746 .collect(),
747 cfn_column_ids: CfnColumn::ids(),
748 cfn_parameter_visible_column_ids: parameter_column_ids(),
749 cfn_parameter_column_ids: parameter_column_ids(),
750 cfn_output_visible_column_ids: output_column_ids(),
751 cfn_output_column_ids: output_column_ids(),
752 cfn_resource_visible_column_ids: resource_column_ids(),
753 cfn_resource_column_ids: resource_column_ids(),
754 cfn_event_visible_column_ids: event_visible_column_ids(),
755 cfn_event_column_ids: event_column_ids(),
756 cfn_change_set_visible_column_ids: change_set_visible_column_ids(),
757 cfn_change_set_column_ids: change_set_column_ids(),
758 iam_user_visible_column_ids: UserColumn::visible(),
759 iam_user_column_ids: UserColumn::ids(),
760 iam_role_visible_column_ids: RoleColumn::visible(),
761 iam_role_column_ids: RoleColumn::ids(),
762 iam_group_visible_column_ids: vec![
763 "Group name".to_string(),
764 "Users".to_string(),
765 "Permissions".to_string(),
766 "Creation time".to_string(),
767 ],
768 iam_group_column_ids: vec![
769 "Group name".to_string(),
770 "Path".to_string(),
771 "Users".to_string(),
772 "Permissions".to_string(),
773 "Creation time".to_string(),
774 ],
775 iam_policy_visible_column_ids: vec![
776 "Policy name".to_string(),
777 "Type".to_string(),
778 "Attached via".to_string(),
779 ],
780 iam_policy_column_ids: vec![
781 "Policy name".to_string(),
782 "Type".to_string(),
783 "Attached via".to_string(),
784 "Attached entities".to_string(),
785 "Description".to_string(),
786 "Creation time".to_string(),
787 "Edited time".to_string(),
788 ],
789 preference_section: Preferences::Columns,
790 view_mode: ViewMode::List,
791 error_message: None,
792 error_scroll: 0,
793 page_input: String::new(),
794 calendar_date: None,
795 calendar_selecting: CalendarField::StartDate,
796 cursor_pos: 0,
797 current_session: None,
798 sessions: Vec::new(),
799 session_picker_selected: 0,
800 session_filter: String::new(),
801 session_filter_active: false,
802 region_filter: String::new(),
803 region_filter_active: false,
804 region_picker_selected: 0,
805 region_latencies: std::collections::HashMap::new(),
806 profile_filter: String::new(),
807 profile_filter_active: false,
808 profile_picker_selected: 0,
809 available_profiles: Vec::new(),
810 clipboard: ClipboardState::default(),
811 })
812 }
813
814 pub fn new_without_client(profile: String, region: Option<String>) -> Self {
815 let config = AwsConfig::dummy(region.clone());
816 Self {
817 running: true,
818 mode: Mode::ServicePicker,
819 config: config.clone(),
820 cloudwatch_client: CloudWatchClient::dummy(config.clone()),
821 cloudtrail_client: CloudTrailClient::new(config.clone()),
822 s3_client: S3Client::new(config.clone()),
823 sqs_client: SqsClient::new(config.clone()),
824 alarms_client: AlarmsClient::new(config.clone()),
825 ec2_client: Ec2Client::new(config.clone()),
826 ecr_client: EcrClient::new(config.clone()),
827 kms_client: KmsClient::new(config.clone()),
828 efs_client: EfsClient::new(config.clone()),
829 fsx_client: FsxClient::new(config.clone()),
830 rds_client: RdsClient::new(config.clone()),
831 vpc_client: VpcClient::new(config.clone()),
832 dynamodb_client: DynamoDbClient::new(config.clone()),
833 apig_client: ApiGatewayClient::new(config.clone()),
834 iam_client: IamClient::new(config.clone()),
835 lambda_client: LambdaClient::new(config.clone()),
836 cloudformation_client: CloudFormationClient::new(config.clone()),
837 current_service: Service::CloudWatchLogGroups,
838 tabs: Vec::new(),
839 current_tab: 0,
840 tab_picker_selected: 0,
841 tab_filter: String::new(),
842 pending_key: None,
843 log_groups_state: CloudWatchLogGroupsState::new(),
844 insights_state: CloudWatchInsightsState::new(),
845 alarms_state: CloudWatchAlarmsState::new(),
846 s3_state: S3State::new(),
847 cloudtrail_state: CloudTrailState {
848 table: TableState::new(),
849 input_focus: InputFocus::Filter,
850 current_event: None,
851 event_json_scroll: 0,
852 detail_focus: CloudTrailDetailFocus::Resources,
853 resources_expanded_index: None,
854 active_event_name_filter: None,
855 },
856 sqs_state: SqsState::new(),
857 ec2_state: Ec2State::default(),
858 ecr_state: EcrState::new(),
859 kms_state: KmsState::new(),
860 efs_state: EfsState::new(),
861 fsx_state: FsxState::new(),
862 rds_state: RdsState::default(),
863 vpc_state: VpcState::default(),
864 resource_finder: crate::finder::State::default(),
865 dynamodb_state: DynamoDbState::new(),
866 apig_state: ApigState::new(),
867 lambda_state: LambdaState::new(),
868 lambda_application_state: LambdaApplicationState::new(),
869 cfn_state: CfnState::new(),
870 iam_state: IamState::new(),
871 service_picker: ServicePickerState::new(),
872 service_selected: false,
873 profile,
874 region: region.unwrap_or_default(),
875 region_selector_index: 0,
876 cw_log_group_visible_column_ids: LogGroupColumn::default_visible(),
877 cw_log_group_column_ids: LogGroupColumn::ids(),
878 column_selector_index: 0,
879 preference_section: Preferences::Columns,
880 cw_log_stream_visible_column_ids: StreamColumn::default_visible(),
881 cw_log_stream_column_ids: StreamColumn::ids(),
882 cw_log_event_visible_column_ids: EventColumn::default_visible(),
883 cw_log_event_column_ids: EventColumn::ids(),
884 cw_log_tag_visible_column_ids: crate::cw::TagColumn::ids(),
885 cw_log_tag_column_ids: crate::cw::TagColumn::ids(),
886 cw_alarm_visible_column_ids: [
887 AlarmColumn::Name,
888 AlarmColumn::State,
889 AlarmColumn::LastStateUpdate,
890 AlarmColumn::Conditions,
891 AlarmColumn::Actions,
892 ]
893 .iter()
894 .map(|c| c.id())
895 .collect(),
896 cw_alarm_column_ids: AlarmColumn::ids(),
897 s3_bucket_visible_column_ids: S3BucketColumn::ids(),
898 cloudtrail_event_visible_column_ids: [
899 CloudTrailEventColumn::EventName,
900 CloudTrailEventColumn::EventTime,
901 CloudTrailEventColumn::Username,
902 CloudTrailEventColumn::EventSource,
903 CloudTrailEventColumn::ResourceType,
904 CloudTrailEventColumn::ResourceName,
905 ]
906 .iter()
907 .map(|c| c.id())
908 .collect(),
909 cloudtrail_event_column_ids: CloudTrailEventColumn::ids(),
910 cloudtrail_resource_visible_column_ids: EventResourceColumn::ids(),
911 cloudtrail_resource_column_ids: EventResourceColumn::ids(),
912 s3_bucket_column_ids: S3BucketColumn::ids(),
913 sqs_visible_column_ids: [
914 SqsColumn::Name,
915 SqsColumn::Type,
916 SqsColumn::Created,
917 SqsColumn::MessagesAvailable,
918 SqsColumn::MessagesInFlight,
919 SqsColumn::Encryption,
920 SqsColumn::ContentBasedDeduplication,
921 ]
922 .iter()
923 .map(|c| c.id())
924 .collect(),
925 sqs_column_ids: SqsColumn::ids(),
926 ec2_visible_column_ids: [
927 Ec2Column::Name,
928 Ec2Column::InstanceId,
929 Ec2Column::InstanceState,
930 Ec2Column::InstanceType,
931 Ec2Column::StatusCheck,
932 Ec2Column::AlarmStatus,
933 Ec2Column::AvailabilityZone,
934 Ec2Column::PublicIpv4Dns,
935 Ec2Column::PublicIpv4Address,
936 Ec2Column::ElasticIp,
937 Ec2Column::Ipv6Ips,
938 Ec2Column::Monitoring,
939 Ec2Column::SecurityGroupName,
940 Ec2Column::KeyName,
941 Ec2Column::LaunchTime,
942 Ec2Column::PlatformDetails,
943 ]
944 .iter()
945 .map(|c| c.id())
946 .collect(),
947 ec2_column_ids: Ec2Column::ids(),
948 ecr_repo_visible_column_ids: EcrColumn::ids(),
949 ecr_repo_column_ids: EcrColumn::ids(),
950 ecr_image_visible_column_ids: EcrImageColumn::ids(),
951 ecr_image_column_ids: EcrImageColumn::ids(),
952 efs_visible_column_ids: EfsColumn::default_visible_ids(),
953 efs_column_ids: EfsColumn::ids(),
954 fsx_visible_column_ids: FsxColumn::default_visible_ids(),
955 fsx_column_ids: FsxColumn::ids(),
956 dynamodb_visible_column_ids: DynamoDbColumn::default_visible_ids(),
957 dynamodb_column_ids: DynamoDbColumn::ids(),
958 lambda_application_visible_column_ids: LambdaApplicationColumn::visible(),
959 lambda_application_column_ids: LambdaApplicationColumn::ids(),
960 apig_api_visible_column_ids: ApigColumn::ids(),
961 apig_api_column_ids: ApigColumn::ids(),
962 apig_route_visible_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
963 apig_route_column_ids: RouteColumn::all().iter().map(|c| c.id()).collect(),
964 apig_resource_visible_column_ids: ResourceColumn::all()
965 .iter()
966 .map(|c| c.id())
967 .collect(),
968 apig_resource_column_ids: ResourceColumn::all().iter().map(|c| c.id()).collect(),
969 lambda_deployment_visible_column_ids: DeploymentColumn::ids(),
970 lambda_deployment_column_ids: DeploymentColumn::ids(),
971 lambda_resource_visible_column_ids: crate::lambda::ResourceColumn::ids(),
972 lambda_resource_column_ids: crate::lambda::ResourceColumn::ids(),
973 cfn_visible_column_ids: [
974 CfnColumn::Name,
975 CfnColumn::Status,
976 CfnColumn::CreatedTime,
977 CfnColumn::Description,
978 ]
979 .iter()
980 .map(|c| c.id())
981 .collect(),
982 cfn_column_ids: CfnColumn::ids(),
983 iam_user_visible_column_ids: UserColumn::visible(),
984 cfn_parameter_visible_column_ids: parameter_column_ids(),
985 cfn_parameter_column_ids: parameter_column_ids(),
986 cfn_output_visible_column_ids: output_column_ids(),
987 cfn_output_column_ids: output_column_ids(),
988 cfn_resource_visible_column_ids: resource_column_ids(),
989 cfn_resource_column_ids: resource_column_ids(),
990 cfn_event_visible_column_ids: event_visible_column_ids(),
991 cfn_event_column_ids: event_column_ids(),
992 cfn_change_set_visible_column_ids: change_set_visible_column_ids(),
993 cfn_change_set_column_ids: change_set_column_ids(),
994 iam_user_column_ids: UserColumn::ids(),
995 iam_role_visible_column_ids: RoleColumn::visible(),
996 iam_role_column_ids: RoleColumn::ids(),
997 iam_group_visible_column_ids: vec![
998 "Group name".to_string(),
999 "Users".to_string(),
1000 "Permissions".to_string(),
1001 "Creation time".to_string(),
1002 ],
1003 iam_group_column_ids: vec![
1004 "Group name".to_string(),
1005 "Path".to_string(),
1006 "Users".to_string(),
1007 "Permissions".to_string(),
1008 "Creation time".to_string(),
1009 ],
1010 iam_policy_visible_column_ids: vec![
1011 "Policy name".to_string(),
1012 "Type".to_string(),
1013 "Attached via".to_string(),
1014 ],
1015 iam_policy_column_ids: vec![
1016 "Policy name".to_string(),
1017 "Type".to_string(),
1018 "Attached via".to_string(),
1019 "Attached entities".to_string(),
1020 "Description".to_string(),
1021 "Creation time".to_string(),
1022 "Edited time".to_string(),
1023 ],
1024 view_mode: ViewMode::List,
1025 error_message: None,
1026 error_scroll: 0,
1027 page_input: String::new(),
1028 calendar_date: None,
1029 calendar_selecting: CalendarField::StartDate,
1030 cursor_pos: 0,
1031 current_session: None,
1032 sessions: Vec::new(),
1033 session_picker_selected: 0,
1034 session_filter: String::new(),
1035 session_filter_active: false,
1036 region_filter: String::new(),
1037 region_filter_active: false,
1038 region_picker_selected: 0,
1039 region_latencies: std::collections::HashMap::new(),
1040 profile_filter: String::new(),
1041 profile_filter_active: false,
1042 profile_picker_selected: 0,
1043 available_profiles: Vec::new(),
1044 clipboard: ClipboardState::default(),
1045 }
1046 }
1047
1048 pub fn handle_action(&mut self, action: Action) {
1049 if crate::s3::object::handle_action(self, &action) {
1050 return;
1051 }
1052 if crate::tags::handle_action(self, &action) {
1053 return;
1054 }
1055 if crate::finder::handle_action(self, &action) {
1056 return;
1057 }
1058 if vpc_actions::handle_action(self, &action) || rds_actions::handle_action(self, &action) {
1059 return;
1060 }
1061 match action {
1062 Action::Noop | Action::OpenResourceFinder | Action::OpenRelatedResources => {}
1063 Action::EnterFilterMode => match self.mode {
1064 Mode::ServicePicker => self.service_picker.filter_active = true,
1065 Mode::RegionPicker => self.region_filter_active = true,
1066 Mode::SessionPicker => self.session_filter_active = true,
1067 Mode::ProfilePicker => self.profile_filter_active = true,
1068 _ => {}
1069 },
1070 Action::ExitFilterMode => match self.mode {
1071 Mode::ServicePicker => {
1072 if self.service_picker.filter_active {
1073 self.service_picker.filter_active = false;
1074 } else {
1075 self.mode = Mode::Normal;
1076 }
1077 }
1078 Mode::RegionPicker => {
1079 if self.region_filter_active {
1080 self.region_filter_active = false;
1081 } else {
1082 self.mode = Mode::Normal;
1083 }
1084 }
1085 Mode::SessionPicker => {
1086 if self.session_filter_active {
1087 self.session_filter_active = false;
1088 } else {
1089 self.mode = Mode::Normal;
1090 }
1091 }
1092 Mode::ProfilePicker => {
1093 if self.profile_filter_active {
1094 self.profile_filter_active = false;
1095 } else {
1096 self.mode = Mode::Normal;
1097 }
1098 }
1099 _ => {}
1100 },
1101 Action::Quit => {
1102 self.mode = Mode::QuitConfirm;
1103 }
1104 Action::ConfirmQuit => {
1105 self.save_current_session();
1106 self.running = false;
1107 }
1108 Action::CancelQuit => {
1109 self.mode = Mode::Normal;
1110 }
1111 Action::CloseService => {
1112 if !self.tabs.is_empty() {
1113 self.tabs.remove(self.current_tab);
1115
1116 if self.tabs.is_empty() {
1117 self.service_selected = false;
1119 self.current_tab = 0;
1120 self.mode = Mode::ServicePicker;
1121 } else {
1122 if self.current_tab >= self.tabs.len() {
1124 self.current_tab = self.tabs.len() - 1;
1125 }
1126 self.current_service = self.tabs[self.current_tab].service;
1127 self.service_selected = true;
1128 self.mode = Mode::Normal;
1129 }
1130 } else {
1131 self.service_selected = false;
1133 self.mode = Mode::Normal;
1134 }
1135 self.service_picker.filter.clear();
1136 self.service_picker.selected = 0;
1137 }
1138 Action::NextItem => {
1139 let should_navigate = match self.mode {
1141 Mode::ServicePicker => !self.service_picker.filter_active,
1142 Mode::RegionPicker => !self.region_filter_active,
1143 Mode::SessionPicker => !self.session_filter_active,
1144 Mode::ProfilePicker => !self.profile_filter_active,
1145 _ => true,
1146 };
1147 if should_navigate {
1148 self.next_item();
1149 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1150 self.region_filter.push('j');
1152 self.region_picker_selected = 0;
1153 }
1154 }
1155 Action::PrevItem => {
1156 let should_navigate = match self.mode {
1158 Mode::ServicePicker => !self.service_picker.filter_active,
1159 Mode::RegionPicker => !self.region_filter_active,
1160 Mode::SessionPicker => !self.session_filter_active,
1161 Mode::ProfilePicker => !self.profile_filter_active,
1162 _ => true,
1163 };
1164 if should_navigate {
1165 self.prev_item();
1166 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1167 self.region_filter.push('k');
1169 self.region_picker_selected = 0;
1170 }
1171 }
1172 Action::PageUp => self.page_up(),
1173 Action::PageDown => self.page_down(),
1174 Action::NextPane => self.next_pane(),
1175 Action::PrevPane => self.prev_pane(),
1176 Action::CollapseRow => self.collapse_row(),
1177 Action::ExpandRow => self.expand_row(),
1178 Action::Select => self.select_item(),
1179 Action::OpenSpaceMenu => {
1180 use crate::app::InsightsFocus;
1182 if self.current_service == Service::CloudWatchInsights
1183 && self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
1184 {
1185 if self.insights_state.insights.show_dropdown
1186 && !self.insights_state.insights.log_group_matches.is_empty()
1187 {
1188 self.handle_action(Action::ToggleFilterCheckbox);
1190 } else {
1191 self.handle_action(Action::Select);
1193 }
1194 return;
1195 }
1196 self.mode = Mode::SpaceMenu;
1197 self.service_picker.filter.clear();
1198 self.service_picker.filter_active = false;
1199 self.service_picker.selected = 0;
1200 }
1201 Action::CloseMenu => {
1202 self.mode = Mode::Normal;
1203 self.service_picker.filter.clear();
1204 match self.current_service {
1206 Service::S3Buckets => {
1207 crate::s3::actions::reset_on_service_select(self);
1208 }
1209 Service::CloudFormationStacks => {
1210 crate::cfn::actions::apply_filter_reset(self);
1211 }
1212 Service::LambdaFunctions => {
1213 crate::lambda::functions::apply_filter_reset(self);
1214 }
1215 Service::SqsQueues => {
1216 self.sqs_state.queues.reset();
1217 }
1218 Service::IamRoles => {
1219 crate::iam::actions::reset_on_service_select_roles(self);
1220 }
1221 Service::IamUsers => {
1222 crate::iam::actions::reset_on_service_select_users(self);
1223 }
1224 Service::IamUserGroups => {
1225 crate::iam::actions::reset_on_service_select_groups(self);
1226 }
1227 Service::CloudWatchAlarms => {
1228 self.alarms_state.table.reset();
1229 }
1230 Service::Ec2Instances => {
1231 crate::ec2::actions::apply_filter_reset(self);
1232 }
1233 Service::EcrRepositories => {
1234 crate::ecr::actions::apply_filter_reset(self);
1235 }
1236 Service::KmsKeys => {
1237 crate::kms::actions::apply_filter_reset(self);
1238 }
1239 Service::EfsFileSystems => {
1240 crate::efs::actions::apply_filter_reset(self);
1241 }
1242 Service::DynamoDbTables => {
1243 ddb_actions::apply_filter_reset(self);
1244 }
1245 Service::RdsInstances => self.rds_state.instances.reset(),
1246 Service::VpcVpcs => self.vpc_state.vpcs.reset(),
1247 Service::FsxFileSystems => {
1248 crate::fsx::actions::apply_filter_reset(self);
1249 }
1250 Service::ApiGatewayApis => {
1251 crate::apig::actions::apply_filter_reset(self);
1252 }
1253 Service::LambdaApplications => {
1254 crate::lambda::applications::apply_filter_reset(self);
1255 }
1256 _ => {}
1257 }
1258 }
1259 Action::NextTab => {
1260 if !self.tabs.is_empty() {
1261 self.current_tab = (self.current_tab + 1) % self.tabs.len();
1262 self.current_service = self.tabs[self.current_tab].service;
1263 }
1264 }
1265 Action::PrevTab => {
1266 if !self.tabs.is_empty() {
1267 self.current_tab = if self.current_tab == 0 {
1268 self.tabs.len() - 1
1269 } else {
1270 self.current_tab - 1
1271 };
1272 self.current_service = self.tabs[self.current_tab].service;
1273 }
1274 }
1275 Action::CloseTab => {
1276 if !self.tabs.is_empty() {
1277 self.tabs.remove(self.current_tab);
1278 if self.tabs.is_empty() {
1279 self.service_selected = false;
1281 self.current_tab = 0;
1282 self.service_picker.filter.clear();
1283 self.service_picker.selected = 0;
1284 self.mode = Mode::ServicePicker;
1285 } else {
1286 if self.current_tab >= self.tabs.len() {
1289 self.current_tab = self.tabs.len() - 1;
1290 }
1291 self.current_service = self.tabs[self.current_tab].service;
1292 self.service_selected = true;
1293 self.mode = Mode::Normal;
1294 }
1295 }
1296 }
1297 Action::OpenTabPicker => {
1298 if !self.tabs.is_empty() {
1299 self.tab_picker_selected = self.current_tab;
1300 self.mode = Mode::TabPicker;
1301 } else {
1302 self.mode = Mode::Normal;
1303 }
1304 }
1305 Action::OpenSessionPicker => {
1306 self.save_current_session();
1307 self.sessions = Session::list_all().unwrap_or_default();
1308 self.session_picker_selected = 0;
1309 self.mode = Mode::SessionPicker;
1310 }
1311 Action::LoadSession => {
1312 let filtered_sessions = self.get_filtered_sessions();
1313 if let Some(&session) = filtered_sessions.get(self.session_picker_selected) {
1314 let session = session.clone();
1315 self.profile = session.profile.clone();
1317 self.region = session.region.clone();
1318 self.config.account_id = session.account_id.clone();
1319 self.config.role_arn = session.role_arn.clone();
1320
1321 self.tabs.clear();
1323 for session_tab in &session.tabs {
1324 let service = match session_tab.service.as_str() {
1326 "ApiGatewayApis" => Service::ApiGatewayApis,
1327 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
1328 "CloudWatchInsights" => Service::CloudWatchInsights,
1329 "CloudWatchAlarms" => Service::CloudWatchAlarms,
1330 "CloudTrailEvents" => Service::CloudTrailEvents,
1331 "S3Buckets" => Service::S3Buckets,
1332 "SqsQueues" => Service::SqsQueues,
1333 "Ec2Instances" => Service::Ec2Instances,
1334 "EcrRepositories" => Service::EcrRepositories,
1335 "KmsKeys" => Service::KmsKeys,
1336 "EfsFileSystems" => Service::EfsFileSystems,
1337 "FsxFileSystems" => Service::FsxFileSystems,
1338 "RdsInstances" => Service::RdsInstances,
1339 "VpcVpcs" => Service::VpcVpcs,
1340 "DynamoDbTables" => Service::DynamoDbTables,
1341 "LambdaFunctions" => Service::LambdaFunctions,
1342 "LambdaApplications" => Service::LambdaApplications,
1343 "CloudFormationStacks" => Service::CloudFormationStacks,
1344 "IamUsers" => Service::IamUsers,
1345 "IamRoles" => Service::IamRoles,
1346 "IamUserGroups" => Service::IamUserGroups,
1347 _ => continue,
1348 };
1349
1350 self.tabs.push(Tab {
1351 service,
1352 title: if service == Service::RdsInstances {
1353 service.name().into()
1354 } else {
1355 session_tab.title.clone()
1356 },
1357 breadcrumb: if service == Service::RdsInstances {
1358 service.name().into()
1359 } else {
1360 session_tab.breadcrumb.clone()
1361 },
1362 });
1363
1364 if let Some(filter) = &session_tab.filter {
1366 if service == Service::CloudWatchLogGroups {
1367 self.log_groups_state.log_groups.filter = filter.clone();
1368 }
1369 }
1370 }
1371
1372 if !self.tabs.is_empty() {
1373 self.current_tab = 0;
1374 self.current_service = self.tabs[0].service;
1375 self.service_selected = true;
1376 self.current_session = Some(session.clone());
1377 }
1378 }
1379 self.mode = Mode::Normal;
1380 }
1381 Action::SaveSession => {
1382 }
1384 Action::OpenServicePicker => {
1385 if self.mode == Mode::ServicePicker {
1386 self.tabs.push(Tab {
1387 service: Service::S3Buckets,
1388 title: "S3 › Buckets".to_string(),
1389 breadcrumb: "S3 › Buckets".to_string(),
1390 });
1391 self.current_tab = self.tabs.len() - 1;
1392 self.current_service = Service::S3Buckets;
1393 self.view_mode = ViewMode::List;
1394 self.service_selected = true;
1395 self.mode = Mode::Normal;
1396 } else {
1397 self.mode = Mode::ServicePicker;
1398 self.service_picker.filter.clear();
1399 self.service_picker.selected = 0;
1400 }
1401 }
1402 Action::OpenCloudWatch => {
1403 self.current_service = Service::CloudWatchLogGroups;
1404 self.view_mode = ViewMode::List;
1405 self.service_selected = true;
1406 self.mode = Mode::Normal;
1407 }
1408 Action::OpenCloudWatchSplit => {
1409 self.current_service = Service::CloudWatchInsights;
1410 self.view_mode = ViewMode::InsightsResults;
1411 self.service_selected = true;
1412 self.mode = Mode::Normal;
1413 }
1414 Action::OpenCloudWatchAlarms => {
1415 self.current_service = Service::CloudWatchAlarms;
1416 self.view_mode = ViewMode::List;
1417 self.service_selected = true;
1418 self.mode = Mode::Normal;
1419 }
1420 Action::FilterInput(c) => {
1421 if self.mode == Mode::TabPicker {
1422 self.tab_filter.push(c);
1423 self.tab_picker_selected = 0;
1424 } else if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1425 self.service_picker.filter.push(c);
1426 self.service_picker.selected = 0;
1427 } else if self.mode == Mode::ServicePicker && c == 'q' {
1428 self.mode = Mode::QuitConfirm;
1429 } else if self.mode == Mode::ServicePicker && c == 'i' {
1430 self.service_picker.filter_active = true;
1431 } else if self.mode == Mode::ServicePicker {
1432 self.service_picker.filter_active = true;
1434 self.service_picker.filter.push(c);
1435 self.service_picker.selected = 0;
1436 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1437 self.region_filter.push(c);
1438 self.region_picker_selected = 0;
1439 } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1440 self.profile_filter.push(c);
1441 self.profile_picker_selected = 0;
1442 } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1443 self.session_filter.push(c);
1444 self.session_picker_selected = 0;
1445 } else if self.mode == Mode::InsightsInput {
1446 match self.insights_state.insights.insights_focus {
1447 InsightsFocus::Query => {
1448 self.insights_state.insights.query_text.push(c);
1449 }
1450 InsightsFocus::LogGroupSearch => {
1451 self.insights_state.insights.log_group_search.push(c);
1452 if !self.insights_state.insights.log_group_search.is_empty() {
1454 self.insights_state.insights.log_group_matches = self
1455 .log_groups_state
1456 .log_groups
1457 .items
1458 .iter()
1459 .filter(|g| {
1460 g.name.to_lowercase().contains(
1461 &self
1462 .insights_state
1463 .insights
1464 .log_group_search
1465 .to_lowercase(),
1466 )
1467 })
1468 .take(50)
1469 .map(|g| g.name.clone())
1470 .collect();
1471 self.insights_state.insights.show_dropdown = true;
1472 } else {
1473 self.insights_state.insights.log_group_matches.clear();
1474 self.insights_state.insights.show_dropdown = false;
1475 }
1476 }
1477 _ => {}
1478 }
1479 } else if self.mode == Mode::FilterInput {
1480 let is_pagination_focused = if self.current_service
1482 == Service::LambdaApplications
1483 {
1484 crate::lambda::applications::is_pagination_focused(self)
1485 } else if self.current_service == Service::CloudFormationStacks {
1486 crate::cfn::actions::is_pagination_focused(self)
1487 } else if self.current_service == Service::IamRoles
1488 && self.iam_state.current_role.is_none()
1489 {
1490 crate::iam::actions::is_pagination_focused_roles(self)
1491 } else if self.view_mode == ViewMode::PolicyView {
1492 crate::iam::actions::is_pagination_focused_policy_view(self)
1493 } else if self.current_service == Service::CloudWatchAlarms {
1494 crate::cw::actions::alarms_is_pagination_focused(self)
1495 } else if self.current_service == Service::CloudTrailEvents {
1496 crate::cloudtrail::actions::is_pagination_focused(self)
1497 } else if self.current_service == Service::Ec2Instances {
1498 crate::ec2::actions::is_pagination_focused(self)
1499 } else if self.current_service == Service::CloudWatchLogGroups {
1500 crate::cw::actions::logs_is_pagination_focused(self)
1501 } else if self.current_service == Service::ApiGatewayApis {
1502 crate::apig::actions::is_pagination_focused(self)
1503 } else if self.current_service == Service::EcrRepositories
1504 && self.ecr_state.current_repository.is_none()
1505 {
1506 crate::ecr::actions::is_pagination_focused(self)
1507 } else if self.current_service == Service::KmsKeys {
1508 crate::kms::actions::is_pagination_focused(self)
1509 } else if self.current_service == Service::EfsFileSystems {
1510 crate::efs::actions::is_pagination_focused(self)
1511 } else if self.current_service == Service::DynamoDbTables {
1512 ddb_actions::is_pagination_focused(self)
1513 } else if self.current_service == Service::FsxFileSystems {
1514 crate::fsx::actions::is_pagination_focused(self)
1515 } else if self.current_service == Service::LambdaFunctions {
1516 crate::lambda::functions::is_pagination_focused(self)
1517 } else if self.current_service == Service::SqsQueues {
1518 if self.sqs_state.current_queue.is_some()
1519 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1520 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1521 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1522 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1523 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1524 {
1525 self.sqs_state.input_focus == InputFocus::Pagination
1526 } else {
1527 false
1528 }
1529 } else {
1530 false
1531 };
1532
1533 if is_pagination_focused && c.is_ascii_digit() {
1534 self.page_input.push(c);
1535 } else if self.current_service == Service::LambdaApplications {
1536 if crate::lambda::applications::is_filter_focused(self) {
1537 self.apply_filter_operation(|f| f.push(c));
1538 }
1539 } else if self.current_service == Service::CloudFormationStacks {
1540 crate::cfn::actions::filter_char_push(self, c);
1541 } else if self.current_service == Service::EcrRepositories
1542 && self.ecr_state.current_repository.is_none()
1543 {
1544 if self.ecr_state.input_focus == InputFocus::Filter {
1545 self.apply_filter_operation(|f| f.push(c));
1546 }
1547 } else if self.current_service == Service::IamRoles
1548 && self.iam_state.current_role.is_none()
1549 {
1550 if crate::iam::actions::is_filter_focused_roles(self) {
1551 self.apply_filter_operation(|f| f.push(c));
1552 }
1553 } else if self.view_mode == ViewMode::PolicyView {
1554 if crate::iam::actions::is_filter_focused_policy_view(self) {
1555 self.apply_filter_operation(|f| f.push(c));
1556 }
1557 } else if self.current_service == Service::ApiGatewayApis
1558 && self.apig_state.current_api.is_some()
1559 && self.apig_state.detail_tab == crate::ui::apig::ApiDetailTab::Routes
1560 {
1561 if crate::apig::actions::filter_char_allowed(self) {
1562 self.apply_filter_operation(|f| f.push(c));
1563 }
1564 } else if self.current_service == Service::LambdaFunctions {
1565 if crate::lambda::functions::filter_char_allowed(self) {
1566 self.apply_filter_operation(|f| f.push(c));
1567 }
1568 } else if self.current_service == Service::SqsQueues
1569 && self.sqs_state.current_queue.is_some()
1570 && (self.sqs_state.detail_tab == SqsQueueDetailTab::LambdaTriggers
1571 || self.sqs_state.detail_tab == SqsQueueDetailTab::EventBridgePipes
1572 || self.sqs_state.detail_tab == SqsQueueDetailTab::Tagging
1573 || self.sqs_state.detail_tab == SqsQueueDetailTab::Encryption
1574 || self.sqs_state.detail_tab == SqsQueueDetailTab::SnsSubscriptions)
1575 {
1576 if self.sqs_state.input_focus == InputFocus::Filter {
1577 self.apply_filter_operation(|f| f.push(c));
1578 }
1579 } else if self.current_service == Service::Ec2Instances
1580 && self.ec2_state.current_instance.is_some()
1581 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1582 {
1583 crate::ec2::actions::filter_char_push(self, c);
1584 } else if self.current_service == Service::CloudWatchLogGroups {
1585 if self.log_groups_state.input_focus == InputFocus::Filter {
1586 self.apply_filter_operation(|f| f.push(c));
1587 }
1588 } else {
1589 self.apply_filter_operation(|f| f.push(c));
1590 }
1591 } else if self.mode == Mode::EventFilterInput {
1592 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1593 self.log_groups_state.event_filter.push(c);
1594 } else if c.is_ascii_digit() {
1595 self.log_groups_state.relative_amount.push(c);
1596 }
1597 } else if self.mode == Mode::Normal && c.is_ascii_digit() {
1598 self.page_input.push(c);
1599 }
1600 }
1601 Action::FilterBackspace => {
1602 if self.mode == Mode::ServicePicker && self.service_picker.filter_active {
1603 self.service_picker.filter.pop();
1604 self.service_picker.selected = 0;
1605 } else if self.mode == Mode::TabPicker {
1606 self.tab_filter.pop();
1607 self.tab_picker_selected = 0;
1608 } else if self.mode == Mode::RegionPicker && self.region_filter_active {
1609 self.region_filter.pop();
1610 self.region_picker_selected = 0;
1611 } else if self.mode == Mode::ProfilePicker && self.profile_filter_active {
1612 self.profile_filter.pop();
1613 self.profile_picker_selected = 0;
1614 } else if self.mode == Mode::SessionPicker && self.session_filter_active {
1615 self.session_filter.pop();
1616 self.session_picker_selected = 0;
1617 } else if self.mode == Mode::InsightsInput {
1618 match self.insights_state.insights.insights_focus {
1619 InsightsFocus::Query => {
1620 self.insights_state.insights.query_text.pop();
1621 }
1622 InsightsFocus::LogGroupSearch => {
1623 self.insights_state.insights.log_group_search.pop();
1624 if !self.insights_state.insights.log_group_search.is_empty() {
1626 self.insights_state.insights.log_group_matches = self
1627 .log_groups_state
1628 .log_groups
1629 .items
1630 .iter()
1631 .filter(|g| {
1632 g.name.to_lowercase().contains(
1633 &self
1634 .insights_state
1635 .insights
1636 .log_group_search
1637 .to_lowercase(),
1638 )
1639 })
1640 .take(50)
1641 .map(|g| g.name.clone())
1642 .collect();
1643 self.insights_state.insights.show_dropdown = true;
1644 } else {
1645 self.insights_state.insights.log_group_matches.clear();
1646 self.insights_state.insights.show_dropdown = false;
1647 }
1648 }
1649 _ => {}
1650 }
1651 } else if self.mode == Mode::FilterInput {
1652 if self.current_service == Service::CloudFormationStacks {
1654 if self.cfn_state.current_stack.is_some()
1655 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
1656 {
1657 if self.cfn_state.parameters_input_focus == InputFocus::Filter {
1658 self.cfn_state.parameters.filter.pop();
1659 self.cfn_state.parameters.selected = 0;
1660 }
1661 } else if self.cfn_state.current_stack.is_some()
1662 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
1663 {
1664 if self.cfn_state.outputs_input_focus == InputFocus::Filter {
1665 self.cfn_state.outputs.filter.pop();
1666 self.cfn_state.outputs.selected = 0;
1667 }
1668 } else if self.cfn_state.current_stack.is_some()
1669 && self.cfn_state.detail_tab == CfnDetailTab::Resources
1670 {
1671 if self.cfn_state.resources_input_focus == InputFocus::Filter {
1672 self.cfn_state.resources.filter.pop();
1673 self.cfn_state.resources.selected = 0;
1674 }
1675 } else if self.cfn_state.input_focus == InputFocus::Filter {
1676 self.apply_filter_operation(|f| {
1677 f.pop();
1678 });
1679 }
1680 } else if self.current_service == Service::Ec2Instances
1681 && self.ec2_state.current_instance.is_some()
1682 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
1683 {
1684 crate::ec2::actions::filter_char_pop(self);
1685 } else if self.current_service == Service::CloudWatchLogGroups {
1686 if self.log_groups_state.input_focus == InputFocus::Filter {
1687 self.apply_filter_operation(|f| {
1688 f.pop();
1689 });
1690 }
1691 } else {
1692 self.apply_filter_operation(|f| {
1693 f.pop();
1694 });
1695 }
1696 } else if self.mode == Mode::EventFilterInput {
1697 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1698 self.log_groups_state.event_filter.pop();
1699 } else {
1700 self.log_groups_state.relative_amount.pop();
1701 }
1702 }
1703 }
1704 Action::DeleteWord => {
1705 let text = if self.mode == Mode::ServicePicker {
1706 &mut self.service_picker.filter
1707 } else if self.mode == Mode::InsightsInput {
1708 use crate::app::InsightsFocus;
1709 match self.insights_state.insights.insights_focus {
1710 InsightsFocus::Query => &mut self.insights_state.insights.query_text,
1711 InsightsFocus::LogGroupSearch => {
1712 &mut self.insights_state.insights.log_group_search
1713 }
1714 _ => return,
1715 }
1716 } else if self.mode == Mode::FilterInput {
1717 if let Some(filter) = self.get_active_filter_mut() {
1718 filter
1719 } else {
1720 return;
1721 }
1722 } else if self.mode == Mode::EventFilterInput {
1723 if self.log_groups_state.event_input_focus == EventFilterFocus::Filter {
1724 &mut self.log_groups_state.event_filter
1725 } else {
1726 &mut self.log_groups_state.relative_amount
1727 }
1728 } else {
1729 return;
1730 };
1731
1732 if text.is_empty() {
1733 return;
1734 }
1735
1736 let mut chars: Vec<char> = text.chars().collect();
1737 while !chars.is_empty() && chars.last().is_some_and(|c| c.is_whitespace()) {
1738 chars.pop();
1739 }
1740 while !chars.is_empty() && !chars.last().is_some_and(|c| c.is_whitespace()) {
1741 chars.pop();
1742 }
1743 *text = chars.into_iter().collect();
1744 }
1745 Action::WordLeft => {
1746 }
1748 Action::WordRight => {
1749 }
1751 Action::OpenColumnSelector => {
1752 if self.current_service == Service::CloudFormationStacks
1754 && crate::cfn::actions::block_column_selector(self)
1755 {
1756 return;
1757 }
1758
1759 if self.current_service == Service::IamUsers
1761 && self.iam_state.current_user.is_some()
1762 && self.iam_state.user_tab == UserTab::SecurityCredentials
1763 {
1764 return;
1765 }
1766
1767 if self.current_service == Service::IamRoles
1769 && self.iam_state.current_role.is_some()
1770 && (self.iam_state.role_tab == RoleTab::TrustRelationships
1771 || self.iam_state.role_tab == RoleTab::RevokeSessions)
1772 {
1773 return;
1774 }
1775
1776 if self.current_service == Service::SqsQueues
1778 && self.sqs_state.current_queue.is_some()
1779 && matches!(
1780 self.sqs_state.detail_tab,
1781 SqsQueueDetailTab::QueuePolicies
1782 | SqsQueueDetailTab::Monitoring
1783 | SqsQueueDetailTab::DeadLetterQueue
1784 | SqsQueueDetailTab::Encryption
1785 | SqsQueueDetailTab::DeadLetterQueueRedriveTasks
1786 )
1787 {
1788 return;
1789 }
1790
1791 if self.current_service == Service::Ec2Instances
1793 && crate::ec2::actions::block_column_selector(self)
1794 {
1795 return;
1796 }
1797
1798 if self.current_service == Service::CloudTrailEvents
1800 && self.cloudtrail_state.current_event.is_some()
1801 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::EventRecord
1802 {
1803 return;
1804 }
1805
1806 if !self.page_input.is_empty() {
1808 if let Ok(page) = self.page_input.parse::<usize>() {
1809 self.go_to_page(page);
1810 }
1811 self.page_input.clear();
1812 } else {
1813 self.mode = Mode::ColumnSelector;
1814 self.column_selector_index = 0;
1815 }
1816 }
1817 Action::ToggleColumn => {
1818 crate::preferences::bindings::activate_active(self);
1819 }
1820 Action::NextPreferences => {
1821 crate::preferences::bindings::move_section_active(self, true);
1822 }
1823 Action::PrevPreferences => {
1824 crate::preferences::bindings::move_section_active(self, false);
1825 }
1826 Action::CloseColumnSelector => {
1827 self.mode = Mode::Normal;
1828 self.preference_section = Preferences::Columns;
1829 }
1830 Action::NextDetailTab => {
1831 if self.current_service == Service::CloudTrailEvents
1832 && self.cloudtrail_state.current_event.is_some()
1833 {
1834 crate::cloudtrail::actions::next_detail_tab(self);
1835 } else if self.current_service == Service::ApiGatewayApis
1836 && self.apig_state.current_api.is_some()
1837 {
1838 crate::apig::actions::next_detail_tab(self);
1839 } else if self.current_service == Service::SqsQueues
1840 && self.sqs_state.current_queue.is_some()
1841 {
1842 crate::sqs::actions::next_detail_tab(self);
1843 } else if self.current_service == Service::Ec2Instances
1844 && self.ec2_state.current_instance.is_some()
1845 {
1846 crate::ec2::actions::next_detail_tab(self);
1847 } else if self.current_service == Service::LambdaApplications
1848 && self.lambda_application_state.current_application.is_some()
1849 {
1850 crate::lambda::applications::next_detail_tab(self);
1851 } else if self.current_service == Service::IamRoles
1852 && self.iam_state.current_role.is_some()
1853 {
1854 crate::iam::actions::next_detail_tab_roles(self);
1855 } else if self.current_service == Service::IamUsers
1856 && self.iam_state.current_user.is_some()
1857 {
1858 crate::iam::actions::next_detail_tab_users(self);
1859 } else if self.current_service == Service::IamUserGroups
1860 && self.iam_state.current_group.is_some()
1861 {
1862 crate::iam::actions::next_detail_tab_groups(self);
1863 } else if self.current_service == Service::CloudWatchAlarms {
1864 crate::cw::actions::alarms_next_detail_tab(self);
1865 } else if self.view_mode == ViewMode::Detail {
1866 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.next();
1867 if self.log_groups_state.detail_tab == DetailTab::Tags {
1868 self.log_groups_state.tags.loading = true;
1869 }
1870 } else if self.current_service == Service::S3Buckets {
1871 crate::s3::actions::next_detail_tab(self);
1872 } else if self.current_service == Service::EcrRepositories
1873 && self.ecr_state.current_repository.is_none()
1874 {
1875 crate::ecr::actions::next_detail_tab(self);
1876 } else if self.current_service == Service::KmsKeys {
1877 crate::kms::actions::next_detail_tab(self);
1878 } else if self.current_service == Service::EfsFileSystems
1879 && self.efs_state.current_file_system.is_some()
1880 {
1881 crate::efs::actions::next_detail_tab(self);
1882 } else if self.current_service == Service::DynamoDbTables
1883 && self.dynamodb_state.current_table.is_some()
1884 {
1885 ddb_actions::next_detail_tab(self);
1886 } else if self.current_service == Service::FsxFileSystems
1887 && self.fsx_state.current_file_system.is_some()
1888 {
1889 crate::fsx::actions::next_detail_tab(self);
1890 } else if self.current_service == Service::LambdaFunctions
1891 && self.lambda_state.current_function.is_some()
1892 {
1893 crate::lambda::functions::next_detail_tab(self);
1894 } else if self.current_service == Service::CloudFormationStacks
1895 && self.cfn_state.current_stack.is_some()
1896 {
1897 crate::cfn::actions::next_detail_tab(self);
1898 }
1899 }
1900 Action::PrevDetailTab => {
1901 if self.current_service == Service::CloudTrailEvents
1902 && self.cloudtrail_state.current_event.is_some()
1903 {
1904 crate::cloudtrail::actions::prev_detail_tab(self);
1905 } else if self.current_service == Service::ApiGatewayApis
1906 && self.apig_state.current_api.is_some()
1907 {
1908 crate::apig::actions::prev_detail_tab(self);
1909 } else if self.current_service == Service::SqsQueues
1910 && self.sqs_state.current_queue.is_some()
1911 {
1912 crate::sqs::actions::prev_detail_tab(self);
1913 } else if self.current_service == Service::Ec2Instances
1914 && self.ec2_state.current_instance.is_some()
1915 {
1916 crate::ec2::actions::prev_detail_tab(self);
1917 } else if self.current_service == Service::LambdaApplications
1918 && self.lambda_application_state.current_application.is_some()
1919 {
1920 crate::lambda::applications::prev_detail_tab(self);
1921 } else if self.current_service == Service::IamRoles
1922 && self.iam_state.current_role.is_some()
1923 {
1924 crate::iam::actions::prev_detail_tab_roles(self);
1925 } else if self.current_service == Service::IamUsers
1926 && self.iam_state.current_user.is_some()
1927 {
1928 crate::iam::actions::prev_detail_tab_users(self);
1929 } else if self.current_service == Service::IamUserGroups
1930 && self.iam_state.current_group.is_some()
1931 {
1932 crate::iam::actions::prev_detail_tab_groups(self);
1933 } else if self.current_service == Service::CloudWatchAlarms {
1934 crate::cw::actions::alarms_prev_detail_tab(self);
1935 } else if self.view_mode == ViewMode::Detail {
1936 self.log_groups_state.detail_tab = self.log_groups_state.detail_tab.prev();
1937 } else if self.current_service == Service::S3Buckets {
1938 crate::s3::actions::prev_detail_tab(self);
1939 } else if self.current_service == Service::EcrRepositories
1940 && self.ecr_state.current_repository.is_none()
1941 {
1942 crate::ecr::actions::prev_detail_tab(self);
1943 } else if self.current_service == Service::KmsKeys {
1944 crate::kms::actions::prev_detail_tab(self);
1945 } else if self.current_service == Service::EfsFileSystems
1946 && self.efs_state.current_file_system.is_some()
1947 {
1948 crate::efs::actions::prev_detail_tab(self);
1949 } else if self.current_service == Service::DynamoDbTables
1950 && self.dynamodb_state.current_table.is_some()
1951 {
1952 ddb_actions::prev_detail_tab(self);
1953 } else if self.current_service == Service::FsxFileSystems
1954 && self.fsx_state.current_file_system.is_some()
1955 {
1956 crate::fsx::actions::prev_detail_tab(self);
1957 } else if self.current_service == Service::LambdaFunctions
1958 && self.lambda_state.current_function.is_some()
1959 {
1960 crate::lambda::functions::prev_detail_tab(self);
1961 } else if self.current_service == Service::CloudFormationStacks
1962 && self.cfn_state.current_stack.is_some()
1963 {
1964 crate::cfn::actions::prev_detail_tab(self);
1965 }
1966 }
1967 Action::StartFilter => {
1968 if !self.service_selected && self.tabs.is_empty() {
1970 return;
1971 }
1972
1973 if self.current_service == Service::CloudWatchInsights {
1974 self.mode = Mode::InsightsInput;
1975 } else if self.current_service == Service::CloudWatchAlarms {
1976 self.mode = Mode::FilterInput;
1977 } else if self.current_service == Service::CloudTrailEvents {
1978 self.mode = Mode::FilterInput;
1979 self.cloudtrail_state.input_focus = InputFocus::Filter;
1980 } else if self.current_service == Service::S3Buckets {
1981 self.mode = Mode::FilterInput;
1982 crate::s3::actions::start_filter(self);
1983 } else if self.current_service == Service::ApiGatewayApis
1984 || self.current_service == Service::EcrRepositories
1985 || self.current_service == Service::KmsKeys
1986 || self.current_service == Service::EfsFileSystems
1987 || self.current_service == Service::FsxFileSystems
1988 || self.current_service == Service::DynamoDbTables
1989 || self.current_service == Service::IamUsers
1990 || self.current_service == Service::IamUserGroups
1991 {
1992 self.mode = Mode::FilterInput;
1993 if self.current_service == Service::ApiGatewayApis {
1994 crate::apig::actions::start_filter(self);
1995 } else if self.current_service == Service::EcrRepositories
1996 && self.ecr_state.current_repository.is_none()
1997 {
1998 self.ecr_state.input_focus = InputFocus::Filter;
1999 } else if self.current_service == Service::KmsKeys {
2000 crate::kms::actions::start_filter(self);
2001 } else if self.current_service == Service::EfsFileSystems {
2002 crate::efs::actions::start_filter(self);
2003 } else if self.current_service == Service::DynamoDbTables {
2004 ddb_actions::start_filter(self)
2005 } else if self.current_service == Service::FsxFileSystems {
2006 crate::fsx::actions::start_filter(self);
2007 }
2008 } else if self.current_service == Service::LambdaFunctions {
2009 self.mode = Mode::FilterInput;
2010 crate::lambda::functions::start_filter(self);
2011 } else if self.current_service == Service::LambdaApplications {
2012 self.mode = Mode::FilterInput;
2013 crate::lambda::applications::start_filter(self);
2014 } else if self.current_service == Service::IamRoles {
2015 self.mode = Mode::FilterInput;
2016 } else if self.current_service == Service::CloudFormationStacks {
2017 self.mode = Mode::FilterInput;
2018 crate::cfn::actions::start_filter(self);
2019 } else if self.current_service == Service::SqsQueues {
2020 self.mode = Mode::FilterInput;
2021 self.sqs_state.input_focus = InputFocus::Filter;
2022 } else if self.view_mode == ViewMode::List
2023 || (self.view_mode == ViewMode::Detail
2024 && (self.log_groups_state.detail_tab == DetailTab::LogStreams
2025 || self.log_groups_state.detail_tab == DetailTab::Tags))
2026 {
2027 self.mode = Mode::FilterInput;
2028 self.log_groups_state.filter_mode = true;
2029 self.log_groups_state.input_focus = InputFocus::Filter;
2030 } else if self.view_mode == ViewMode::Events {
2031 self.mode = Mode::EventFilterInput;
2032 }
2033 }
2034 Action::StartEventFilter => {
2035 if self.current_service == Service::CloudWatchInsights {
2036 self.mode = Mode::InsightsInput;
2037 } else if self.view_mode == ViewMode::List {
2038 self.mode = Mode::FilterInput;
2039 self.log_groups_state.filter_mode = true;
2040 self.log_groups_state.input_focus = InputFocus::Filter;
2041 } else if self.view_mode == ViewMode::Events {
2042 self.mode = Mode::EventFilterInput;
2043 } else if self.view_mode == ViewMode::Detail
2044 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2045 {
2046 self.mode = Mode::FilterInput;
2047 self.log_groups_state.filter_mode = true;
2048 self.log_groups_state.input_focus = InputFocus::Filter;
2049 }
2050 }
2051 Action::NextFilterFocus => {
2052 if self.current_service == Service::CloudTrailEvents
2053 && self.cloudtrail_state.current_event.is_some()
2054 {
2055 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.next();
2056 } else if self.mode == Mode::FilterInput
2057 && self.current_service == Service::S3Buckets
2058 {
2059 crate::s3::actions::next_filter_focus(self);
2060 } else if self.mode == Mode::FilterInput
2061 && self.current_service == Service::Ec2Instances
2062 {
2063 crate::ec2::actions::next_filter_focus(self);
2064 } else if self.mode == Mode::FilterInput
2065 && self.current_service == Service::LambdaApplications
2066 {
2067 crate::lambda::applications::next_filter_focus(self);
2068 } else if self.mode == Mode::FilterInput
2069 && self.current_service == Service::IamRoles
2070 {
2071 crate::iam::actions::next_filter_focus_roles(self);
2072 } else if self.mode == Mode::FilterInput
2073 && self.current_service == Service::IamUsers
2074 && self.iam_state.current_user.is_some()
2075 {
2076 crate::iam::actions::next_filter_focus_users(self);
2077 } else if self.mode == Mode::FilterInput
2078 && self.current_service == Service::IamUserGroups
2079 {
2080 crate::iam::actions::next_filter_focus_groups(self);
2081 } else if self.mode == Mode::InsightsInput {
2082 use crate::app::InsightsFocus;
2083 self.insights_state.insights.insights_focus =
2084 match self.insights_state.insights.insights_focus {
2085 InsightsFocus::QueryLanguage => InsightsFocus::DatePicker,
2086 InsightsFocus::DatePicker => InsightsFocus::LogGroupSearch,
2087 InsightsFocus::LogGroupSearch => InsightsFocus::Query,
2088 InsightsFocus::Query => InsightsFocus::QueryLanguage,
2089 };
2090 } else if self.mode == Mode::FilterInput
2091 && self.current_service == Service::CloudFormationStacks
2092 {
2093 crate::cfn::actions::next_filter_focus(self);
2094 } else if self.mode == Mode::FilterInput
2095 && self.current_service == Service::SqsQueues
2096 {
2097 crate::sqs::actions::next_filter_focus(self);
2098 } else if self.mode == Mode::FilterInput
2099 && self.current_service == Service::CloudWatchLogGroups
2100 {
2101 crate::cw::actions::logs_next_filter_focus(self);
2102 } else if self.mode == Mode::EventFilterInput {
2103 crate::cw::actions::logs_next_event_filter_focus(self);
2104 } else if self.mode == Mode::FilterInput
2105 && self.current_service == Service::CloudWatchAlarms
2106 {
2107 crate::cw::actions::alarms_next_filter_focus(self);
2108 } else if self.mode == Mode::FilterInput
2109 && self.current_service == Service::CloudTrailEvents
2110 {
2111 crate::cloudtrail::actions::next_filter_focus(self);
2112 } else if self.mode == Mode::FilterInput
2113 && self.current_service == Service::ApiGatewayApis
2114 {
2115 crate::apig::actions::next_filter_focus(self);
2116 } else if self.mode == Mode::FilterInput
2117 && self.current_service == Service::EcrRepositories
2118 && self.ecr_state.current_repository.is_none()
2119 {
2120 crate::ecr::actions::next_filter_focus(self);
2121 } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys
2122 {
2123 crate::kms::actions::next_filter_focus(self);
2124 } else if self.mode == Mode::FilterInput
2125 && self.current_service == Service::EfsFileSystems
2126 {
2127 crate::efs::actions::next_filter_focus(self);
2128 } else if self.mode == Mode::FilterInput
2129 && self.current_service == Service::DynamoDbTables
2130 {
2131 ddb_actions::next_filter_focus(self);
2132 } else if self.mode == Mode::FilterInput
2133 && self.current_service == Service::FsxFileSystems
2134 {
2135 crate::fsx::actions::next_filter_focus(self);
2136 } else if self.mode == Mode::FilterInput
2137 && self.current_service == Service::LambdaFunctions
2138 {
2139 crate::lambda::functions::next_filter_focus(self);
2140 }
2141 }
2142 Action::PrevFilterFocus => {
2143 if self.current_service == Service::CloudTrailEvents
2144 && self.cloudtrail_state.current_event.is_some()
2145 {
2146 self.cloudtrail_state.detail_focus = self.cloudtrail_state.detail_focus.prev();
2147 } else if self.mode == Mode::FilterInput
2148 && self.current_service == Service::ApiGatewayApis
2149 {
2150 crate::apig::actions::prev_filter_focus(self);
2151 } else if self.mode == Mode::FilterInput
2152 && self.current_service == Service::S3Buckets
2153 {
2154 crate::s3::actions::prev_filter_focus(self);
2155 } else if self.mode == Mode::FilterInput
2156 && self.current_service == Service::Ec2Instances
2157 {
2158 crate::ec2::actions::prev_filter_focus(self);
2159 } else if self.mode == Mode::FilterInput
2160 && self.current_service == Service::LambdaApplications
2161 {
2162 crate::lambda::applications::prev_filter_focus(self);
2163 } else if self.mode == Mode::FilterInput
2164 && self.current_service == Service::CloudFormationStacks
2165 {
2166 crate::cfn::actions::prev_filter_focus(self);
2167 } else if self.mode == Mode::FilterInput
2168 && self.current_service == Service::SqsQueues
2169 {
2170 crate::sqs::actions::prev_filter_focus(self);
2171 } else if self.mode == Mode::FilterInput
2172 && self.current_service == Service::IamRoles
2173 {
2174 crate::iam::actions::prev_filter_focus_roles(self);
2175 } else if self.mode == Mode::FilterInput
2176 && self.current_service == Service::IamUsers
2177 && self.iam_state.current_user.is_some()
2178 {
2179 crate::iam::actions::prev_filter_focus_users(self);
2180 } else if self.mode == Mode::FilterInput
2181 && self.current_service == Service::IamUserGroups
2182 {
2183 crate::iam::actions::prev_filter_focus_groups(self);
2184 } else if self.mode == Mode::FilterInput
2185 && self.current_service == Service::CloudWatchLogGroups
2186 {
2187 crate::cw::actions::logs_prev_filter_focus(self);
2188 } else if self.mode == Mode::EventFilterInput {
2189 crate::cw::actions::logs_prev_event_filter_focus(self);
2190 } else if self.mode == Mode::FilterInput
2191 && self.current_service == Service::IamRoles
2192 && self.iam_state.current_role.is_some()
2193 {
2194 crate::iam::actions::prev_filter_focus_roles(self);
2195 } else if self.mode == Mode::FilterInput
2196 && self.current_service == Service::CloudWatchAlarms
2197 {
2198 crate::cw::actions::alarms_prev_filter_focus(self);
2199 } else if self.mode == Mode::FilterInput
2200 && self.current_service == Service::CloudTrailEvents
2201 {
2202 crate::cloudtrail::actions::prev_filter_focus(self);
2203 } else if self.mode == Mode::FilterInput
2204 && self.current_service == Service::EcrRepositories
2205 && self.ecr_state.current_repository.is_none()
2206 {
2207 crate::ecr::actions::prev_filter_focus(self);
2208 } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys
2209 {
2210 crate::kms::actions::prev_filter_focus(self);
2211 } else if self.mode == Mode::FilterInput
2212 && self.current_service == Service::EfsFileSystems
2213 {
2214 crate::efs::actions::prev_filter_focus(self);
2215 } else if self.mode == Mode::FilterInput
2216 && self.current_service == Service::DynamoDbTables
2217 {
2218 ddb_actions::prev_filter_focus(self);
2219 } else if self.mode == Mode::FilterInput
2220 && self.current_service == Service::FsxFileSystems
2221 {
2222 crate::fsx::actions::prev_filter_focus(self);
2223 } else if self.mode == Mode::FilterInput
2224 && self.current_service == Service::LambdaFunctions
2225 {
2226 crate::lambda::functions::prev_filter_focus(self);
2227 }
2228 }
2229 Action::ToggleFilterCheckbox => {
2230 if self.mode == Mode::FilterInput && self.current_service == Service::Ec2Instances {
2231 crate::ec2::actions::toggle_filter_checkbox(self);
2232 } else if self.mode == Mode::InsightsInput
2233 || (self.mode == Mode::Normal
2234 && self.current_service == Service::CloudWatchInsights)
2235 {
2236 use crate::app::InsightsFocus;
2237 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2238 && self.insights_state.insights.show_dropdown
2239 && !self.insights_state.insights.log_group_matches.is_empty()
2240 {
2241 let selected_idx = self.insights_state.insights.dropdown_selected;
2242 if let Some(group_name) = self
2243 .insights_state
2244 .insights
2245 .log_group_matches
2246 .get(selected_idx)
2247 {
2248 let group_name = group_name.clone();
2249 if let Some(pos) = self
2250 .insights_state
2251 .insights
2252 .selected_log_groups
2253 .iter()
2254 .position(|g| g == &group_name)
2255 {
2256 self.insights_state.insights.selected_log_groups.remove(pos);
2257 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2258 self.insights_state
2259 .insights
2260 .selected_log_groups
2261 .push(group_name);
2262 }
2263 }
2264 }
2265 } else if self.mode == Mode::FilterInput
2266 && self.current_service == Service::CloudFormationStacks
2267 {
2268 crate::cfn::actions::toggle_filter_checkbox(self);
2269 } else if self.mode == Mode::FilterInput
2270 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2271 {
2272 match self.log_groups_state.input_focus {
2273 InputFocus::Checkbox("ExactMatch") => {
2274 self.log_groups_state.exact_match = !self.log_groups_state.exact_match
2275 }
2276 InputFocus::Checkbox("ShowExpired") => {
2277 self.log_groups_state.show_expired = !self.log_groups_state.show_expired
2278 }
2279 _ => {}
2280 }
2281 } else if self.mode == Mode::EventFilterInput
2282 && self.log_groups_state.event_input_focus == EventFilterFocus::DateRange
2283 {
2284 self.log_groups_state.relative_unit =
2285 self.log_groups_state.relative_unit.next();
2286 }
2287 }
2288 Action::CycleSortColumn => {
2289 if self.view_mode == ViewMode::Detail
2290 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2291 {
2292 self.log_groups_state.stream_sort = match self.log_groups_state.stream_sort {
2293 StreamSort::Name => StreamSort::CreationTime,
2294 StreamSort::CreationTime => StreamSort::LastEventTime,
2295 StreamSort::LastEventTime => StreamSort::Name,
2296 };
2297 }
2298 }
2299 Action::ToggleSortDirection => {
2300 if self.view_mode == ViewMode::Detail
2301 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2302 {
2303 self.log_groups_state.stream_sort_desc =
2304 !self.log_groups_state.stream_sort_desc;
2305 }
2306 }
2307 Action::ScrollUp => {
2308 if self.mode == Mode::ErrorModal {
2309 self.error_scroll = self.error_scroll.saturating_sub(1);
2310 } else if self.current_service == Service::CloudTrailEvents
2311 && self.cloudtrail_state.current_event.is_some()
2312 {
2313 self.cloudtrail_state.event_json_scroll =
2314 self.cloudtrail_state.event_json_scroll.saturating_sub(10);
2315 } else if self.current_service == Service::LambdaFunctions {
2316 crate::lambda::functions::scroll_up(self);
2317 } else if self.current_service == Service::Ec2Instances {
2318 crate::ec2::actions::scroll_up(self);
2319 } else if self.current_service == Service::SqsQueues
2320 && self.sqs_state.current_queue.is_some()
2321 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2322 && !self.sqs_state.is_metrics_loading()
2323 {
2324 self.sqs_state.set_monitoring_scroll(
2325 self.sqs_state.monitoring_scroll().saturating_sub(1),
2326 );
2327 } else if self.current_service == Service::DynamoDbTables
2328 && self.dynamodb_state.current_table.is_some()
2329 && self.dynamodb_state.detail_tab == crate::ui::dynamodb::DetailTab::Monitor
2330 && !self.dynamodb_state.is_metrics_loading()
2331 {
2332 ddb_actions::scroll_up_detail(self);
2333 } else if self.current_service == Service::EfsFileSystems
2334 && self.efs_state.current_file_system.is_some()
2335 && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
2336 || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
2337 && !self.efs_state.is_metrics_loading()
2338 {
2339 crate::efs::actions::scroll_up_detail(self);
2340 } else if self.view_mode == ViewMode::PolicyView {
2341 crate::iam::actions::scroll_up_policy_view(self);
2342 } else if self.current_service == Service::IamRoles
2343 && self.iam_state.current_role.is_some()
2344 && self.iam_state.role_tab == RoleTab::TrustRelationships
2345 {
2346 crate::iam::actions::scroll_up_trust_policy(self);
2347 } else if self.view_mode == ViewMode::Events {
2348 if self.log_groups_state.event_scroll_offset == 0
2349 && self.log_groups_state.has_older_events
2350 {
2351 self.log_groups_state.loading = true;
2352 } else {
2353 self.log_groups_state.event_scroll_offset =
2354 self.log_groups_state.event_scroll_offset.saturating_sub(1);
2355 }
2356 } else if self.view_mode == ViewMode::InsightsResults {
2357 self.insights_state.insights.results_selected = self
2358 .insights_state
2359 .insights
2360 .results_selected
2361 .saturating_sub(1);
2362 } else if self.view_mode == ViewMode::Detail {
2363 self.log_groups_state.selected_stream =
2364 self.log_groups_state.selected_stream.saturating_sub(1);
2365 self.log_groups_state.expanded_stream = None;
2366 } else if self.view_mode == ViewMode::List
2367 && self.current_service == Service::CloudWatchLogGroups
2368 {
2369 self.log_groups_state.log_groups.selected =
2370 self.log_groups_state.log_groups.selected.saturating_sub(1);
2371 self.log_groups_state.log_groups.snap_to_page();
2372 } else if self.current_service == Service::EcrRepositories {
2373 crate::ecr::actions::scroll_up(self);
2374 }
2375 }
2376 Action::ScrollDown => {
2377 if self.mode == Mode::ErrorModal {
2378 if let Some(error_msg) = &self.error_message {
2379 let lines = error_msg.lines().count();
2380 let max_scroll = lines.saturating_sub(1);
2381 self.error_scroll = (self.error_scroll + 1).min(max_scroll);
2382 }
2383 } else if self.current_service == Service::CloudTrailEvents
2384 && self.cloudtrail_state.current_event.is_some()
2385 {
2386 if let Some(event) = &self.cloudtrail_state.current_event {
2387 let lines = event.cloud_trail_event_json.lines().count();
2388 let max_scroll = lines.saturating_sub(1);
2389 self.cloudtrail_state.event_json_scroll =
2390 (self.cloudtrail_state.event_json_scroll + 10).min(max_scroll);
2391 }
2392 } else if self.current_service == Service::SqsQueues
2393 && self.sqs_state.current_queue.is_some()
2394 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
2395 {
2396 self.sqs_state
2397 .set_monitoring_scroll((self.sqs_state.monitoring_scroll() + 1).min(1));
2398 } else if self.current_service == Service::DynamoDbTables
2399 && self.dynamodb_state.current_table.is_some()
2400 && self.dynamodb_state.detail_tab == crate::ui::dynamodb::DetailTab::Monitor
2401 && !self.dynamodb_state.is_metrics_loading()
2402 {
2403 ddb_actions::scroll_down_detail(self);
2404 } else if self.current_service == Service::EfsFileSystems
2405 && self.efs_state.current_file_system.is_some()
2406 && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
2407 || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
2408 {
2409 crate::efs::actions::scroll_down_detail(self);
2410 } else if self.view_mode == ViewMode::PolicyView {
2411 crate::iam::actions::scroll_down_policy_view(self);
2412 } else if self.current_service == Service::IamRoles
2413 && self.iam_state.current_role.is_some()
2414 && self.iam_state.role_tab == RoleTab::TrustRelationships
2415 {
2416 crate::iam::actions::scroll_down_trust_policy(self);
2417 } else if self.view_mode == ViewMode::Events {
2418 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
2419 if self.log_groups_state.event_scroll_offset >= max_scroll {
2420 } else {
2422 self.log_groups_state.event_scroll_offset =
2423 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
2424 }
2425 } else if self.view_mode == ViewMode::InsightsResults {
2426 let max = self
2427 .insights_state
2428 .insights
2429 .query_results
2430 .len()
2431 .saturating_sub(1);
2432 self.insights_state.insights.results_selected =
2433 (self.insights_state.insights.results_selected + 1).min(max);
2434 } else if self.view_mode == ViewMode::Detail {
2435 let filtered_streams = filtered_log_streams(self);
2436 let max = filtered_streams.len().saturating_sub(1);
2437 self.log_groups_state.selected_stream =
2438 (self.log_groups_state.selected_stream + 1).min(max);
2439 } else if self.view_mode == ViewMode::List
2440 && self.current_service == Service::CloudWatchLogGroups
2441 {
2442 let filtered_groups = filtered_log_groups(self);
2443 self.log_groups_state
2444 .log_groups
2445 .next_item(filtered_groups.len());
2446 } else if self.current_service == Service::EcrRepositories {
2447 crate::ecr::actions::scroll_down(self);
2448 }
2449 }
2450
2451 Action::MeasureLatency => {
2452 if self.mode == Mode::RegionPicker {
2453 self.measure_region_latencies();
2454 }
2455 }
2456 Action::LoadMore => ddb_items::load_more(self),
2457 Action::Refresh => {
2458 if self.mode == Mode::ProfilePicker {
2459 self.log_groups_state.loading = true;
2460 } else if self.mode == Mode::RegionPicker {
2461 } else if self.mode == Mode::SessionPicker {
2463 self.sessions = Session::list_all().unwrap_or_default();
2464 } else if self.current_service == Service::DynamoDbTables
2465 && ddb_actions::is_items_tab(self)
2466 {
2467 ddb_actions::reload_items(self);
2468 } else if self.current_service == Service::CloudWatchAlarms
2469 && self.alarms_state.current_alarm.is_some()
2470 {
2471 crate::cw::actions::alarms_refresh(self);
2472 } else if self.current_service == Service::CloudWatchInsights
2473 && !self.insights_state.insights.selected_log_groups.is_empty()
2474 {
2475 crate::cw::actions::insights_refresh(self);
2476 } else if self.current_service == Service::LambdaFunctions {
2477 crate::lambda::functions::refresh(self);
2478 } else if self.current_service == Service::LambdaApplications {
2479 crate::lambda::applications::refresh(self);
2480 } else if matches!(
2481 self.view_mode,
2482 ViewMode::Events | ViewMode::Detail | ViewMode::List
2483 ) {
2484 self.log_groups_state.loading = true;
2485 }
2486 }
2487 Action::Yank => {
2488 if self.mode == Mode::ErrorModal {
2489 if let Some(error) = &self.error_message {
2491 self.clipboard.request_text(error);
2492 }
2493 } else if self.view_mode == ViewMode::Events {
2494 if let Some(event) = self
2495 .log_groups_state
2496 .log_events
2497 .get(self.log_groups_state.event_scroll_offset)
2498 {
2499 self.clipboard.request_text(&event.message);
2500 }
2501 } else if self.current_service == Service::EcrRepositories {
2502 crate::ecr::actions::yank(self);
2503 } else if self.current_service == Service::KmsKeys {
2504 crate::kms::actions::yank(self);
2505 } else if self.current_service == Service::EfsFileSystems {
2506 crate::efs::actions::yank(self);
2507 } else if self.current_service == Service::DynamoDbTables {
2508 ddb_actions::yank(self)
2509 } else if self.current_service == Service::FsxFileSystems {
2510 crate::fsx::actions::yank(self);
2511 } else if self.current_service == Service::LambdaFunctions {
2512 crate::lambda::functions::yank(self);
2513 } else if self.current_service == Service::CloudFormationStacks {
2514 crate::cfn::actions::yank(self);
2515 } else if self.current_service == Service::CloudTrailEvents
2516 && self.cloudtrail_state.current_event.is_some()
2517 {
2518 crate::cloudtrail::actions::yank(self);
2519 } else if self.current_service == Service::IamUsers {
2520 crate::iam::actions::yank_users(self);
2521 } else if self.current_service == Service::IamRoles {
2522 crate::iam::actions::yank_roles(self);
2523 } else if self.current_service == Service::IamUserGroups {
2524 crate::iam::actions::yank_groups(self);
2525 } else if self.current_service == Service::SqsQueues {
2526 if self.sqs_state.current_queue.is_some() {
2527 if let Some(queue) = self
2529 .sqs_state
2530 .queues
2531 .items
2532 .iter()
2533 .find(|q| Some(&q.url) == self.sqs_state.current_queue.as_ref())
2534 {
2535 let arn = format!(
2536 "arn:aws:sqs:{}:{}:{}",
2537 extract_region(&queue.url),
2538 extract_account_id(&queue.url),
2539 queue.name
2540 );
2541 self.clipboard.request_text(&arn);
2542 }
2543 } else {
2544 let filtered_queues = filtered_queues(
2546 &self.sqs_state.queues.items,
2547 &self.sqs_state.queues.filter,
2548 );
2549 if let Some(queue) = self.sqs_state.queues.get_selected(&filtered_queues) {
2550 let arn = format!(
2551 "arn:aws:sqs:{}:{}:{}",
2552 extract_region(&queue.url),
2553 extract_account_id(&queue.url),
2554 queue.name
2555 );
2556 self.clipboard.request_text(&arn);
2557 }
2558 }
2559 } else if self.current_service == Service::ApiGatewayApis {
2560 crate::apig::actions::yank(self);
2561 }
2562 }
2563 Action::CopyToClipboard => {
2564 self.clipboard.request_snapshot();
2566 }
2567 Action::RetryLoad => {
2568 self.error_message = None;
2569 self.mode = Mode::Normal;
2570 if self.current_service == Service::DynamoDbTables
2571 && ddb_actions::is_items_tab(self)
2572 && self.dynamodb_state.items_failed
2573 {
2574 ddb_items::retry(self);
2575 } else {
2576 self.log_groups_state.loading = true;
2577 }
2578 }
2579 Action::ApplyFilter => {
2580 if self.mode == Mode::FilterInput
2581 && self.current_service == Service::SqsQueues
2582 && self.sqs_state.input_focus == InputFocus::Dropdown("SubscriptionRegion")
2583 {
2584 let regions = AwsRegion::all();
2585 if let Some(region) = regions.get(self.sqs_state.subscription_region_selected) {
2586 self.sqs_state.subscription_region_filter = region.code.to_string();
2587 }
2588 self.mode = Mode::Normal;
2589 } else if self.mode == Mode::FilterInput
2590 && self.current_service == Service::CloudTrailEvents
2591 {
2592 let filter_text = self.cloudtrail_state.table.filter.trim().to_string();
2594 self.cloudtrail_state.active_event_name_filter = if filter_text.is_empty() {
2595 None
2596 } else {
2597 Some(filter_text)
2598 };
2599 self.cloudtrail_state.table.items.clear();
2601 self.cloudtrail_state.table.next_token = None;
2602 self.cloudtrail_state.table.selected = 0;
2603 self.cloudtrail_state.table.scroll_offset = 0;
2604 self.cloudtrail_state.table.loading = true;
2605 self.mode = Mode::Normal;
2606 } else if self.mode == Mode::InsightsInput {
2607 use crate::app::InsightsFocus;
2608 if self.insights_state.insights.insights_focus == InsightsFocus::LogGroupSearch
2609 && self.insights_state.insights.show_dropdown
2610 {
2611 let selected_idx = self.insights_state.insights.dropdown_selected;
2613 if let Some(group_name) = self
2614 .insights_state
2615 .insights
2616 .log_group_matches
2617 .get(selected_idx)
2618 .cloned()
2619 {
2620 if let Some(pos) = self
2621 .insights_state
2622 .insights
2623 .selected_log_groups
2624 .iter()
2625 .position(|g| g == &group_name)
2626 {
2627 self.insights_state.insights.selected_log_groups.remove(pos);
2628 } else if self.insights_state.insights.selected_log_groups.len() < 50 {
2629 self.insights_state
2630 .insights
2631 .selected_log_groups
2632 .push(group_name);
2633 }
2634 }
2635 self.insights_state.insights.show_dropdown = false;
2637 self.insights_state.insights.log_group_search.clear();
2638 self.insights_state.insights.log_group_matches.clear();
2639 self.mode = Mode::Normal;
2640 }
2641 } else if self.mode == Mode::Normal && !self.page_input.is_empty() {
2642 if let Ok(page) = self.page_input.parse::<usize>() {
2643 self.go_to_page(page);
2644 }
2645 self.page_input.clear();
2646 } else {
2647 self.mode = Mode::Normal;
2648 self.log_groups_state.filter_mode = false;
2649 }
2650 }
2651 Action::ToggleExactMatch => {
2652 if self.view_mode == ViewMode::Detail
2653 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2654 {
2655 self.log_groups_state.exact_match = !self.log_groups_state.exact_match;
2656 }
2657 }
2658 Action::ToggleShowExpired => {
2659 if self.view_mode == ViewMode::Detail
2660 && self.log_groups_state.detail_tab == DetailTab::LogStreams
2661 {
2662 self.log_groups_state.show_expired = !self.log_groups_state.show_expired;
2663 }
2664 }
2665 Action::GoBack => {
2666 if self.mode == Mode::ServicePicker && !self.tabs.is_empty() {
2668 self.mode = Mode::Normal;
2669 self.service_picker.filter.clear();
2670 }
2671 else if self.current_service == Service::ApiGatewayApis
2673 && self.apig_state.current_api.is_some()
2674 {
2675 crate::apig::actions::go_back(self);
2676 }
2677 else if self.current_service == Service::S3Buckets
2679 && self.s3_state.current_bucket.is_some()
2680 {
2681 crate::s3::actions::go_back(self);
2682 }
2683 else if self.current_service == Service::EcrRepositories
2685 && self.ecr_state.current_repository.is_some()
2686 {
2687 crate::ecr::actions::go_back(self);
2688 } else if self.current_service == Service::EfsFileSystems
2689 && self.efs_state.current_file_system.is_some()
2690 {
2691 crate::efs::actions::go_back(self);
2692 } else if self.current_service == Service::DynamoDbTables
2693 && self.dynamodb_state.current_table.is_some()
2694 {
2695 ddb_actions::go_back(self);
2696 } else if self.current_service == Service::FsxFileSystems
2697 && self.fsx_state.current_file_system.is_some()
2698 {
2699 crate::fsx::actions::go_back(self);
2700 }
2701 else if self.current_service == Service::Ec2Instances
2703 && self.ec2_state.current_instance.is_some()
2704 {
2705 crate::ec2::actions::go_back(self);
2706 }
2707 else if self.current_service == Service::SqsQueues
2709 && self.sqs_state.current_queue.is_some()
2710 {
2711 self.sqs_state.current_queue = None;
2712 }
2713 else if self.current_service == Service::IamUsers
2715 && self.iam_state.current_user.is_some()
2716 {
2717 crate::iam::actions::go_back_users(self);
2718 } else if self.current_service == Service::IamUserGroups
2719 && self.iam_state.current_group.is_some()
2720 {
2721 crate::iam::actions::go_back_groups(self);
2722 } else if self.current_service == Service::IamRoles {
2723 crate::iam::actions::go_back_roles(self);
2724 }
2725 else if self.current_service == Service::LambdaFunctions
2727 && (self.lambda_state.current_version.is_some()
2728 || self.lambda_state.current_alias.is_some()
2729 || self.lambda_state.current_function.is_some())
2730 {
2731 crate::lambda::functions::go_back(self);
2732 }
2733 else if self.current_service == Service::CloudWatchAlarms
2735 && self.alarms_state.current_alarm.is_some()
2736 {
2737 crate::cw::actions::alarms_go_back(self);
2738 }
2739 else if self.current_service == Service::LambdaApplications
2741 && self.lambda_application_state.current_application.is_some()
2742 {
2743 crate::lambda::applications::go_back(self);
2744 }
2745 else if self.current_service == Service::CloudFormationStacks
2747 && self.cfn_state.current_stack.is_some()
2748 {
2749 crate::cfn::actions::go_back(self);
2750 }
2751 else if self.current_service == Service::CloudTrailEvents
2753 && self.cloudtrail_state.current_event.is_some()
2754 {
2755 crate::cloudtrail::actions::go_back(self);
2756 }
2757 else if self.view_mode == ViewMode::InsightsResults {
2759 if self.insights_state.insights.expanded_result.is_some() {
2760 self.insights_state.insights.expanded_result = None;
2761 }
2762 }
2763 else if self.current_service == Service::CloudWatchAlarms {
2765 crate::cw::actions::alarms_go_back_list(self);
2766 }
2767 else if self.current_service == Service::Ec2Instances {
2769 crate::ec2::actions::collapse_row(self);
2770 }
2771 else if self.view_mode == ViewMode::Events {
2773 if self.log_groups_state.expanded_event.is_some() {
2774 self.log_groups_state.expanded_event = None;
2775 } else {
2776 self.view_mode = ViewMode::Detail;
2777 self.log_groups_state.event_filter.clear();
2778 }
2779 }
2780 else if self.view_mode == ViewMode::Detail {
2782 self.view_mode = ViewMode::List;
2783 self.log_groups_state.stream_filter.clear();
2784 self.log_groups_state.exact_match = false;
2785 self.log_groups_state.show_expired = false;
2786 }
2787 }
2788 Action::OpenInConsole | Action::OpenInBrowser => {
2789 let url = self.get_console_url();
2790 let _ = webbrowser::open(&url);
2791 }
2792 Action::ShowHelp => {
2793 self.mode = Mode::HelpModal;
2794 }
2795 Action::OpenRegionPicker => {
2796 self.region_filter.clear();
2797 self.region_picker_selected = 0;
2798 self.mode = Mode::RegionPicker;
2799 }
2800 Action::OpenProfilePicker => {
2801 self.profile_filter.clear();
2802 self.profile_picker_selected = 0;
2803 self.available_profiles = Self::load_aws_profiles();
2804 self.mode = Mode::ProfilePicker;
2805 }
2806 Action::OpenCalendar => {
2807 self.calendar_date = Some(time::OffsetDateTime::now_utc().date());
2808 self.calendar_selecting = CalendarField::StartDate;
2809 self.mode = Mode::CalendarPicker;
2810 }
2811 Action::CloseCalendar => {
2812 self.mode = Mode::Normal;
2813 self.calendar_date = None;
2814 }
2815 Action::CalendarPrevDay => {
2816 if let Some(date) = self.calendar_date {
2817 self.calendar_date = date.checked_sub(time::Duration::days(1));
2818 }
2819 }
2820 Action::CalendarNextDay => {
2821 if let Some(date) = self.calendar_date {
2822 self.calendar_date = date.checked_add(time::Duration::days(1));
2823 }
2824 }
2825 Action::CalendarPrevWeek => {
2826 if let Some(date) = self.calendar_date {
2827 self.calendar_date = date.checked_sub(time::Duration::weeks(1));
2828 }
2829 }
2830 Action::CalendarNextWeek => {
2831 if let Some(date) = self.calendar_date {
2832 self.calendar_date = date.checked_add(time::Duration::weeks(1));
2833 }
2834 }
2835 Action::CalendarPrevMonth => {
2836 if let Some(date) = self.calendar_date {
2837 self.calendar_date = Some(if date.month() == time::Month::January {
2838 date.replace_month(time::Month::December)
2839 .unwrap()
2840 .replace_year(date.year() - 1)
2841 .unwrap()
2842 } else {
2843 date.replace_month(date.month().previous()).unwrap()
2844 });
2845 }
2846 }
2847 Action::CalendarNextMonth => {
2848 if let Some(date) = self.calendar_date {
2849 self.calendar_date = Some(if date.month() == time::Month::December {
2850 date.replace_month(time::Month::January)
2851 .unwrap()
2852 .replace_year(date.year() + 1)
2853 .unwrap()
2854 } else {
2855 date.replace_month(date.month().next()).unwrap()
2856 });
2857 }
2858 }
2859 Action::CalendarSelect => {
2860 if let Some(date) = self.calendar_date {
2861 let timestamp = time::OffsetDateTime::new_utc(date, time::Time::MIDNIGHT)
2862 .unix_timestamp()
2863 * 1000;
2864 match self.calendar_selecting {
2865 CalendarField::StartDate => {
2866 self.log_groups_state.start_time = Some(timestamp);
2867 self.calendar_selecting = CalendarField::EndDate;
2868 }
2869 CalendarField::EndDate => {
2870 self.log_groups_state.end_time = Some(timestamp);
2871 self.mode = Mode::Normal;
2872 self.calendar_date = None;
2873 }
2874 }
2875 }
2876 }
2877 }
2878 }
2879
2880 pub fn filtered_services(&self) -> Vec<&'static str> {
2881 let mut services = if self.service_picker.filter.is_empty() {
2882 self.service_picker.services.clone()
2883 } else {
2884 self.service_picker
2885 .services
2886 .iter()
2887 .filter(|s| {
2888 s.to_lowercase()
2889 .contains(&self.service_picker.filter.to_lowercase())
2890 })
2891 .copied()
2892 .collect()
2893 };
2894 services.sort();
2895 services
2896 }
2897
2898 pub fn breadcrumbs(&self) -> String {
2899 if !self.service_selected {
2900 return String::new();
2901 }
2902
2903 let mut parts = vec![];
2904
2905 match self.current_service {
2906 Service::CloudWatchLogGroups => {
2907 parts.push("CloudWatch".to_string());
2908 parts.push("Log groups".to_string());
2909
2910 if self.view_mode != ViewMode::List {
2911 if let Some(group) = selected_log_group(self) {
2912 parts.push(group.name.clone());
2913 }
2914 }
2915
2916 if self.view_mode == ViewMode::Events {
2917 if let Some(stream) = self
2918 .log_groups_state
2919 .log_streams
2920 .get(self.log_groups_state.selected_stream)
2921 {
2922 parts.push(stream.name.clone());
2923 }
2924 }
2925 }
2926 Service::CloudWatchInsights => {
2927 parts.push("CloudWatch".to_string());
2928 parts.push("Logs Insights".to_string());
2929 }
2930 Service::CloudWatchAlarms => {
2931 parts.extend(crate::cw::actions::alarms_breadcrumb());
2932 }
2933 Service::CloudTrailEvents => {
2934 parts.extend(crate::cloudtrail::actions::breadcrumb(self));
2935 }
2936 Service::S3Buckets => {
2937 parts.extend(crate::s3::actions::breadcrumb(self));
2938 }
2939 Service::SqsQueues => {
2940 parts.extend(crate::sqs::actions::breadcrumb());
2941 }
2942 Service::EcrRepositories => {
2943 parts.extend(crate::ecr::actions::breadcrumb(self));
2944 }
2945 Service::KmsKeys => {
2946 parts.extend(crate::kms::actions::breadcrumb(self));
2947 }
2948 Service::EfsFileSystems => {
2949 parts.extend(crate::efs::actions::breadcrumb(self));
2950 }
2951 Service::RdsInstances => parts.extend(["RDS".into(), "Databases".into()]),
2952 Service::VpcVpcs => parts.extend(["VPC".into(), "VPCs".into()]),
2953 Service::FsxFileSystems => {
2954 parts.extend(crate::fsx::actions::breadcrumb(self));
2955 }
2956 Service::DynamoDbTables => {
2957 parts.extend(ddb_actions::breadcrumb(self));
2958 }
2959 Service::LambdaFunctions => {
2960 parts.extend(crate::lambda::functions::breadcrumb(self));
2961 }
2962 Service::LambdaApplications => {
2963 parts.extend(crate::lambda::applications::breadcrumb());
2964 }
2965 Service::CloudFormationStacks => {
2966 parts.extend(crate::cfn::actions::breadcrumb(self));
2967 }
2968 Service::IamUsers => {
2969 parts.extend(crate::iam::actions::breadcrumb_users());
2970 }
2971 Service::IamRoles => {
2972 parts.extend(crate::iam::actions::breadcrumb_roles(self));
2973 }
2974 Service::IamUserGroups => {
2975 parts.extend(crate::iam::actions::breadcrumb_groups(self));
2976 }
2977 Service::Ec2Instances => {
2978 parts.extend(crate::ec2::actions::breadcrumb(self));
2979 }
2980 Service::ApiGatewayApis => {
2981 parts.extend(crate::apig::actions::breadcrumb(self));
2982 }
2983 }
2984
2985 parts.join(" > ")
2986 }
2987
2988 pub fn update_current_tab_breadcrumb(&mut self) {
2989 if !self.tabs.is_empty() {
2990 self.tabs[self.current_tab].breadcrumb = self.breadcrumbs();
2991 }
2992 }
2993
2994 pub fn get_console_url(&self) -> String {
2995 use crate::cw;
2996
2997 match self.current_service {
2998 Service::CloudWatchLogGroups => {
2999 if self.view_mode == ViewMode::Events {
3000 if let Some(group) = selected_log_group(self) {
3001 if let Some(stream) = self
3002 .log_groups_state
3003 .log_streams
3004 .get(self.log_groups_state.selected_stream)
3005 {
3006 return cw::logs::console_url_stream(
3007 &self.config.region,
3008 &group.name,
3009 &stream.name,
3010 );
3011 }
3012 }
3013 } else if self.view_mode == ViewMode::Detail {
3014 if let Some(group) = selected_log_group(self) {
3015 return cw::logs::console_url_detail(&self.config.region, &group.name);
3016 }
3017 }
3018 cw::logs::console_url_list(&self.config.region)
3019 }
3020 Service::CloudWatchInsights => cw::insights::console_url(
3021 &self.config.region,
3022 &self.config.account_id,
3023 &self.insights_state.insights.query_text,
3024 &self.insights_state.insights.selected_log_groups,
3025 ),
3026 Service::CloudWatchAlarms => {
3027 let alarm_name = if let Some(name) = self.alarms_state.current_alarm.as_deref() {
3029 Some(name.to_string())
3030 } else {
3031 let page_size = self.alarms_state.table.page_size.value();
3033 let current_page = self.alarms_state.table.selected / page_size;
3034 let start = current_page * page_size;
3035 let filtered: Vec<_> = self.alarms_state.table.items.iter().collect();
3036 let relative = self.alarms_state.table.selected.saturating_sub(start);
3037 filtered.get(start + relative).map(|a| a.name.clone())
3038 };
3039 cw::alarms::console_url(&self.config.region, alarm_name.as_deref())
3040 }
3041 Service::CloudTrailEvents => crate::cloudtrail::actions::console_url(self),
3042 Service::S3Buckets => crate::s3::actions::console_url(self),
3043 Service::SqsQueues => crate::sqs::actions::console_url(self),
3044 Service::EcrRepositories => crate::ecr::actions::console_url(self),
3045 Service::KmsKeys => crate::kms::actions::console_url(self),
3046 Service::EfsFileSystems => crate::efs::actions::console_url(self),
3047 Service::FsxFileSystems => crate::fsx::actions::console_url(self),
3048 Service::RdsInstances => rds_actions::console_url(self),
3049 Service::VpcVpcs => vpc_actions::console_url(self),
3050 Service::DynamoDbTables => ddb_actions::console_url(self),
3051 Service::LambdaFunctions => crate::lambda::functions::console_url(self),
3052 Service::LambdaApplications => crate::lambda::applications::console_url(self),
3053 Service::CloudFormationStacks => crate::cfn::actions::console_url(self),
3054 Service::IamUsers => crate::iam::actions::console_url_users(self),
3055 Service::IamRoles => crate::iam::actions::console_url_roles(self),
3056 Service::IamUserGroups => crate::iam::actions::console_url_groups(self),
3057 Service::Ec2Instances => crate::ec2::actions::console_url(self),
3058 Service::ApiGatewayApis => crate::apig::actions::console_url(self),
3059 }
3060 }
3061
3062 pub fn calculate_total_bucket_rows(&self) -> usize {
3063 calculate_total_bucket_rows(self)
3064 }
3065
3066 pub fn calculate_total_object_rows(&self) -> usize {
3067 calculate_total_object_rows(self)
3068 }
3069
3070 fn get_column_selector_max(&self) -> usize {
3071 crate::preferences::bindings::model_for(self, self.current_service).max_index()
3072 }
3073
3074 #[cfg(test)]
3075 fn get_column_count(&self) -> usize {
3076 crate::preferences::bindings::model_for(self, self.current_service).column_count()
3077 }
3078
3079 fn is_blank_row_index(&self, idx: usize) -> bool {
3080 crate::preferences::bindings::model_for(self, self.current_service).is_separator(idx)
3081 }
3082
3083 fn next_item(&mut self) {
3084 match self.mode {
3085 Mode::FilterInput => {
3086 if self.current_service == Service::S3Buckets
3087 && crate::s3::actions::is_pagination_focused(self)
3088 {
3089 crate::s3::actions::page_down_filter_input(self);
3090 } else if self.current_service == Service::CloudTrailEvents
3091 && self.cloudtrail_state.input_focus == InputFocus::Pagination
3092 {
3093 let page_size = self.cloudtrail_state.table.page_size.value();
3095 let total_items = self.cloudtrail_state.table.items.len();
3096 let current_page = self.cloudtrail_state.table.selected / page_size;
3097 let total_pages = total_items.div_ceil(page_size);
3098 if current_page + 1 < total_pages {
3099 self.cloudtrail_state.table.selected = (current_page + 1) * page_size;
3100 }
3101 } else if self.current_service == Service::CloudFormationStacks {
3102 crate::cfn::actions::next_item_filter_input(self);
3103 } else if self.current_service == Service::IamUsers
3104 && self.iam_state.current_user.is_some()
3105 {
3106 crate::iam::actions::next_item_filter_input_users(self);
3107 } else if self.current_service == Service::IamRoles
3108 && self.iam_state.current_role.is_some()
3109 && self.iam_state.role_tab == RoleTab::Permissions
3110 {
3111 crate::iam::actions::next_item_filter_input_roles(self);
3112 } else if self.current_service == Service::DynamoDbTables {
3113 ddb_actions::next_dropdown_value(self);
3114 } else if self.current_service == Service::Ec2Instances {
3115 crate::ec2::actions::toggle_state_filter_next(self);
3116 } else if self.current_service == Service::SqsQueues {
3117 crate::sqs::actions::next_item_filter_input(self);
3118 }
3119 }
3120 Mode::RegionPicker => {
3121 let filtered = self.get_filtered_regions();
3122 if !filtered.is_empty() {
3123 self.region_picker_selected =
3124 (self.region_picker_selected + 1).min(filtered.len() - 1);
3125 }
3126 }
3127 Mode::ProfilePicker => {
3128 let filtered = self.get_filtered_profiles();
3129 if !filtered.is_empty() {
3130 self.profile_picker_selected =
3131 (self.profile_picker_selected + 1).min(filtered.len() - 1);
3132 }
3133 }
3134 Mode::SessionPicker => {
3135 let filtered = self.get_filtered_sessions();
3136 if !filtered.is_empty() {
3137 self.session_picker_selected =
3138 (self.session_picker_selected + 1).min(filtered.len() - 1);
3139 }
3140 }
3141 Mode::InsightsInput => {
3142 crate::cw::actions::insights_next_item_dropdown(self);
3143 }
3144 Mode::ColumnSelector => {
3145 self.column_selector_index =
3146 crate::preferences::bindings::model_for(self, self.current_service)
3147 .move_row(self.column_selector_index, true);
3148 }
3149 Mode::ServicePicker => {
3150 let filtered = self.filtered_services();
3151 if !filtered.is_empty() {
3152 self.service_picker.selected =
3153 (self.service_picker.selected + 1).min(filtered.len() - 1);
3154 }
3155 }
3156 Mode::TabPicker => {
3157 let filtered = self.get_filtered_tabs();
3158 if !filtered.is_empty() {
3159 self.tab_picker_selected =
3160 (self.tab_picker_selected + 1).min(filtered.len() - 1);
3161 }
3162 }
3163 Mode::Normal => {
3164 if !self.service_selected {
3165 let filtered = self.filtered_services();
3166 if !filtered.is_empty() {
3167 self.service_picker.selected =
3168 (self.service_picker.selected + 1).min(filtered.len() - 1);
3169 }
3170 } else if self.current_service == Service::S3Buckets {
3171 crate::s3::actions::next_item(self);
3172 } else if self.view_mode == ViewMode::InsightsResults {
3173 crate::cw::actions::insights_next_item_results(self);
3174 } else if self.current_service == Service::CloudWatchInsights
3175 && self.insights_state.insights.show_dropdown
3176 {
3177 crate::cw::actions::insights_next_item_dropdown(self);
3179 } else if self.current_service == Service::ApiGatewayApis {
3180 crate::apig::actions::next_item(self);
3181 } else if self.current_service == Service::SqsQueues
3182 && self.sqs_state.current_queue.is_some()
3183 && (self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
3184 || (self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3185 && !self.sqs_state.is_metrics_loading()))
3186 {
3187 crate::sqs::actions::scroll_down_detail(self);
3188 } else if self.view_mode == ViewMode::Events {
3189 let max_scroll = self.log_groups_state.log_events.len().saturating_sub(1);
3190 if self.log_groups_state.event_scroll_offset < max_scroll {
3191 self.log_groups_state.event_scroll_offset =
3192 (self.log_groups_state.event_scroll_offset + 1).min(max_scroll);
3193 }
3194 } else if self.current_service == Service::CloudWatchLogGroups {
3195 crate::cw::actions::logs_next_item(self);
3196 } else if self.current_service == Service::CloudWatchAlarms {
3197 crate::cw::actions::alarms_next_item(self);
3198 } else if self.current_service == Service::CloudTrailEvents {
3199 crate::cloudtrail::actions::next_item(self);
3200 } else if self.current_service == Service::Ec2Instances {
3201 crate::ec2::actions::next_item(self);
3202 } else if self.current_service == Service::EcrRepositories {
3203 crate::ecr::actions::next_item(self);
3204 } else if self.current_service == Service::KmsKeys {
3205 crate::kms::actions::next_item(self);
3206 } else if self.current_service == Service::EfsFileSystems {
3207 crate::efs::actions::next_item(self);
3208 } else if self.current_service == Service::DynamoDbTables {
3209 ddb_actions::next_item(self)
3210 } else if self.current_service == Service::FsxFileSystems {
3211 crate::fsx::actions::next_item(self);
3212 } else if self.current_service == Service::SqsQueues {
3213 crate::sqs::actions::next_item(self);
3214 } else if self.current_service == Service::LambdaFunctions {
3215 crate::lambda::functions::next_item(self);
3216 } else if self.current_service == Service::LambdaApplications {
3217 crate::lambda::applications::next_item(self);
3218 } else if self.current_service == Service::CloudFormationStacks {
3219 crate::cfn::actions::next_item(self);
3220 } else if self.current_service == Service::IamUsers {
3221 crate::iam::actions::next_item_users(self);
3222 } else if self.current_service == Service::IamRoles {
3223 crate::iam::actions::next_item_roles(self);
3224 } else if self.current_service == Service::IamUserGroups {
3225 crate::iam::actions::next_item_groups(self);
3226 }
3227 }
3228 _ => {}
3229 }
3230 }
3231
3232 fn prev_item(&mut self) {
3233 match self.mode {
3234 Mode::FilterInput => {
3235 if self.current_service == Service::S3Buckets
3236 && crate::s3::actions::is_pagination_focused(self)
3237 {
3238 crate::s3::actions::page_up_filter_input(self);
3239 } else if self.current_service == Service::CloudTrailEvents
3240 && self.cloudtrail_state.input_focus == InputFocus::Pagination
3241 {
3242 let page_size = self.cloudtrail_state.table.page_size.value();
3244 let current_page = self.cloudtrail_state.table.selected / page_size;
3245 if current_page > 0 {
3246 self.cloudtrail_state.table.selected = (current_page - 1) * page_size;
3247 }
3248 } else if self.current_service == Service::CloudFormationStacks {
3249 crate::cfn::actions::prev_item_filter_input(self);
3250 } else if self.current_service == Service::IamUsers
3251 && self.iam_state.current_user.is_some()
3252 {
3253 crate::iam::actions::prev_item_filter_input_users(self);
3254 } else if self.current_service == Service::IamRoles
3255 && self.iam_state.current_role.is_some()
3256 && self.iam_state.role_tab == RoleTab::Permissions
3257 {
3258 crate::iam::actions::prev_item_filter_input_roles(self);
3259 } else if self.current_service == Service::DynamoDbTables {
3260 ddb_actions::prev_dropdown_value(self);
3261 } else if self.current_service == Service::Ec2Instances {
3262 crate::ec2::actions::toggle_state_filter_prev(self);
3263 } else if self.current_service == Service::SqsQueues {
3264 use crate::ui::sqs::SUBSCRIPTION_REGION;
3265 if self.sqs_state.input_focus == SUBSCRIPTION_REGION {
3266 self.sqs_state.subscription_region_selected = self
3267 .sqs_state
3268 .subscription_region_selected
3269 .saturating_sub(1);
3270 self.sqs_state.subscriptions.reset();
3271 }
3272 }
3273 }
3274 Mode::RegionPicker => {
3275 self.region_picker_selected = self.region_picker_selected.saturating_sub(1);
3276 }
3277 Mode::ProfilePicker => {
3278 self.profile_picker_selected = self.profile_picker_selected.saturating_sub(1);
3279 }
3280 Mode::SessionPicker => {
3281 self.session_picker_selected = self.session_picker_selected.saturating_sub(1);
3282 }
3283 Mode::InsightsInput => {
3284 crate::cw::actions::insights_prev_item_dropdown(self);
3285 }
3286 Mode::ColumnSelector => {
3287 self.column_selector_index =
3288 crate::preferences::bindings::model_for(self, self.current_service)
3289 .move_row(self.column_selector_index, false);
3290 }
3291 Mode::ServicePicker => {
3292 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3293 }
3294 Mode::TabPicker => {
3295 self.tab_picker_selected = self.tab_picker_selected.saturating_sub(1);
3296 }
3297 Mode::Normal => {
3298 if !self.service_selected {
3299 self.service_picker.selected = self.service_picker.selected.saturating_sub(1);
3300 } else if self.current_service == Service::S3Buckets {
3301 crate::s3::actions::prev_item(self);
3302 } else if self.current_service == Service::ApiGatewayApis {
3303 crate::apig::actions::prev_item(self);
3304 } else if self.view_mode == ViewMode::InsightsResults {
3305 if self.insights_state.insights.results_selected > 0 {
3306 self.insights_state.insights.results_selected -= 1;
3307 }
3308 } else if self.current_service == Service::CloudWatchInsights
3309 && self.insights_state.insights.show_dropdown
3310 {
3311 crate::cw::actions::insights_prev_item_dropdown(self);
3312 } else if self.view_mode == ViewMode::PolicyView {
3313 crate::iam::actions::scroll_up_policy_view_one(self);
3314 } else if self.current_service == Service::SqsQueues
3315 && self.sqs_state.current_queue.is_some()
3316 && self.sqs_state.detail_tab == SqsQueueDetailTab::QueuePolicies
3317 {
3318 } else if self.current_service == Service::LambdaFunctions
3319 && self.lambda_state.current_function.is_some()
3320 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3321 && !self.lambda_state.is_metrics_loading()
3322 {
3323 crate::lambda::functions::scroll_up(self);
3324 } else if self.current_service == Service::SqsQueues
3325 && self.sqs_state.current_queue.is_some()
3326 && self.sqs_state.detail_tab == SqsQueueDetailTab::Monitoring
3327 && !self.sqs_state.is_metrics_loading()
3328 {
3329 self.sqs_state.set_monitoring_scroll(
3330 self.sqs_state.monitoring_scroll().saturating_sub(1),
3331 );
3332 } else if self.view_mode == ViewMode::Events {
3333 if self.log_groups_state.event_scroll_offset == 0 {
3334 if self.log_groups_state.has_older_events {
3335 self.log_groups_state.loading = true;
3336 }
3337 } else {
3339 self.log_groups_state.event_scroll_offset =
3340 self.log_groups_state.event_scroll_offset.saturating_sub(1);
3341 }
3342 } else if self.current_service == Service::CloudWatchLogGroups {
3343 crate::cw::actions::logs_prev_item(self);
3344 } else if self.current_service == Service::CloudWatchAlarms {
3345 crate::cw::actions::alarms_prev_item(self);
3346 } else if self.current_service == Service::CloudTrailEvents {
3347 crate::cloudtrail::actions::prev_item(self);
3348 } else if self.current_service == Service::Ec2Instances {
3349 if self.ec2_state.current_instance.is_some()
3350 && self.ec2_state.detail_tab == Ec2DetailTab::Tags
3351 {
3352 self.ec2_state.tags.prev_item();
3353 } else {
3354 self.ec2_state.table.prev_item();
3355 }
3356 } else if self.current_service == Service::EcrRepositories {
3357 crate::ecr::actions::prev_item(self);
3358 } else if self.current_service == Service::KmsKeys {
3359 crate::kms::actions::prev_item(self);
3360 } else if self.current_service == Service::EfsFileSystems {
3361 crate::efs::actions::prev_item(self);
3362 } else if self.current_service == Service::DynamoDbTables {
3363 ddb_actions::prev_item(self)
3364 } else if self.current_service == Service::FsxFileSystems {
3365 crate::fsx::actions::prev_item(self);
3366 } else if self.current_service == Service::SqsQueues {
3367 crate::sqs::actions::prev_item(self);
3368 } else if self.current_service == Service::LambdaFunctions {
3369 crate::lambda::functions::prev_item(self);
3370 } else if self.current_service == Service::LambdaApplications {
3371 crate::lambda::applications::prev_item(self);
3372 } else if self.current_service == Service::CloudFormationStacks {
3373 crate::cfn::actions::prev_item(self);
3374 } else if self.current_service == Service::IamUsers {
3375 crate::iam::actions::prev_item_users(self);
3376 } else if self.current_service == Service::IamRoles {
3377 crate::iam::actions::prev_item_roles(self);
3378 } else if self.current_service == Service::IamUserGroups {
3379 crate::iam::actions::prev_item_groups(self);
3380 }
3381 }
3382 _ => {}
3383 }
3384 }
3385
3386 fn page_down(&mut self) {
3387 if self.current_service == Service::CloudTrailEvents
3388 && self.cloudtrail_state.current_event.is_some()
3389 {
3390 crate::cloudtrail::actions::page_down_fast(self);
3391 } else if self.mode == Mode::ColumnSelector {
3392 let max = self.get_column_selector_max();
3393 let mut next_idx = (self.column_selector_index + 10).min(max);
3394 if self.is_blank_row_index(next_idx) {
3396 next_idx = (next_idx + 1).min(max);
3397 }
3398 self.column_selector_index = next_idx;
3399 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
3400 crate::s3::actions::page_down_filter_input(self);
3401 } else if self.mode == Mode::FilterInput
3402 && self.current_service == Service::CloudFormationStacks
3403 {
3404 crate::cfn::actions::page_down_filter_input(self);
3405 } else if self.mode == Mode::FilterInput
3406 && self.current_service == Service::IamRoles
3407 && self.iam_state.current_role.is_none()
3408 {
3409 crate::iam::actions::page_down_filter_input_roles(self);
3410 } else if self.mode == Mode::FilterInput
3411 && self.current_service == Service::CloudWatchAlarms
3412 {
3413 crate::cw::actions::alarms_page_down_filter_input(self);
3414 } else if self.mode == Mode::FilterInput
3415 && self.current_service == Service::CloudTrailEvents
3416 {
3417 crate::cloudtrail::actions::page_down_filter_input(self);
3418 } else if self.mode == Mode::FilterInput
3419 && self.current_service == Service::CloudWatchLogGroups
3420 {
3421 crate::cw::actions::logs_page_down_filter_input(self);
3422 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
3423 {
3424 crate::lambda::functions::page_down_filter_input(self);
3425 } else if self.mode == Mode::FilterInput
3426 && self.current_service == Service::LambdaApplications
3427 {
3428 crate::lambda::applications::page_down_filter_input(self);
3429 } else if self.mode == Mode::FilterInput
3430 && self.current_service == Service::EcrRepositories
3431 && self.ecr_state.current_repository.is_none()
3432 {
3433 crate::ecr::actions::page_down_filter_input(self);
3434 } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys {
3435 crate::kms::actions::page_down_filter_input(self);
3436 } else if self.mode == Mode::FilterInput && self.current_service == Service::EfsFileSystems
3437 {
3438 crate::efs::actions::page_down_filter_input(self);
3439 } else if self.mode == Mode::FilterInput && self.current_service == Service::DynamoDbTables
3440 {
3441 ddb_actions::page_down_filter_input(self);
3442 } else if self.mode == Mode::FilterInput && self.current_service == Service::FsxFileSystems
3443 {
3444 crate::fsx::actions::page_down_filter_input(self);
3445 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
3446 crate::iam::actions::page_down_filter_input_policy_view(self);
3447 } else if self.view_mode == ViewMode::PolicyView {
3448 crate::iam::actions::scroll_down_policy_view(self);
3449 } else if self.current_service == Service::CloudFormationStacks
3450 && self.cfn_state.current_stack.is_some()
3451 && self.cfn_state.detail_tab == CfnDetailTab::Template
3452 {
3453 crate::cfn::actions::scroll_down_template_fast(self);
3454 } else if self.current_service == Service::LambdaFunctions
3455 && self.lambda_state.current_function.is_some()
3456 && self.lambda_state.detail_tab == LambdaDetailTab::Monitor
3457 && !self.lambda_state.is_metrics_loading()
3458 {
3459 crate::lambda::functions::scroll_down(self);
3460 } else if self.current_service == Service::Ec2Instances {
3461 crate::ec2::actions::scroll_down(self);
3462 } else if self.current_service == Service::SqsQueues
3463 && self.sqs_state.current_queue.is_some()
3464 {
3465 crate::sqs::actions::scroll_down_fast(self);
3466 } else if self.current_service == Service::DynamoDbTables
3467 && self.dynamodb_state.current_table.is_some()
3468 && self.dynamodb_state.detail_tab == crate::ui::dynamodb::DetailTab::Monitor
3469 && !self.dynamodb_state.is_metrics_loading()
3470 {
3471 ddb_actions::scroll_down_detail(self);
3472 } else if self.current_service == Service::EfsFileSystems
3473 && self.efs_state.current_file_system.is_some()
3474 && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
3475 || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
3476 && !self.efs_state.is_metrics_loading()
3477 {
3478 crate::efs::actions::scroll_down_detail(self);
3479 } else if self.current_service == Service::IamRoles
3480 && self.iam_state.current_role.is_some()
3481 && self.iam_state.role_tab == RoleTab::TrustRelationships
3482 {
3483 crate::iam::actions::scroll_down_trust_policy(self);
3484 } else if self.current_service == Service::IamRoles
3485 && self.iam_state.current_role.is_some()
3486 && self.iam_state.role_tab == RoleTab::RevokeSessions
3487 {
3488 crate::iam::actions::scroll_down_revoke_sessions(self);
3489 } else if self.mode == Mode::Normal {
3490 if self.current_service == Service::S3Buckets {
3491 crate::s3::actions::page_down_normal(self);
3492 } else if self.current_service == Service::CloudWatchLogGroups
3493 && self.view_mode == ViewMode::List
3494 {
3495 crate::cw::actions::logs_page_down_normal_list(self);
3496 } else if self.current_service == Service::CloudWatchLogGroups
3497 && self.view_mode == ViewMode::Detail
3498 {
3499 crate::cw::actions::logs_page_down_normal_detail(self);
3500 } else if self.view_mode == ViewMode::Events {
3501 crate::cw::actions::logs_page_down_events(self);
3502 } else if self.view_mode == ViewMode::InsightsResults {
3503 crate::cw::actions::insights_page_down(self);
3504 } else if self.current_service == Service::CloudWatchAlarms {
3505 crate::cw::actions::alarms_page_down_normal(self);
3506 } else if self.current_service == Service::CloudTrailEvents {
3507 crate::cloudtrail::actions::page_down_normal(self);
3508 } else if self.current_service == Service::Ec2Instances {
3509 crate::ec2::actions::page_down_normal(self);
3510 } else if self.current_service == Service::EcrRepositories {
3511 crate::ecr::actions::page_down_normal(self);
3512 } else if self.current_service == Service::KmsKeys {
3513 crate::kms::actions::page_down_normal(self);
3514 } else if self.current_service == Service::EfsFileSystems {
3515 crate::efs::actions::page_down_normal(self);
3516 } else if self.current_service == Service::DynamoDbTables {
3517 ddb_actions::page_down_normal(self)
3518 } else if self.current_service == Service::FsxFileSystems {
3519 crate::fsx::actions::page_down_normal(self);
3520 } else if self.current_service == Service::SqsQueues {
3521 crate::sqs::actions::page_down_normal(self);
3522 } else if self.current_service == Service::LambdaFunctions {
3523 crate::lambda::functions::page_down_normal(self);
3524 } else if self.current_service == Service::LambdaApplications {
3525 crate::lambda::applications::page_down_normal(self);
3526 } else if self.current_service == Service::CloudFormationStacks {
3527 crate::cfn::actions::page_down_normal(self);
3528 } else if self.current_service == Service::IamUsers {
3529 crate::iam::actions::page_down_normal_users(self);
3530 } else if self.current_service == Service::IamRoles {
3531 crate::iam::actions::page_down_normal_roles(self);
3532 } else if self.current_service == Service::IamUserGroups {
3533 crate::iam::actions::page_down_normal_groups(self);
3534 }
3535 }
3536 }
3537
3538 fn page_up(&mut self) {
3539 if self.current_service == Service::CloudTrailEvents
3540 && self.cloudtrail_state.current_event.is_some()
3541 {
3542 crate::cloudtrail::actions::page_up_fast(self);
3543 } else if self.mode == Mode::ColumnSelector {
3544 let mut prev_idx = self.column_selector_index.saturating_sub(10);
3545 if self.is_blank_row_index(prev_idx) {
3547 prev_idx = prev_idx.saturating_sub(1);
3548 }
3549 self.column_selector_index = prev_idx;
3550 } else if self.mode == Mode::FilterInput && self.current_service == Service::S3Buckets {
3551 crate::s3::actions::page_up_filter_input(self);
3552 } else if self.mode == Mode::FilterInput
3553 && self.current_service == Service::CloudFormationStacks
3554 {
3555 crate::cfn::actions::page_up_filter_input(self);
3556 } else if self.mode == Mode::FilterInput
3557 && self.current_service == Service::IamRoles
3558 && self.iam_state.current_role.is_none()
3559 {
3560 crate::iam::actions::page_up_filter_input_roles(self);
3561 } else if self.mode == Mode::FilterInput
3562 && self.current_service == Service::CloudWatchAlarms
3563 {
3564 crate::cw::actions::alarms_page_up_filter_input(self);
3565 } else if self.mode == Mode::FilterInput
3566 && self.current_service == Service::CloudTrailEvents
3567 {
3568 crate::cloudtrail::actions::page_up_filter_input(self);
3569 } else if self.mode == Mode::FilterInput
3570 && self.current_service == Service::CloudWatchLogGroups
3571 {
3572 crate::cw::actions::logs_page_up_filter_input(self);
3573 } else if self.mode == Mode::FilterInput && self.current_service == Service::LambdaFunctions
3574 {
3575 crate::lambda::functions::page_up_filter_input(self);
3576 } else if self.mode == Mode::FilterInput
3577 && self.current_service == Service::LambdaApplications
3578 {
3579 crate::lambda::applications::page_up_filter_input(self);
3580 } else if self.mode == Mode::FilterInput && self.current_service == Service::KmsKeys {
3581 crate::kms::actions::page_up_filter_input(self);
3582 } else if self.mode == Mode::FilterInput && self.current_service == Service::EfsFileSystems
3583 {
3584 crate::efs::actions::page_up_filter_input(self);
3585 } else if self.mode == Mode::FilterInput && self.current_service == Service::DynamoDbTables
3586 {
3587 ddb_actions::page_up_filter_input(self);
3588 } else if self.mode == Mode::FilterInput && self.current_service == Service::FsxFileSystems
3589 {
3590 crate::fsx::actions::page_up_filter_input(self);
3591 } else if self.mode == Mode::FilterInput
3592 && self.current_service == Service::EcrRepositories
3593 && self.ecr_state.current_repository.is_none()
3594 {
3595 crate::ecr::actions::page_up_filter_input(self);
3596 } else if self.mode == Mode::FilterInput && self.view_mode == ViewMode::PolicyView {
3597 crate::iam::actions::page_up_filter_input_policy_view(self);
3598 } else if self.view_mode == ViewMode::PolicyView {
3599 crate::iam::actions::scroll_up_policy_view(self);
3600 } else if self.current_service == Service::CloudFormationStacks
3601 && self.cfn_state.current_stack.is_some()
3602 && self.cfn_state.detail_tab == CfnDetailTab::Template
3603 {
3604 crate::cfn::actions::scroll_up_template_fast(self);
3605 } else if self.current_service == Service::SqsQueues
3606 && self.sqs_state.current_queue.is_some()
3607 {
3608 crate::sqs::actions::scroll_up_fast(self);
3609 } else if self.current_service == Service::DynamoDbTables
3610 && self.dynamodb_state.current_table.is_some()
3611 && self.dynamodb_state.detail_tab == crate::ui::dynamodb::DetailTab::Monitor
3612 && !self.dynamodb_state.is_metrics_loading()
3613 {
3614 ddb_actions::scroll_up_detail(self);
3615 } else if self.current_service == Service::EfsFileSystems
3616 && self.efs_state.current_file_system.is_some()
3617 && (self.efs_state.detail_tab == crate::ui::efs::DetailTab::Monitoring
3618 || self.efs_state.detail_tab == crate::ui::efs::DetailTab::FileSystemPolicy)
3619 && !self.efs_state.is_metrics_loading()
3620 {
3621 crate::efs::actions::scroll_up_detail(self);
3622 } else if self.current_service == Service::IamRoles
3623 && self.iam_state.current_role.is_some()
3624 && self.iam_state.role_tab == RoleTab::TrustRelationships
3625 {
3626 crate::iam::actions::scroll_up_trust_policy(self);
3627 } else if self.current_service == Service::IamRoles
3628 && self.iam_state.current_role.is_some()
3629 && self.iam_state.role_tab == RoleTab::RevokeSessions
3630 {
3631 crate::iam::actions::scroll_up_revoke_sessions(self);
3632 } else if self.mode == Mode::Normal {
3633 if self.current_service == Service::S3Buckets {
3634 crate::s3::actions::page_up_normal(self);
3635 } else if self.current_service == Service::CloudWatchLogGroups
3636 && self.view_mode == ViewMode::List
3637 {
3638 crate::cw::actions::logs_page_up_normal_list(self);
3639 } else if self.current_service == Service::CloudWatchLogGroups
3640 && self.view_mode == ViewMode::Detail
3641 {
3642 crate::cw::actions::logs_page_up_normal_detail(self);
3643 } else if self.view_mode == ViewMode::Events {
3644 crate::cw::actions::logs_page_up_events(self);
3645 } else if self.view_mode == ViewMode::InsightsResults {
3646 crate::cw::actions::insights_page_up(self);
3647 } else if self.current_service == Service::CloudWatchAlarms {
3648 crate::cw::actions::alarms_page_up_normal(self);
3649 } else if self.current_service == Service::CloudTrailEvents {
3650 crate::cloudtrail::actions::page_up_normal(self);
3651 } else if self.current_service == Service::KmsKeys {
3652 crate::kms::actions::page_up_normal(self);
3653 } else if self.current_service == Service::EfsFileSystems {
3654 crate::efs::actions::page_up_normal(self);
3655 } else if self.current_service == Service::DynamoDbTables {
3656 ddb_actions::page_up_normal(self)
3657 } else if self.current_service == Service::FsxFileSystems {
3658 crate::fsx::actions::page_up_normal(self);
3659 } else if self.current_service == Service::Ec2Instances {
3660 crate::ec2::actions::page_up_normal(self);
3661 } else if self.current_service == Service::EcrRepositories {
3662 crate::ecr::actions::page_up_normal(self);
3663 } else if self.current_service == Service::SqsQueues {
3664 self.sqs_state.queues.page_up();
3665 } else if self.current_service == Service::LambdaFunctions {
3666 crate::lambda::functions::page_up_normal(self);
3667 } else if self.current_service == Service::LambdaApplications {
3668 crate::lambda::applications::page_up_normal(self);
3669 } else if self.current_service == Service::CloudFormationStacks {
3670 crate::cfn::actions::page_up_normal(self);
3671 } else if self.current_service == Service::IamUsers {
3672 crate::iam::actions::page_up_normal_users(self);
3673 } else if self.current_service == Service::IamRoles {
3674 crate::iam::actions::page_up_normal_roles(self);
3675 }
3676 }
3677 }
3678
3679 fn next_pane(&mut self) {
3680 if self.current_service == Service::S3Buckets {
3681 crate::s3::actions::expand_row(self);
3682 } else if self.view_mode == ViewMode::InsightsResults {
3683 let max_cols = self
3685 .insights_state
3686 .insights
3687 .query_results
3688 .first()
3689 .map(|r| r.len())
3690 .unwrap_or(0);
3691 if self.insights_state.insights.results_horizontal_scroll < max_cols.saturating_sub(1) {
3692 self.insights_state.insights.results_horizontal_scroll += 1;
3693 }
3694 } else if self.current_service == Service::CloudWatchLogGroups
3695 || self.view_mode == ViewMode::Events
3696 {
3697 crate::cw::actions::logs_expand_row(self);
3698 } else if self.current_service == Service::CloudWatchAlarms {
3699 crate::cw::actions::alarms_expand_row(self);
3700 } else if self.current_service == Service::Ec2Instances {
3701 crate::ec2::actions::expand_row(self);
3702 } else if self.current_service == Service::EcrRepositories {
3703 crate::ecr::actions::next_pane(self);
3704 } else if self.current_service == Service::KmsKeys {
3705 crate::kms::actions::expand_row(self);
3706 } else if self.current_service == Service::EfsFileSystems {
3707 crate::efs::actions::expand_row(self);
3708 } else if self.current_service == Service::DynamoDbTables {
3709 ddb_actions::expand_row(self)
3710 } else if self.current_service == Service::FsxFileSystems {
3711 crate::fsx::actions::expand_row(self);
3712 } else if self.current_service == Service::SqsQueues {
3713 crate::sqs::actions::expand_row(self);
3714 } else if self.current_service == Service::LambdaFunctions {
3715 crate::lambda::functions::expand_row(self);
3716 } else if self.current_service == Service::LambdaApplications {
3717 crate::lambda::applications::expand_row(self);
3718 } else if self.current_service == Service::CloudFormationStacks
3719 && self.cfn_state.current_stack.is_none()
3720 {
3721 self.cfn_state.table.toggle_expand();
3722 } else if self.current_service == Service::CloudFormationStacks
3723 && self.cfn_state.current_stack.is_some()
3724 && self.cfn_state.detail_tab == CfnDetailTab::Parameters
3725 {
3726 self.cfn_state.parameters.toggle_expand();
3727 } else if self.current_service == Service::CloudFormationStacks
3728 && self.cfn_state.current_stack.is_some()
3729 && self.cfn_state.detail_tab == CfnDetailTab::Outputs
3730 {
3731 self.cfn_state.outputs.toggle_expand();
3732 } else if self.current_service == Service::CloudFormationStacks
3733 && self.cfn_state.current_stack.is_some()
3734 && self.cfn_state.detail_tab == CfnDetailTab::Resources
3735 {
3736 self.cfn_state.resources.toggle_expand();
3737 } else if self.current_service == Service::IamUsers {
3738 crate::iam::actions::expand_row_users(self);
3739 } else if self.current_service == Service::IamRoles {
3740 crate::iam::actions::expand_row_roles(self);
3741 } else if self.current_service == Service::IamUserGroups {
3742 crate::iam::actions::expand_row_groups(self);
3743 }
3744 }
3745
3746 fn go_to_page(&mut self, page: usize) {
3747 if self.current_service == Service::VpcVpcs {
3748 vpc_actions::go_to_page(self, page);
3749 return;
3750 }
3751 if self.current_service == Service::RdsInstances {
3752 rds_actions::go_to_page(self, page);
3753 return;
3754 }
3755 if page == 0 {
3756 return;
3757 }
3758
3759 match self.current_service {
3760 Service::CloudWatchAlarms => {
3761 crate::cw::actions::alarms_go_to_page(self, page);
3762 }
3763 Service::CloudTrailEvents => {
3764 let page_size = self.cloudtrail_state.table.page_size.value();
3765 let filtered_count = self.cloudtrail_state.table.items.len();
3766 let max_page = (filtered_count / page_size) + 1; if page <= max_page {
3770 let target = (page - 1) * page_size;
3771 self.cloudtrail_state.table.scroll_offset = target;
3772 self.cloudtrail_state.table.selected = target;
3773 self.cloudtrail_state.table.expanded_item = None; }
3775 }
3777 Service::CloudWatchLogGroups => {
3778 crate::cw::actions::logs_go_to_page(self, page);
3779 }
3780 Service::EcrRepositories => {
3781 crate::ecr::actions::go_to_page(self, page);
3782 }
3783 Service::KmsKeys => {
3784 crate::kms::actions::go_to_page(self, page);
3785 }
3786 Service::EfsFileSystems => {
3787 crate::efs::actions::go_to_page(self, page);
3788 }
3789 Service::DynamoDbTables => {
3790 ddb_actions::go_to_page(self, page);
3791 }
3792 Service::FsxFileSystems => {
3793 crate::fsx::actions::go_to_page(self, page);
3794 }
3795 Service::SqsQueues => {
3796 crate::sqs::actions::go_to_page(self, page);
3797 }
3798 Service::S3Buckets => {
3799 crate::s3::actions::go_to_page(self, page);
3800 }
3801 Service::LambdaFunctions => {
3802 crate::lambda::functions::go_to_page(self, page);
3803 }
3804 Service::LambdaApplications => {
3805 crate::lambda::applications::go_to_page(self, page);
3806 }
3807 Service::CloudFormationStacks => {
3808 crate::cfn::actions::go_to_page(self, page);
3809 }
3810 Service::IamUsers => {
3811 crate::iam::actions::go_to_page_users(self, page);
3812 }
3813 Service::IamRoles => {
3814 crate::iam::actions::go_to_page_roles(self, page);
3815 }
3816 _ => {}
3817 }
3818 }
3819
3820 fn prev_pane(&mut self) {
3821 if self.current_service == Service::S3Buckets {
3822 crate::s3::actions::prev_pane(self);
3823 } else if self.view_mode == ViewMode::InsightsResults {
3824 self.insights_state.insights.results_horizontal_scroll = self
3826 .insights_state
3827 .insights
3828 .results_horizontal_scroll
3829 .saturating_sub(1);
3830 } else if self.current_service == Service::CloudWatchLogGroups
3831 || self.view_mode == ViewMode::Events
3832 {
3833 crate::cw::actions::logs_prev_pane(self);
3834 } else if self.current_service == Service::CloudWatchAlarms {
3835 crate::cw::actions::alarms_prev_pane(self);
3836 } else if self.current_service == Service::Ec2Instances {
3837 crate::ec2::actions::prev_pane(self);
3838 } else if self.current_service == Service::ApiGatewayApis {
3839 crate::apig::actions::prev_pane(self);
3840 } else if self.current_service == Service::EcrRepositories {
3841 crate::ecr::actions::prev_pane(self);
3842 } else if self.current_service == Service::KmsKeys {
3843 crate::kms::actions::prev_pane(self);
3844 } else if self.current_service == Service::EfsFileSystems {
3845 crate::efs::actions::prev_pane(self);
3846 } else if self.current_service == Service::DynamoDbTables {
3847 ddb_actions::prev_pane(self)
3848 } else if self.current_service == Service::FsxFileSystems {
3849 crate::fsx::actions::prev_pane(self);
3850 } else if self.current_service == Service::SqsQueues {
3851 crate::sqs::actions::prev_pane(self);
3852 } else if self.current_service == Service::LambdaFunctions {
3853 crate::lambda::functions::prev_pane(self);
3854 } else if self.current_service == Service::LambdaApplications {
3855 crate::lambda::applications::prev_pane(self);
3856 } else if self.current_service == Service::CloudFormationStacks {
3857 crate::cfn::actions::prev_pane(self);
3858 } else if self.current_service == Service::IamUsers {
3859 crate::iam::actions::prev_pane_users(self);
3860 } else if self.current_service == Service::IamRoles {
3861 crate::iam::actions::prev_pane_roles(self);
3862 } else if self.current_service == Service::IamUserGroups {
3863 crate::iam::actions::prev_pane_groups(self);
3864 }
3865 }
3866
3867 fn collapse_row(&mut self) {
3868 match self.current_service {
3869 Service::S3Buckets => {
3870 crate::s3::actions::collapse_row(self);
3871 }
3872 Service::CloudWatchLogGroups => {
3873 crate::cw::actions::logs_collapse_row(self);
3874 }
3875 Service::CloudWatchAlarms => crate::cw::actions::alarms_collapse_row(self),
3876 Service::Ec2Instances => {
3877 crate::ec2::actions::collapse_row(self);
3878 }
3879 Service::EcrRepositories => {
3880 crate::ecr::actions::collapse_row(self);
3881 }
3882 Service::LambdaFunctions => crate::lambda::functions::collapse_row(self),
3883 Service::LambdaApplications => crate::lambda::applications::prev_pane(self),
3884 Service::SqsQueues => crate::sqs::actions::collapse_row(self),
3885 Service::CloudFormationStacks => {
3886 crate::cfn::actions::collapse_row(self);
3887 }
3888 Service::IamUsers => {
3889 crate::iam::actions::collapse_row_users(self);
3890 }
3891 Service::IamRoles => {
3892 crate::iam::actions::collapse_row_roles(self);
3893 }
3894 Service::IamUserGroups => {
3895 crate::iam::actions::collapse_row_groups(self);
3896 }
3897 Service::ApiGatewayApis => {
3898 crate::apig::actions::collapse_row(self);
3899 }
3900 Service::KmsKeys => {
3901 crate::kms::actions::collapse_row(self);
3902 }
3903 Service::EfsFileSystems => {
3904 crate::efs::actions::collapse_row(self);
3905 }
3906 Service::DynamoDbTables => {
3907 ddb_actions::collapse_row(self);
3908 }
3909 Service::FsxFileSystems => {
3910 crate::fsx::actions::collapse_row(self);
3911 }
3912 Service::CloudTrailEvents => {
3913 if self.cloudtrail_state.current_event.is_some()
3914 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
3915 {
3916 self.cloudtrail_state.resources_expanded_index = None;
3918 } else {
3919 self.cloudtrail_state.table.collapse();
3920 }
3921 }
3922 _ => {}
3923 }
3924 }
3925
3926 fn expand_row(&mut self) {
3927 match self.current_service {
3928 Service::S3Buckets => {
3929 crate::s3::actions::expand_row_left(self);
3930 }
3931 Service::ApiGatewayApis => {
3932 crate::apig::actions::expand_row(self);
3933 }
3934 Service::CloudTrailEvents => {
3935 if self.cloudtrail_state.current_event.is_some()
3936 && self.cloudtrail_state.detail_focus == CloudTrailDetailFocus::Resources
3937 {
3938 self.cloudtrail_state.resources_expanded_index = Some(0);
3940 } else {
3941 self.cloudtrail_state.table.expand();
3942 }
3943 }
3944 Service::CloudFormationStacks => {
3945 crate::cfn::actions::expand_row(self);
3946 }
3947 _ => {
3948 self.next_pane();
3950 }
3951 }
3952 }
3953
3954 fn select_item(&mut self) {
3955 if self.mode == Mode::RegionPicker {
3956 if self.region_filter_active {
3958 self.region_filter_active = false;
3959 return;
3960 }
3961 let filtered = self.get_filtered_regions();
3962 if let Some(region) = filtered.get(self.region_picker_selected) {
3963 if !self.tabs.is_empty() {
3965 let mut session = Session::new(
3966 self.profile.clone(),
3967 self.region.clone(),
3968 self.config.account_id.clone(),
3969 self.config.role_arn.clone(),
3970 );
3971
3972 for tab in &self.tabs {
3973 session.tabs.push(SessionTab {
3974 service: format!("{:?}", tab.service),
3975 title: tab.title.clone(),
3976 breadcrumb: tab.breadcrumb.clone(),
3977 filter: None,
3978 selected_item: None,
3979 });
3980 }
3981
3982 let _ = session.save();
3983 }
3984
3985 self.region = region.code.to_string();
3986 self.config.region = region.code.to_string();
3987
3988 self.tabs.clear();
3990 self.current_tab = 0;
3991 self.service_selected = false;
3992
3993 self.mode = Mode::Normal;
3994 }
3995 } else if self.mode == Mode::ProfilePicker {
3996 let filtered = self.get_filtered_profiles();
3997 if let Some(profile) = filtered.get(self.profile_picker_selected) {
3998 let profile_name = profile.name.clone();
3999 let profile_region = profile.region.clone();
4000
4001 self.profile = profile_name.clone();
4002 std::env::set_var("AWS_PROFILE", &profile_name);
4003
4004 if let Some(region) = profile_region {
4006 self.region = region;
4007 }
4008
4009 self.mode = Mode::Normal;
4010 }
4012 } else if self.mode == Mode::ServicePicker {
4013 if self.service_picker.filter_active {
4015 self.service_picker.filter_active = false;
4016 return;
4017 }
4018
4019 let filtered = self.filtered_services();
4020 if let Some(&service) = filtered.get(self.service_picker.selected) {
4021 let new_service = match service {
4022 "API Gateway › APIs" => Service::ApiGatewayApis,
4023 "CloudWatch › Log Groups" => Service::CloudWatchLogGroups,
4024 "CloudWatch › Logs Insights" => Service::CloudWatchInsights,
4025 "CloudWatch › Alarms" => Service::CloudWatchAlarms,
4026 "CloudTrail › Event History" => Service::CloudTrailEvents,
4027 "CloudFormation › Stacks" => Service::CloudFormationStacks,
4028 "EC2 › Instances" => Service::Ec2Instances,
4029 "ECR › Repositories" => Service::EcrRepositories,
4030 "KMS › Managed Keys" => Service::KmsKeys,
4031 "EFS › File Systems" => Service::EfsFileSystems,
4032 "FSx › File Systems" => Service::FsxFileSystems,
4033 "RDS › Databases" | "RDS › DB Instances" => Service::RdsInstances,
4034 "VPC › VPCs" => Service::VpcVpcs,
4035 "DynamoDB › Tables" => Service::DynamoDbTables,
4036 "IAM › Users" => Service::IamUsers,
4037 "IAM › Roles" => Service::IamRoles,
4038 "IAM › User Groups" => Service::IamUserGroups,
4039 "Lambda › Functions" => Service::LambdaFunctions,
4040 "Lambda › Applications" => Service::LambdaApplications,
4041 "S3 › Buckets" => Service::S3Buckets,
4042 "SQS › Queues" => Service::SqsQueues,
4043 _ => return,
4044 };
4045
4046 self.tabs.push(Tab {
4048 service: new_service,
4049 title: service.to_string(),
4050 breadcrumb: service.to_string(),
4051 });
4052 self.current_tab = self.tabs.len() - 1;
4053 self.current_service = new_service;
4054 if new_service == Service::VpcVpcs {
4055 self.vpc_state.vpcs.reset();
4056 self.vpc_state.vpcs.loading = true;
4057 }
4058 if new_service == Service::RdsInstances {
4059 self.rds_state.instances.reset();
4060 self.rds_state.instances.loading = true;
4061 }
4062 self.view_mode = ViewMode::List;
4063 self.service_selected = true;
4064 self.mode = Mode::Normal;
4065 }
4066 } else if self.mode == Mode::TabPicker {
4067 let filtered = self.get_filtered_tabs();
4068 if let Some(&(idx, _)) = filtered.get(self.tab_picker_selected) {
4069 self.current_tab = idx;
4070 self.current_service = self.tabs[idx].service;
4071 self.mode = Mode::Normal;
4072 self.tab_filter.clear();
4073 }
4074 } else if self.mode == Mode::SessionPicker {
4075 let filtered = self.get_filtered_sessions();
4076 if let Some(&session) = filtered.get(self.session_picker_selected) {
4077 let session = session.clone();
4078
4079 self.current_session = Some(session.clone());
4081 self.profile = session.profile.clone();
4082 self.region = session.region.clone();
4083 self.config.region = session.region.clone();
4084 self.config.account_id = session.account_id.clone();
4085 self.config.role_arn = session.role_arn.clone();
4086
4087 self.tabs = session
4089 .tabs
4090 .iter()
4091 .map(|st| Tab {
4092 service: match st.service.as_str() {
4093 "RdsInstances" => Service::RdsInstances,
4094 "VpcVpcs" => Service::VpcVpcs,
4095 "CloudWatchLogGroups" => Service::CloudWatchLogGroups,
4096 "CloudWatchInsights" => Service::CloudWatchInsights,
4097 "CloudWatchAlarms" => Service::CloudWatchAlarms,
4098 "S3Buckets" => Service::S3Buckets,
4099 "CloudTrailEvents" => Service::CloudTrailEvents,
4100 "SqsQueues" => Service::SqsQueues,
4101 _ => Service::CloudWatchLogGroups,
4102 },
4103 title: if st.service == "RdsInstances" {
4104 Service::RdsInstances.name().into()
4105 } else {
4106 st.title.clone()
4107 },
4108 breadcrumb: if st.service == "RdsInstances" {
4109 Service::RdsInstances.name().into()
4110 } else {
4111 st.breadcrumb.clone()
4112 },
4113 })
4114 .collect();
4115
4116 if !self.tabs.is_empty() {
4117 self.current_tab = 0;
4118 self.current_service = self.tabs[0].service;
4119 self.service_selected = true;
4120 }
4121
4122 self.mode = Mode::Normal;
4123 }
4124 } else if self.mode == Mode::InsightsInput {
4125 use crate::app::InsightsFocus;
4127 match self.insights_state.insights.insights_focus {
4128 InsightsFocus::Query => {
4129 self.insights_state.insights.query_text.push('\n');
4131 self.insights_state.insights.query_cursor_line += 1;
4132 self.insights_state.insights.query_cursor_col = 0;
4133 }
4134 InsightsFocus::LogGroupSearch => {
4135 let was_open = self.insights_state.insights.show_dropdown;
4137 self.insights_state.insights.show_dropdown = !was_open;
4138 if !was_open {
4139 if self.insights_state.insights.log_group_search.is_empty() {
4141 self.insights_state.insights.log_group_matches = self
4142 .log_groups_state
4143 .log_groups
4144 .items
4145 .iter()
4146 .take(50)
4147 .map(|g| g.name.clone())
4148 .collect();
4149 }
4150 self.insights_state.insights.dropdown_selected = 0;
4151 }
4152 }
4153 _ => {}
4154 }
4155 } else if self.mode == Mode::Normal {
4156 if !self.service_selected {
4158 let filtered = self.filtered_services();
4159 if let Some(&service) = filtered.get(self.service_picker.selected) {
4160 match service {
4161 "CloudWatch › Log Groups" => {
4162 self.current_service = Service::CloudWatchLogGroups;
4163 self.view_mode = ViewMode::List;
4164 self.service_selected = true;
4165 }
4166 "CloudWatch › Logs Insights" => {
4167 self.current_service = Service::CloudWatchInsights;
4168 self.view_mode = ViewMode::InsightsResults;
4169 self.service_selected = true;
4170 }
4171 "CloudWatch › Alarms" => {
4172 self.current_service = Service::CloudWatchAlarms;
4173 self.view_mode = ViewMode::List;
4174 self.service_selected = true;
4175 }
4176 "S3 › Buckets" => {
4177 self.current_service = Service::S3Buckets;
4178 self.view_mode = ViewMode::List;
4179 self.service_selected = true;
4180 }
4181 "EC2 › Instances" => {
4182 self.current_service = Service::Ec2Instances;
4183 self.view_mode = ViewMode::List;
4184 self.service_selected = true;
4185 }
4186 "ECR › Repositories" => {
4187 self.current_service = Service::EcrRepositories;
4188 self.view_mode = ViewMode::List;
4189 self.service_selected = true;
4190 }
4191 "Lambda › Functions" => {
4192 self.current_service = Service::LambdaFunctions;
4193 self.view_mode = ViewMode::List;
4194 self.service_selected = true;
4195 }
4196 "Lambda › Applications" => {
4197 self.current_service = Service::LambdaApplications;
4198 self.view_mode = ViewMode::List;
4199 self.service_selected = true;
4200 }
4201 _ => {}
4202 }
4203 }
4204 return;
4205 }
4206
4207 if self.view_mode == ViewMode::InsightsResults {
4209 if self.insights_state.insights.expanded_result
4211 == Some(self.insights_state.insights.results_selected)
4212 {
4213 self.insights_state.insights.expanded_result = None;
4214 } else {
4215 self.insights_state.insights.expanded_result =
4216 Some(self.insights_state.insights.results_selected);
4217 }
4218 } else if self.current_service == Service::S3Buckets {
4219 crate::s3::actions::select_item(self);
4220 } else if self.current_service == Service::ApiGatewayApis {
4221 crate::apig::actions::select_item(self);
4222 } else if self.current_service == Service::CloudFormationStacks {
4223 if self.cfn_state.current_stack.is_none() {
4224 let filtered_stacks = filtered_cloudformation_stacks(self);
4226 if let Some(stack) = self.cfn_state.table.get_selected(&filtered_stacks) {
4227 let stack_name = stack.name.clone();
4228 let mut tags = stack.tags.clone();
4229 tags.sort_by(|a, b| a.0.cmp(&b.0));
4230
4231 self.cfn_state.current_stack = Some(stack_name);
4232 self.cfn_state.tags.items = tags;
4233 self.cfn_state.tags.reset();
4234 self.cfn_state.table.loading = true;
4235 self.update_current_tab_breadcrumb();
4236 }
4237 }
4238 } else if self.current_service == Service::CloudWatchAlarms {
4239 crate::cw::actions::alarms_select_item(self);
4240 } else if self.current_service == Service::CloudTrailEvents {
4241 crate::cloudtrail::actions::select_item(self);
4242 } else if self.current_service == Service::EcrRepositories {
4243 crate::ecr::actions::select_item(self);
4244 } else if self.current_service == Service::KmsKeys {
4245 } else if self.current_service == Service::EfsFileSystems {
4247 crate::efs::actions::select_item(self);
4248 } else if self.current_service == Service::DynamoDbTables {
4249 ddb_actions::select_item(self)
4250 } else if self.current_service == Service::FsxFileSystems {
4251 crate::fsx::actions::select_item(self);
4252 } else if self.current_service == Service::Ec2Instances {
4253 crate::ec2::actions::select_item(self);
4254 } else if self.current_service == Service::SqsQueues {
4255 crate::sqs::actions::select_item(self);
4256 } else if self.current_service == Service::IamUsers {
4257 crate::iam::actions::select_item_users(self);
4258 } else if self.current_service == Service::IamRoles {
4259 crate::iam::actions::select_item_roles(self);
4260 } else if self.current_service == Service::IamUserGroups {
4261 crate::iam::actions::select_item_groups(self);
4262 } else if self.current_service == Service::LambdaFunctions {
4263 crate::lambda::functions::select_item(self);
4264 } else if self.current_service == Service::LambdaApplications {
4265 crate::lambda::applications::select_item(self);
4266 } else if self.current_service == Service::CloudWatchLogGroups {
4267 crate::cw::actions::logs_select_item(self);
4268 } else if self.current_service == Service::CloudWatchAlarms
4269 && self.view_mode != ViewMode::Detail
4270 {
4271 crate::cw::actions::alarms_select_expand(self);
4272 } else if self.current_service == Service::CloudWatchInsights {
4273 if !self.insights_state.insights.selected_log_groups.is_empty() {
4275 self.log_groups_state.loading = true;
4276 self.insights_state.insights.query_completed = true;
4277 }
4278 }
4279 }
4280 }
4281
4282 pub async fn load_log_groups(&mut self) -> anyhow::Result<()> {
4283 self.log_groups_state.log_groups.items = self.cloudwatch_client.list_log_groups().await?;
4284 Ok(())
4285 }
4286
4287 pub async fn load_alarms(&mut self) -> anyhow::Result<()> {
4288 let alarm_data = self.alarms_client.list_alarms().await?;
4289 self.alarms_state.table.items = alarm_data
4290 .into_iter()
4291 .map(|d| Alarm {
4292 name: d.name,
4293 state: d.state,
4294 state_updated_timestamp: d.state_updated,
4295 description: d.description,
4296 metric_name: d.metric_name,
4297 namespace: d.namespace,
4298 statistic: d.statistic,
4299 period: d.period,
4300 comparison_operator: d.comparison,
4301 threshold: d.threshold,
4302 actions_enabled: d.actions_enabled,
4303 state_reason: d.state_reason,
4304 resource: d.resource,
4305 dimensions: d.dimensions,
4306 expression: d.expression,
4307 alarm_type: d.alarm_type,
4308 cross_account: d.cross_account,
4309 alarm_arn: d.alarm_arn,
4310 datapoints_to_alarm: d.datapoints_to_alarm,
4311 evaluation_periods: d.evaluation_periods,
4312 treat_missing_data: d.treat_missing_data,
4313 evaluate_low_sample_percentile: d.evaluate_low_sample_percentile,
4314 sub_metrics: d.sub_metrics,
4315 })
4316 .collect();
4317 Ok(())
4318 }
4319
4320 pub async fn load_alarm_metric_math_data(&mut self, alarm_name: &str) -> anyhow::Result<()> {
4322 if let Some(alarm) = self
4323 .alarms_state
4324 .table
4325 .items
4326 .iter()
4327 .find(|a| a.name == alarm_name)
4328 {
4329 if alarm.sub_metrics.is_empty() {
4330 return Ok(());
4331 }
4332 let sub_metrics = alarm.sub_metrics.clone();
4333 let end_time = chrono::Utc::now();
4334 let start_time = end_time - chrono::TimeDelta::hours(72);
4335 let data = self
4336 .alarms_client
4337 .get_metric_math_data(&sub_metrics, start_time, end_time)
4338 .await?;
4339 self.alarms_state.metric_data = data;
4340 }
4341 Ok(())
4342 }
4343
4344 pub async fn load_cloudtrail_events(&mut self) -> anyhow::Result<()> {
4345 let filter = self.cloudtrail_state.active_event_name_filter.clone();
4346 let (events, next_token) = self
4347 .cloudtrail_client
4348 .lookup_events(None, None, filter)
4349 .await?;
4350 self.cloudtrail_state.table.items = events
4351 .into_iter()
4352 .map(
4353 |(
4354 event_name,
4355 event_time,
4356 username,
4357 event_source,
4358 resource_type,
4359 resource_name,
4360 read_only,
4361 aws_region,
4362 event_id,
4363 access_key_id,
4364 source_ip_address,
4365 error_code,
4366 request_id,
4367 event_type,
4368 cloud_trail_event_json,
4369 )| CloudTrailEvent {
4370 event_name,
4371 event_time,
4372 username,
4373 event_source,
4374 resource_type,
4375 resource_name,
4376 read_only,
4377 aws_region,
4378 event_id,
4379 access_key_id,
4380 source_ip_address,
4381 error_code,
4382 request_id,
4383 event_type,
4384 cloud_trail_event_json,
4385 },
4386 )
4387 .collect();
4388 self.cloudtrail_state.table.next_token = next_token;
4389 Ok(())
4390 }
4391
4392 pub async fn load_more_cloudtrail_events(&mut self) -> anyhow::Result<()> {
4393 if let Some(token) = self.cloudtrail_state.table.next_token.clone() {
4394 let filter = self.cloudtrail_state.active_event_name_filter.clone();
4395 let (events, next_token) = self
4397 .cloudtrail_client
4398 .lookup_events(None, Some(token), filter)
4399 .await?;
4400 self.cloudtrail_state
4401 .table
4402 .items
4403 .extend(events.into_iter().map(
4404 |(
4405 event_name,
4406 event_time,
4407 username,
4408 event_source,
4409 resource_type,
4410 resource_name,
4411 read_only,
4412 aws_region,
4413 event_id,
4414 access_key_id,
4415 source_ip_address,
4416 error_code,
4417 request_id,
4418 event_type,
4419 cloud_trail_event_json,
4420 )| CloudTrailEvent {
4421 event_name,
4422 event_time,
4423 username,
4424 event_source,
4425 resource_type,
4426 resource_name,
4427 read_only,
4428 aws_region,
4429 event_id,
4430 access_key_id,
4431 source_ip_address,
4432 error_code,
4433 request_id,
4434 event_type,
4435 cloud_trail_event_json,
4436 },
4437 ));
4438 self.cloudtrail_state.table.next_token = next_token;
4439 }
4440 Ok(())
4441 }
4442
4443 pub async fn load_s3_objects(&mut self) -> anyhow::Result<()> {
4444 if let Some(bucket_name) = &self.s3_state.current_bucket {
4445 let bucket_region = if let Some(bucket) = self
4447 .s3_state
4448 .buckets
4449 .items
4450 .iter_mut()
4451 .find(|b| &b.name == bucket_name)
4452 {
4453 if bucket.region.is_empty() {
4454 let region = self.s3_client.get_bucket_location(bucket_name).await?;
4456 bucket.region = region.clone();
4457 region
4458 } else {
4459 bucket.region.clone()
4460 }
4461 } else {
4462 self.config.region.clone()
4463 };
4464
4465 let prefix = self
4466 .s3_state
4467 .prefix_stack
4468 .last()
4469 .cloned()
4470 .unwrap_or_default();
4471 let objects = self
4472 .s3_client
4473 .list_objects(bucket_name, &bucket_region, &prefix)
4474 .await?;
4475 self.s3_state.objects = objects
4476 .into_iter()
4477 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
4478 key,
4479 size,
4480 last_modified: modified,
4481 is_prefix,
4482 storage_class,
4483 })
4484 .collect();
4485 self.s3_state.selected_object = 0;
4486 }
4487 Ok(())
4488 }
4489
4490 pub async fn load_bucket_preview(&mut self, bucket_name: String) -> anyhow::Result<()> {
4491 let stored_region = self
4492 .s3_state
4493 .buckets
4494 .items
4495 .iter()
4496 .find(|b| b.name == bucket_name)
4497 .and_then(|b| {
4498 if b.region.is_empty() {
4499 None
4500 } else {
4501 Some(b.region.clone())
4502 }
4503 });
4504
4505 let bucket_region = match stored_region {
4506 Some(r) => r,
4507 None => {
4508 let r = self.s3_client.get_bucket_location(&bucket_name).await?;
4509 if let Some(b) = self
4511 .s3_state
4512 .buckets
4513 .items
4514 .iter_mut()
4515 .find(|b| b.name == bucket_name)
4516 {
4517 b.region = r.clone();
4518 }
4519 r
4520 }
4521 };
4522 let objects = self
4523 .s3_client
4524 .list_objects(&bucket_name, &bucket_region, "")
4525 .await?;
4526 let preview: Vec<S3Object> = objects
4527 .into_iter()
4528 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
4529 key,
4530 size,
4531 last_modified: modified,
4532 is_prefix,
4533 storage_class,
4534 })
4535 .collect();
4536 self.s3_state
4537 .bucket_preview
4538 .insert(bucket_name.clone(), preview);
4539 self.after_bucket_preview_loaded(&bucket_name);
4540 Ok(())
4541 }
4542
4543 pub fn after_bucket_preview_loaded(&mut self, bucket_name: &str) {
4547 if self.s3_state.current_bucket.is_some() {
4548 return; }
4550 let mut row_idx = 0usize;
4552 for b in &self.s3_state.buckets.items {
4553 if b.name == bucket_name {
4554 if self.s3_state.selected_row == row_idx {
4556 if let Some(preview) = self.s3_state.bucket_preview.get(bucket_name) {
4557 if !preview.is_empty() {
4558 self.s3_state.selected_row = row_idx + 1;
4559 let visible = self.s3_state.bucket_visible_rows.get();
4560 if self.s3_state.selected_row
4561 >= self.s3_state.bucket_scroll_offset + visible
4562 {
4563 self.s3_state.bucket_scroll_offset =
4564 self.s3_state.selected_row.saturating_sub(visible - 1);
4565 }
4566 }
4567 }
4568 }
4569 return;
4570 }
4571 row_idx += 1;
4572 if self.s3_state.expanded_prefixes.contains(&b.name) {
4574 if let Some(p) = self.s3_state.bucket_preview.get(&b.name) {
4575 row_idx += p.len();
4576 }
4577 }
4578 }
4579 }
4580
4581 pub async fn load_prefix_preview(
4582 &mut self,
4583 bucket_name: String,
4584 prefix: String,
4585 ) -> anyhow::Result<()> {
4586 let stored_region = self
4587 .s3_state
4588 .buckets
4589 .items
4590 .iter()
4591 .find(|b| b.name == bucket_name)
4592 .and_then(|b| {
4593 if b.region.is_empty() {
4594 None
4595 } else {
4596 Some(b.region.clone())
4597 }
4598 });
4599
4600 let bucket_region = match stored_region {
4601 Some(r) => r,
4602 None => {
4603 let r = self.s3_client.get_bucket_location(&bucket_name).await?;
4604 if let Some(b) = self
4605 .s3_state
4606 .buckets
4607 .items
4608 .iter_mut()
4609 .find(|b| b.name == bucket_name)
4610 {
4611 b.region = r.clone();
4612 }
4613 r
4614 }
4615 };
4616 let objects = self
4617 .s3_client
4618 .list_objects(&bucket_name, &bucket_region, &prefix)
4619 .await?;
4620 let preview: Vec<S3Object> = objects
4621 .into_iter()
4622 .map(|(key, size, modified, is_prefix, storage_class)| S3Object {
4623 key,
4624 size,
4625 last_modified: modified,
4626 is_prefix,
4627 storage_class,
4628 })
4629 .collect();
4630 self.s3_state.prefix_preview.insert(prefix, preview);
4631 Ok(())
4632 }
4633
4634 pub async fn load_ecr_repositories(&mut self) -> anyhow::Result<()> {
4635 let repos = match self.ecr_state.tab {
4636 EcrTab::Private => self.ecr_client.list_private_repositories().await?,
4637 EcrTab::Public => self.ecr_client.list_public_repositories().await?,
4638 };
4639
4640 self.ecr_state.repositories.items = repos
4641 .into_iter()
4642 .map(|r| EcrRepository {
4643 name: r.name,
4644 uri: r.uri,
4645 created_at: r.created_at,
4646 tag_immutability: r.tag_immutability,
4647 encryption_type: r.encryption_type,
4648 })
4649 .collect();
4650
4651 self.ecr_state
4652 .repositories
4653 .items
4654 .sort_by(|a, b| a.name.cmp(&b.name));
4655 Ok(())
4656 }
4657
4658 pub async fn load_kms_keys(&mut self) -> anyhow::Result<()> {
4659 let keys = self.kms_client.list_keys().await?;
4660 self.kms_state.keys.items = keys
4661 .into_iter()
4662 .map(|k| KmsKey {
4663 key_id: k.key_id,
4664 key_arn: k.key_arn,
4665 alias: k.alias,
4666 description: k.description,
4667 key_state: k.key_state,
4668 key_usage: k.key_usage,
4669 key_spec: k.key_spec,
4670 key_manager: k.key_manager,
4671 creation_date: k.creation_date,
4672 expiration_date: k.expiration_date,
4673 deletion_date: k.deletion_date,
4674 custom_key_store_id: k.custom_key_store_id,
4675 origin: k.origin,
4676 multi_region: k.multi_region,
4677 enabled: k.enabled,
4678 })
4679 .collect();
4680 Ok(())
4681 }
4682
4683 pub async fn load_efs_file_systems(&mut self) -> anyhow::Result<()> {
4684 let file_systems = self.efs_client.list_file_systems().await?;
4685 self.efs_state.file_systems.items = file_systems
4686 .into_iter()
4687 .map(|f| EfsFileSystem {
4688 file_system_id: f.file_system_id,
4689 file_system_arn: f.file_system_arn,
4690 name: f.name,
4691 creation_token: f.creation_token,
4692 encrypted: f.encrypted,
4693 kms_key_id: f.kms_key_id,
4694 total_size: f.total_size,
4695 size_in_standard: f.size_in_standard,
4696 size_in_ia: f.size_in_ia,
4697 size_in_archive: f.size_in_archive,
4698 provisioned_throughput: f.provisioned_throughput,
4699 throughput_mode: f.throughput_mode,
4700 life_cycle_state: f.life_cycle_state,
4701 number_of_mount_targets: f.number_of_mount_targets,
4702 owner_id: f.owner_id,
4703 creation_time: f.creation_time,
4704 performance_mode: f.performance_mode,
4705 availability_zone: f.availability_zone,
4706 replication_overwrite_protection: f.replication_overwrite_protection,
4707 dns_name: f.dns_name,
4708 tags: f.tags,
4709 total_size_bytes: f.total_size_bytes,
4710 size_in_standard_bytes: f.size_in_standard_bytes,
4711 size_in_ia_bytes: f.size_in_ia_bytes,
4712 size_in_archive_bytes: f.size_in_archive_bytes,
4713 })
4714 .collect();
4715 Ok(())
4716 }
4717
4718 pub async fn load_dynamodb_tables(&mut self) -> anyhow::Result<()> {
4719 let tables = self.dynamodb_client.list_tables().await?;
4720 self.dynamodb_state.tables.items = tables;
4721 Ok(())
4722 }
4723
4724 pub async fn load_dynamodb_backups(&mut self, table_name: &str) -> anyhow::Result<()> {
4725 let backups = self.dynamodb_client.list_backups(table_name).await?;
4726 self.dynamodb_state.backups.items = backups;
4727 self.dynamodb_state.backups.selected = 0;
4728 self.dynamodb_state.backups.expanded_item = None;
4729 Ok(())
4730 }
4731
4732 pub async fn load_dynamodb_items(&mut self) -> anyhow::Result<()> {
4734 let Some(request) = ddb_items::context(self) else {
4735 self.dynamodb_state.clear_items_result();
4736 return Ok(());
4737 };
4738 let cursor = self.dynamodb_state.begin_items_request(request.clone());
4739 let projection = match request.projection {
4740 ItemProjection::AllAttributes => None,
4741 ItemProjection::KeysOnly => Some(request.keys.as_slice()),
4742 };
4743 let result = match request.operation {
4744 ItemOperation::Scan => {
4745 self.dynamodb_client
4746 .scan_items(
4747 &request.table,
4748 request.index.as_deref(),
4749 ITEM_LIMIT,
4750 projection,
4751 cursor,
4752 )
4753 .await
4754 }
4755 ItemOperation::Query => {
4756 let value = request.query.as_deref().unwrap_or_default();
4757 if value.is_empty() {
4758 Ok(DdbItemsPage::default())
4759 } else if let Some(pk) = request.keys.first() {
4760 self.dynamodb_client
4761 .query_items(
4762 &request.table,
4763 request.index.as_deref(),
4764 pk,
4765 value,
4766 &request.partition_type,
4767 ITEM_LIMIT,
4768 projection,
4769 cursor,
4770 )
4771 .await
4772 } else {
4773 Err(anyhow::anyhow!(
4774 "The selected table or index has no partition key"
4775 ))
4776 }
4777 }
4778 };
4779 match result {
4780 Ok(page) => {
4781 self.dynamodb_state.finish_items_request(page);
4782 Ok(())
4783 }
4784 Err(error) => {
4785 self.dynamodb_state.fail_items_request();
4786 Err(error)
4787 }
4788 }
4789 }
4790
4791 pub async fn load_dynamodb_tags(&mut self, table_arn: &str) -> anyhow::Result<()> {
4792 let tags = self.dynamodb_client.list_tags(table_arn).await?;
4793 self.dynamodb_state.tags_table.items = tags;
4794 self.dynamodb_state.tags_table.selected = 0;
4795 self.dynamodb_state.tags_table.expanded_item = None;
4796 Ok(())
4797 }
4798
4799 pub async fn load_dynamodb_metrics(&mut self, table_name: &str) -> anyhow::Result<()> {
4801 self.dynamodb_state.metrics = self
4802 .dynamodb_client
4803 .get_monitoring_metrics(table_name)
4804 .await?;
4805 Ok(())
4806 }
4807
4808 pub async fn load_dynamodb_permissions(
4810 &mut self,
4811 table_arn: &str,
4812 stream_arn: &str,
4813 ) -> anyhow::Result<()> {
4814 self.dynamodb_state.table_policy =
4815 self.dynamodb_client.get_resource_policy(table_arn).await?;
4816 self.dynamodb_state.stream_policy = if stream_arn.is_empty() {
4817 None
4818 } else {
4819 self.dynamodb_client.get_resource_policy(stream_arn).await?
4820 };
4821 Ok(())
4822 }
4823
4824 pub async fn load_dynamodb_settings(&mut self, table_name: &str) -> anyhow::Result<()> {
4825 self.dynamodb_state.settings = self.dynamodb_client.describe_settings(table_name).await?;
4826 Ok(())
4827 }
4828
4829 pub async fn load_vpcs(&mut self) -> anyhow::Result<()> {
4830 self.vpc_state.vpcs.loading = true;
4831 let result = self.vpc_client.list_vpcs_with_properties().await;
4832 self.vpc_state.vpcs.loading = false;
4833 self.vpc_state.vpcs.items = result?;
4835 self.vpc_state.vpcs.reset();
4836 Ok(())
4837 }
4838
4839 pub async fn load_rds_instances(&mut self) -> anyhow::Result<()> {
4840 self.rds_state.instances.loading = true;
4841 let options =
4842 crate::rds::instance::Column::inventory_options(&self.rds_state.visible_column_ids);
4843 let result = self.rds_client.list_instances_with_options(options).await;
4844 self.rds_state.instances.loading = false;
4845 let instances = result?;
4847 self.rds_state.instances.items = instances;
4848 self.rds_state.instances.reset();
4849 Ok(())
4850 }
4851
4852 pub async fn load_fsx_file_systems(&mut self) -> anyhow::Result<()> {
4853 let file_systems = self.fsx_client.list_file_systems().await?;
4854 self.fsx_state.file_systems.items = file_systems;
4855 Ok(())
4856 }
4857
4858 pub async fn load_fsx_backups(&mut self, file_system_id: &str) -> anyhow::Result<()> {
4859 let backups = self.fsx_client.describe_backups(file_system_id).await?;
4860 self.fsx_state.backups.items = backups;
4861 self.fsx_state.backups.selected = 0;
4862 self.fsx_state.backups.expanded_item = None;
4863 Ok(())
4864 }
4865
4866 pub async fn load_apis(&mut self) -> anyhow::Result<()> {
4867 let apis = self.apig_client.list_rest_apis().await?;
4868
4869 self.apig_state.apis.items = apis
4870 .into_iter()
4871 .map(|a| crate::apig::api::RestApi {
4872 id: a.id,
4873 name: a.name,
4874 description: a.description,
4875 created_date: a.created_date,
4876 api_key_source: a.api_key_source,
4877 endpoint_configuration: a.endpoint_configuration,
4878 protocol_type: a.protocol_type,
4879 disable_execute_api_endpoint: a.disable_execute_api_endpoint,
4880 status: a.status,
4881 })
4882 .collect();
4883
4884 self.apig_state
4885 .apis
4886 .items
4887 .sort_by(|a, b| a.name.cmp(&b.name));
4888 Ok(())
4889 }
4890
4891 pub async fn load_ec2_instances(&mut self) -> anyhow::Result<()> {
4892 let instances = self.ec2_client.list_instances().await?;
4893
4894 self.ec2_state.table.items = instances
4895 .into_iter()
4896 .map(|i| Ec2Instance {
4897 instance_id: i.instance_id,
4898 name: i.name,
4899 state: i.state,
4900 instance_type: i.instance_type,
4901 availability_zone: i.availability_zone,
4902 public_ipv4_dns: i.public_ipv4_dns,
4903 public_ipv4_address: i.public_ipv4_address,
4904 elastic_ip: i.elastic_ip,
4905 ipv6_ips: i.ipv6_ips,
4906 monitoring: i.monitoring,
4907 security_groups: i.security_groups,
4908 key_name: i.key_name,
4909 launch_time: i.launch_time,
4910 platform_details: i.platform_details,
4911 status_checks: i.status_checks,
4912 alarm_status: i.alarm_status,
4913 private_dns_name: i.private_dns_name,
4914 private_ip_address: i.private_ip_address,
4915 security_group_ids: String::new(),
4916 owner_id: String::new(),
4917 volume_id: String::new(),
4918 root_device_name: String::new(),
4919 root_device_type: String::new(),
4920 ebs_optimized: String::new(),
4921 image_id: String::new(),
4922 kernel_id: String::new(),
4923 ramdisk_id: String::new(),
4924 ami_launch_index: String::new(),
4925 reservation_id: String::new(),
4926 vpc_id: String::new(),
4927 subnet_ids: String::new(),
4928 instance_lifecycle: String::new(),
4929 architecture: String::new(),
4930 virtualization_type: String::new(),
4931 platform: String::new(),
4932 iam_instance_profile_arn: String::new(),
4933 tenancy: String::new(),
4934 affinity: String::new(),
4935 host_id: String::new(),
4936 placement_group: String::new(),
4937 partition_number: String::new(),
4938 capacity_reservation_id: String::new(),
4939 state_transition_reason_code: String::new(),
4940 state_transition_reason_message: String::new(),
4941 stop_hibernation_behavior: String::new(),
4942 outpost_arn: String::new(),
4943 product_codes: String::new(),
4944 availability_zone_id: String::new(),
4945 imdsv2: String::new(),
4946 usage_operation: String::new(),
4947 managed: String::new(),
4948 operator: String::new(),
4949 })
4950 .collect();
4951
4952 self.ec2_state
4954 .table
4955 .items
4956 .sort_by(|a, b| b.launch_time.cmp(&a.launch_time));
4957 Ok(())
4958 }
4959
4960 pub async fn load_ecr_images(&mut self) -> anyhow::Result<()> {
4961 if let Some(repo_name) = &self.ecr_state.current_repository {
4962 if let Some(repo_uri) = &self.ecr_state.current_repository_uri {
4963 let images = self.ecr_client.list_images(repo_name, repo_uri).await?;
4964
4965 self.ecr_state.images.items = images
4966 .into_iter()
4967 .map(|i| EcrImage {
4968 tag: i.tag,
4969 artifact_type: i.artifact_type,
4970 pushed_at: i.pushed_at,
4971 size_bytes: i.size_bytes,
4972 uri: i.uri,
4973 digest: i.digest,
4974 last_pull_time: i.last_pull_time,
4975 })
4976 .collect();
4977
4978 self.ecr_state
4979 .images
4980 .items
4981 .sort_by(|a, b| b.pushed_at.cmp(&a.pushed_at));
4982 }
4983 }
4984 Ok(())
4985 }
4986
4987 pub async fn load_cloudformation_stacks(&mut self) -> anyhow::Result<()> {
4988 let stacks = self
4989 .cloudformation_client
4990 .list_stacks(self.cfn_state.view_nested)
4991 .await?;
4992
4993 let mut stacks: Vec<CfnStack> = stacks
4994 .into_iter()
4995 .map(|s| CfnStack {
4996 name: s.name,
4997 stack_id: s.stack_id,
4998 status: s.status,
4999 created_time: s.created_time,
5000 updated_time: s.updated_time,
5001 deleted_time: s.deleted_time,
5002 drift_status: s.drift_status,
5003 last_drift_check_time: s.last_drift_check_time,
5004 status_reason: s.status_reason,
5005 description: s.description,
5006 detailed_status: String::new(),
5007 root_stack: s.root_stack,
5008 parent_stack: s.parent_stack,
5009 termination_protection: false,
5010 iam_role: String::new(),
5011 tags: Vec::new(),
5012 stack_policy: String::new(),
5013 rollback_monitoring_time: String::new(),
5014 rollback_alarms: Vec::new(),
5015 notification_arns: Vec::new(),
5016 })
5017 .collect();
5018
5019 stacks.sort_by(|a, b| b.created_time.cmp(&a.created_time));
5021
5022 self.cfn_state.table.items = stacks;
5023
5024 Ok(())
5025 }
5026
5027 pub async fn load_cfn_template(&mut self, stack_name: &str) -> anyhow::Result<()> {
5028 let template = self.cloudformation_client.get_template(stack_name).await?;
5029 self.cfn_state.template_body = template;
5030 self.cfn_state.template_scroll = 0;
5031 Ok(())
5032 }
5033
5034 pub async fn load_cfn_parameters(&mut self, stack_name: &str) -> anyhow::Result<()> {
5035 let mut parameters = self
5036 .cloudformation_client
5037 .get_stack_parameters(stack_name)
5038 .await?;
5039 parameters.sort_by(|a, b| a.key.cmp(&b.key));
5040 self.cfn_state.parameters.items = parameters;
5041 self.cfn_state.parameters.reset();
5042 Ok(())
5043 }
5044
5045 pub async fn load_cfn_outputs(&mut self, stack_name: &str) -> anyhow::Result<()> {
5046 let outputs = self
5047 .cloudformation_client
5048 .get_stack_outputs(stack_name)
5049 .await?;
5050 self.cfn_state.outputs.items = outputs;
5051 self.cfn_state.outputs.reset();
5052 Ok(())
5053 }
5054
5055 pub async fn load_cfn_resources(&mut self, stack_name: &str) -> anyhow::Result<()> {
5056 let resources = self
5057 .cloudformation_client
5058 .get_stack_resources(stack_name)
5059 .await?;
5060 self.cfn_state.resources.items = resources;
5061 self.cfn_state.resources.reset();
5062 Ok(())
5063 }
5064
5065 pub async fn load_cfn_events(&mut self, stack_name: &str) -> anyhow::Result<()> {
5066 let events = self
5067 .cloudformation_client
5068 .list_stack_events(stack_name)
5069 .await?;
5070 self.cfn_state.events.items = events;
5071 self.cfn_state.events.reset();
5072 Ok(())
5073 }
5074
5075 pub async fn load_cfn_change_sets(&mut self, stack_name: &str) -> anyhow::Result<()> {
5076 let change_sets = self
5077 .cloudformation_client
5078 .list_change_sets(stack_name)
5079 .await?;
5080 self.cfn_state.change_sets.items = change_sets;
5081 self.cfn_state.change_sets.reset();
5082 Ok(())
5083 }
5084
5085 pub async fn load_role_policies(&mut self, role_name: &str) -> anyhow::Result<()> {
5086 let attached_policies = self
5088 .iam_client
5089 .list_attached_role_policies(role_name)
5090 .await
5091 .map_err(|e| anyhow::anyhow!(e))?;
5092
5093 let mut policies: Vec<IamPolicy> = Vec::new();
5094 for p in attached_policies {
5095 let (creation_time, edited_time) = if let Some(arn) = p.policy_arn() {
5096 self.iam_client
5097 .get_policy_metadata(arn)
5098 .await
5099 .unwrap_or_else(|_| ("-".to_string(), "-".to_string()))
5100 } else {
5101 ("-".to_string(), "-".to_string())
5102 };
5103 policies.push(IamPolicy {
5104 policy_name: p.policy_name().unwrap_or("").to_string(),
5105 policy_type: "Managed".to_string(),
5106 attached_via: "Direct".to_string(),
5107 attached_entities: "-".to_string(),
5108 description: "-".to_string(),
5109 creation_time,
5110 edited_time,
5111 policy_arn: p.policy_arn().map(|s| s.to_string()),
5112 });
5113 }
5114
5115 let inline_policy_names = self
5117 .iam_client
5118 .list_role_policies(role_name)
5119 .await
5120 .map_err(|e| anyhow::anyhow!(e))?;
5121
5122 for policy_name in inline_policy_names {
5123 policies.push(IamPolicy {
5124 policy_name,
5125 policy_type: "Inline".to_string(),
5126 attached_via: "Direct".to_string(),
5127 attached_entities: "-".to_string(),
5128 description: "-".to_string(),
5129 creation_time: "-".to_string(),
5130 edited_time: "-".to_string(),
5131 policy_arn: None,
5132 });
5133 }
5134
5135 self.iam_state.policies.items = policies;
5136
5137 Ok(())
5138 }
5139
5140 pub async fn load_group_policies(&mut self, group_name: &str) -> anyhow::Result<()> {
5141 let attached_policies = self
5142 .iam_client
5143 .list_attached_group_policies(group_name)
5144 .await
5145 .map_err(|e| anyhow::anyhow!(e))?;
5146
5147 let mut policies: Vec<IamPolicy> = attached_policies
5148 .into_iter()
5149 .map(|p| IamPolicy {
5150 policy_name: p.policy_name().unwrap_or("").to_string(),
5151 policy_type: "AWS managed".to_string(),
5152 attached_via: "Direct".to_string(),
5153 attached_entities: "-".to_string(),
5154 description: "-".to_string(),
5155 creation_time: "-".to_string(),
5156 edited_time: "-".to_string(),
5157 policy_arn: p.policy_arn().map(|s| s.to_string()),
5158 })
5159 .collect();
5160
5161 let inline_policy_names = self
5162 .iam_client
5163 .list_group_policies(group_name)
5164 .await
5165 .map_err(|e| anyhow::anyhow!(e))?;
5166
5167 for policy_name in inline_policy_names {
5168 policies.push(IamPolicy {
5169 policy_name,
5170 policy_type: "Inline".to_string(),
5171 attached_via: "Direct".to_string(),
5172 attached_entities: "-".to_string(),
5173 description: "-".to_string(),
5174 creation_time: "-".to_string(),
5175 edited_time: "-".to_string(),
5176 policy_arn: None,
5177 });
5178 }
5179
5180 self.iam_state.policies.items = policies;
5181
5182 Ok(())
5183 }
5184
5185 pub async fn load_group_users(&mut self, group_name: &str) -> anyhow::Result<()> {
5186 let users = self
5187 .iam_client
5188 .get_group_users(group_name)
5189 .await
5190 .map_err(|e| anyhow::anyhow!(e))?;
5191
5192 let group_users: Vec<IamGroupUser> = users
5193 .into_iter()
5194 .map(|u| {
5195 let creation_time = {
5196 let dt = u.create_date();
5197 let timestamp = dt.secs();
5198 let datetime =
5199 chrono::DateTime::from_timestamp(timestamp, 0).unwrap_or_default();
5200 datetime.format("%Y-%m-%d %H:%M:%S (UTC)").to_string()
5201 };
5202
5203 IamGroupUser {
5204 user_name: u.user_name().to_string(),
5205 groups: String::new(),
5206 last_activity: String::new(),
5207 creation_time,
5208 }
5209 })
5210 .collect();
5211
5212 self.iam_state.group_users.items = group_users;
5213
5214 Ok(())
5215 }
5216
5217 pub async fn load_policy_document(
5218 &mut self,
5219 role_name: &str,
5220 policy_name: &str,
5221 ) -> anyhow::Result<()> {
5222 let policy = self
5224 .iam_state
5225 .policies
5226 .items
5227 .iter()
5228 .find(|p| p.policy_name == policy_name)
5229 .ok_or_else(|| anyhow::anyhow!("Policy not found"))?;
5230
5231 let document = if let Some(policy_arn) = &policy.policy_arn {
5232 self.iam_client
5234 .get_policy_version(policy_arn)
5235 .await
5236 .map_err(|e| anyhow::anyhow!(e))?
5237 } else {
5238 self.iam_client
5240 .get_role_policy(role_name, policy_name)
5241 .await
5242 .map_err(|e| anyhow::anyhow!(e))?
5243 };
5244
5245 self.iam_state.policy_document = document;
5246
5247 Ok(())
5248 }
5249
5250 pub async fn load_trust_policy(&mut self, role_name: &str) -> anyhow::Result<()> {
5251 let document = self
5252 .iam_client
5253 .get_role(role_name)
5254 .await
5255 .map_err(|e| anyhow::anyhow!(e))?;
5256
5257 self.iam_state.trust_policy_document = document;
5258
5259 Ok(())
5260 }
5261
5262 pub async fn load_last_accessed_services(&mut self, _role_name: &str) -> anyhow::Result<()> {
5263 self.iam_state.last_accessed_services.items = vec![];
5265 self.iam_state.last_accessed_services.selected = 0;
5266
5267 Ok(())
5268 }
5269
5270 pub async fn load_role_tags(&mut self, role_name: &str) -> anyhow::Result<()> {
5271 let tags = self
5272 .iam_client
5273 .list_role_tags(role_name)
5274 .await
5275 .map_err(|e| anyhow::anyhow!(e))?;
5276 self.iam_state.tags.items = tags
5277 .into_iter()
5278 .map(|(k, v)| IamRoleTag { key: k, value: v })
5279 .collect();
5280 self.iam_state.tags.reset();
5281 Ok(())
5282 }
5283
5284 pub async fn load_user_tags(&mut self, user_name: &str) -> anyhow::Result<()> {
5285 let tags = self
5286 .iam_client
5287 .list_user_tags(user_name)
5288 .await
5289 .map_err(|e| anyhow::anyhow!(e))?;
5290 self.iam_state.user_tags.items = tags
5291 .into_iter()
5292 .map(|(k, v)| IamUserTag { key: k, value: v })
5293 .collect();
5294 self.iam_state.user_tags.reset();
5295 Ok(())
5296 }
5297
5298 pub async fn load_log_streams(&mut self) -> anyhow::Result<()> {
5299 if let Some(group) = self
5300 .log_groups_state
5301 .log_groups
5302 .items
5303 .get(self.log_groups_state.log_groups.selected)
5304 {
5305 self.log_groups_state.log_streams =
5306 self.cloudwatch_client.list_log_streams(&group.name).await?;
5307 self.log_groups_state.selected_stream = 0;
5308 }
5309 Ok(())
5310 }
5311
5312 pub async fn load_log_group_tags(&mut self) -> anyhow::Result<()> {
5313 if let Some(group) = self
5314 .log_groups_state
5315 .log_groups
5316 .items
5317 .get(self.log_groups_state.log_groups.selected)
5318 {
5319 let arn = if let Some(arn) = &group.log_group_arn {
5321 arn.clone()
5322 } else if let Some(arn) = &group.arn {
5323 arn.clone()
5324 } else {
5325 let account_id = if self.config.account_id.is_empty() {
5327 "*"
5328 } else {
5329 &self.config.account_id
5330 };
5331 format!(
5332 "arn:aws:logs:{}:{}:log-group:{}",
5333 self.config.region, account_id, group.name
5334 )
5335 };
5336
5337 let tags = self.cloudwatch_client.list_tags_for_log_group(&arn).await?;
5338 self.log_groups_state.tags.items = tags;
5339 self.log_groups_state.tags.selected = 0;
5340 self.log_groups_state.tags.scroll_offset = 0;
5341 }
5342 Ok(())
5343 }
5344
5345 pub async fn load_log_events(&mut self) -> anyhow::Result<()> {
5346 if let Some(group) = self
5347 .log_groups_state
5348 .log_groups
5349 .items
5350 .get(self.log_groups_state.log_groups.selected)
5351 {
5352 if let Some(stream) = self
5353 .log_groups_state
5354 .log_streams
5355 .get(self.log_groups_state.selected_stream)
5356 {
5357 let (start_time, end_time) =
5359 if let Ok(amount) = self.log_groups_state.relative_amount.parse::<i64>() {
5360 let now = chrono::Utc::now().timestamp_millis();
5361 let duration_ms = match self.log_groups_state.relative_unit {
5362 TimeUnit::Minutes => amount * 60 * 1000,
5363 TimeUnit::Hours => amount * 60 * 60 * 1000,
5364 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
5365 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
5366 };
5367 (Some(now - duration_ms), Some(now))
5368 } else {
5369 (None, None)
5370 };
5371
5372 let (mut events, has_more, token) = self
5373 .cloudwatch_client
5374 .get_log_events(
5375 &group.name,
5376 &stream.name,
5377 self.log_groups_state.next_backward_token.clone(),
5378 start_time,
5379 end_time,
5380 )
5381 .await?;
5382
5383 if self.log_groups_state.next_backward_token.is_some() {
5384 events.append(&mut self.log_groups_state.log_events);
5386 self.log_groups_state.event_scroll_offset = 0;
5387 } else {
5388 self.log_groups_state.event_scroll_offset = 0;
5390 }
5391
5392 self.log_groups_state.log_events = events;
5393 self.log_groups_state.has_older_events =
5394 has_more && self.log_groups_state.log_events.len() >= 25;
5395 self.log_groups_state.next_backward_token = token;
5396 self.log_groups_state.selected_event = 0;
5397 }
5398 }
5399 Ok(())
5400 }
5401
5402 pub async fn execute_insights_query(&mut self) -> anyhow::Result<()> {
5403 if self.insights_state.insights.selected_log_groups.is_empty() {
5404 return Err(anyhow::anyhow!(
5405 "No log groups selected. Please select at least one log group."
5406 ));
5407 }
5408
5409 let now = chrono::Utc::now().timestamp_millis();
5410 let amount = self
5411 .insights_state
5412 .insights
5413 .insights_relative_amount
5414 .parse::<i64>()
5415 .unwrap_or(1);
5416 let duration_ms = match self.insights_state.insights.insights_relative_unit {
5417 TimeUnit::Minutes => amount * 60 * 1000,
5418 TimeUnit::Hours => amount * 60 * 60 * 1000,
5419 TimeUnit::Days => amount * 24 * 60 * 60 * 1000,
5420 TimeUnit::Weeks => amount * 7 * 24 * 60 * 60 * 1000,
5421 };
5422 let start_time = now - duration_ms;
5423
5424 let query_id = self
5425 .cloudwatch_client
5426 .start_query(
5427 self.insights_state.insights.selected_log_groups.clone(),
5428 self.insights_state.insights.query_text.trim().to_string(),
5429 start_time,
5430 now,
5431 )
5432 .await?;
5433
5434 for _ in 0..60 {
5436 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
5437 let (status, results) = self.cloudwatch_client.get_query_results(&query_id).await?;
5438
5439 if status == "Complete" {
5440 self.insights_state.insights.query_results = results;
5441 self.insights_state.insights.query_completed = true;
5442 self.insights_state.insights.results_selected = 0;
5443 self.insights_state.insights.expanded_result = None;
5444 self.view_mode = ViewMode::InsightsResults;
5445 return Ok(());
5446 } else if status == "Failed" || status == "Cancelled" {
5447 return Err(anyhow::anyhow!("Query {}", status.to_lowercase()));
5448 }
5449 }
5450
5451 Err(anyhow::anyhow!("Query timeout"))
5452 }
5453}
5454
5455impl CloudWatchInsightsState {
5456 fn new() -> Self {
5457 Self {
5458 insights: InsightsState::default(),
5459 loading: false,
5460 }
5461 }
5462}
5463
5464impl CloudWatchAlarmsState {
5465 fn new() -> Self {
5466 Self {
5467 table: TableState::new(),
5468 current_alarm: None,
5469 alarm_tab: AlarmTab::AllAlarms,
5470 detail_tab: AlarmDetailTab::Details,
5471 view_as: AlarmViewMode::Table,
5472 wrap_lines: false,
5473 sort_column: "Last state update".to_string(),
5474 sort_direction: SortDirection::Asc,
5475 input_focus: InputFocus::Filter,
5476 metric_data: Vec::new(),
5477 metrics_loading: false,
5478 }
5479 }
5480}
5481
5482impl ServicePickerState {
5483 fn new() -> Self {
5484 Self {
5485 filter: String::new(),
5486 filter_active: false,
5487 selected: 0,
5488 services: vec![
5489 "API Gateway › APIs",
5490 "CloudWatch › Log Groups",
5491 "CloudWatch › Logs Insights",
5492 "CloudWatch › Alarms",
5493 "CloudTrail › Event History",
5494 "CloudFormation › Stacks",
5495 "EC2 › Instances",
5496 "ECR › Repositories",
5497 "IAM › Users",
5498 "IAM › Roles",
5499 "IAM › User Groups",
5500 "KMS › Managed Keys",
5501 "EFS › File Systems",
5502 "FSx › File Systems",
5503 "DynamoDB › Tables",
5504 "Lambda › Functions",
5505 "Lambda › Applications",
5506 "RDS › Databases",
5507 "S3 › Buckets",
5508 "SQS › Queues",
5509 "VPC › VPCs",
5510 ],
5511 }
5512 }
5513}
5514
5515#[cfg(test)]
5516mod test_helpers {
5517 use super::*;
5518
5519 pub fn test_app() -> App {
5521 App::new_without_client("test".to_string(), Some("us-east-1".to_string()))
5522 }
5523
5524 pub fn test_app_no_region() -> App {
5525 App::new_without_client("test".to_string(), None)
5526 }
5527}
5528
5529#[cfg(test)]
5530mod tests {
5531 use super::*;
5532 use crate::keymap::Action;
5533 use test_helpers::*;
5534
5535 #[test]
5536 fn test_next_tab_cycles_forward() {
5537 let mut app = test_app();
5538 app.tabs = vec![
5539 Tab {
5540 service: Service::CloudWatchLogGroups,
5541 title: "CloudWatch › Log Groups".to_string(),
5542 breadcrumb: "CloudWatch › Log Groups".to_string(),
5543 },
5544 Tab {
5545 service: Service::CloudWatchInsights,
5546 title: "CloudWatch › Logs Insights".to_string(),
5547 breadcrumb: "CloudWatch › Logs Insights".to_string(),
5548 },
5549 Tab {
5550 service: Service::CloudWatchAlarms,
5551 title: "CloudWatch › Alarms".to_string(),
5552 breadcrumb: "CloudWatch › Alarms".to_string(),
5553 },
5554 ];
5555 app.current_tab = 0;
5556
5557 app.handle_action(Action::NextTab);
5558 assert_eq!(app.current_tab, 1);
5559 assert_eq!(app.current_service, Service::CloudWatchInsights);
5560
5561 app.handle_action(Action::NextTab);
5562 assert_eq!(app.current_tab, 2);
5563 assert_eq!(app.current_service, Service::CloudWatchAlarms);
5564
5565 app.handle_action(Action::NextTab);
5567 assert_eq!(app.current_tab, 0);
5568 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
5569 }
5570
5571 #[test]
5572 fn test_prev_tab_cycles_backward() {
5573 let mut app = test_app();
5574 app.tabs = vec![
5575 Tab {
5576 service: Service::CloudWatchLogGroups,
5577 title: "CloudWatch › Log Groups".to_string(),
5578 breadcrumb: "CloudWatch › Log Groups".to_string(),
5579 },
5580 Tab {
5581 service: Service::CloudWatchInsights,
5582 title: "CloudWatch › Logs Insights".to_string(),
5583 breadcrumb: "CloudWatch › Logs Insights".to_string(),
5584 },
5585 Tab {
5586 service: Service::CloudWatchAlarms,
5587 title: "CloudWatch › Alarms".to_string(),
5588 breadcrumb: "CloudWatch › Alarms".to_string(),
5589 },
5590 ];
5591 app.current_tab = 2;
5592
5593 app.handle_action(Action::PrevTab);
5594 assert_eq!(app.current_tab, 1);
5595 assert_eq!(app.current_service, Service::CloudWatchInsights);
5596
5597 app.handle_action(Action::PrevTab);
5598 assert_eq!(app.current_tab, 0);
5599 assert_eq!(app.current_service, Service::CloudWatchLogGroups);
5600
5601 app.handle_action(Action::PrevTab);
5603 assert_eq!(app.current_tab, 2);
5604 assert_eq!(app.current_service, Service::CloudWatchAlarms);
5605 }
5606
5607 #[test]
5608 fn test_close_tab_removes_current() {
5609 let mut app = test_app();
5610 app.tabs = vec![
5611 Tab {
5612 service: Service::CloudWatchLogGroups,
5613 title: "CloudWatch › Log Groups".to_string(),
5614 breadcrumb: "CloudWatch › Log Groups".to_string(),
5615 },
5616 Tab {
5617 service: Service::CloudWatchInsights,
5618 title: "CloudWatch › Logs Insights".to_string(),
5619 breadcrumb: "CloudWatch › Logs Insights".to_string(),
5620 },
5621 Tab {
5622 service: Service::CloudWatchAlarms,
5623 title: "CloudWatch › Alarms".to_string(),
5624 breadcrumb: "CloudWatch › Alarms".to_string(),
5625 },
5626 ];
5627 app.current_tab = 1;
5628 app.service_selected = true;
5629
5630 app.handle_action(Action::CloseTab);
5631 assert_eq!(app.tabs.len(), 2);
5632 assert_eq!(app.current_tab, 1);
5633 assert_eq!(app.current_service, Service::CloudWatchAlarms);
5634 }
5635
5636 #[test]
5637 fn test_close_last_tab_exits_service() {
5638 let mut app = test_app();
5639 app.tabs = vec![Tab {
5640 service: Service::CloudWatchLogGroups,
5641 title: "CloudWatch › Log Groups".to_string(),
5642 breadcrumb: "CloudWatch › Log Groups".to_string(),
5643 }];
5644 app.current_tab = 0;
5645 app.service_selected = true;
5646
5647 app.handle_action(Action::CloseTab);
5648 assert_eq!(app.tabs.len(), 0);
5649 assert!(!app.service_selected);
5650 assert_eq!(app.current_tab, 0);
5651 }
5652
5653 #[test]
5654 fn test_close_service_removes_current_tab() {
5655 let mut app = test_app();
5656 app.tabs = vec![
5657 Tab {
5658 service: Service::CloudWatchLogGroups,
5659 title: "CloudWatch › Log Groups".to_string(),
5660 breadcrumb: "CloudWatch › Log Groups".to_string(),
5661 },
5662 Tab {
5663 service: Service::CloudWatchInsights,
5664 title: "CloudWatch › Logs Insights".to_string(),
5665 breadcrumb: "CloudWatch › Logs Insights".to_string(),
5666 },
5667 Tab {
5668 service: Service::CloudWatchAlarms,
5669 title: "CloudWatch › Alarms".to_string(),
5670 breadcrumb: "CloudWatch › Alarms".to_string(),
5671 },
5672 ];
5673 app.current_tab = 1;
5674 app.service_selected = true;
5675
5676 app.handle_action(Action::CloseService);
5677
5678 assert_eq!(app.tabs.len(), 2);
5680 assert_eq!(app.current_tab, 1);
5682 assert_eq!(app.current_service, Service::CloudWatchAlarms);
5683 assert!(app.service_selected);
5685 assert_eq!(app.mode, Mode::Normal);
5686 }
5687
5688 #[test]
5689 fn test_close_service_last_tab_shows_picker() {
5690 let mut app = test_app();
5691 app.tabs = vec![Tab {
5692 service: Service::CloudWatchLogGroups,
5693 title: "CloudWatch › Log Groups".to_string(),
5694 breadcrumb: "CloudWatch › Log Groups".to_string(),
5695 }];
5696 app.current_tab = 0;
5697 app.service_selected = true;
5698
5699 app.handle_action(Action::CloseService);
5700
5701 assert_eq!(app.tabs.len(), 0);
5703 assert!(!app.service_selected);
5705 assert_eq!(app.mode, Mode::ServicePicker);
5706 }
5707
5708 #[test]
5709 fn test_open_tab_picker_with_tabs() {
5710 let mut app = test_app();
5711 app.tabs = vec![
5712 Tab {
5713 service: Service::CloudWatchLogGroups,
5714 title: "CloudWatch › Log Groups".to_string(),
5715 breadcrumb: "CloudWatch › Log Groups".to_string(),
5716 },
5717 Tab {
5718 service: Service::CloudWatchInsights,
5719 title: "CloudWatch › Logs Insights".to_string(),
5720 breadcrumb: "CloudWatch › Logs Insights".to_string(),
5721 },
5722 ];
5723 app.current_tab = 1;
5724
5725 app.handle_action(Action::OpenTabPicker);
5726 assert_eq!(app.mode, Mode::TabPicker);
5727 assert_eq!(app.tab_picker_selected, 1);
5728 }
5729
5730 #[test]
5731 fn test_open_tab_picker_without_tabs() {
5732 let mut app = test_app();
5733 app.tabs = vec![];
5734
5735 app.handle_action(Action::OpenTabPicker);
5736 assert_eq!(app.mode, Mode::Normal);
5737 }
5738
5739 #[test]
5740 fn test_pending_key_state() {
5741 let mut app = test_app();
5742 assert_eq!(app.pending_key, None);
5743
5744 app.pending_key = Some('g');
5745 assert_eq!(app.pending_key, Some('g'));
5746 }
5747
5748 #[test]
5749 fn test_tab_breadcrumb_updates() {
5750 let mut app = test_app();
5751 app.tabs = vec![Tab {
5752 service: Service::CloudWatchLogGroups,
5753 title: "CloudWatch › Log Groups".to_string(),
5754 breadcrumb: "CloudWatch > Log groups".to_string(),
5755 }];
5756 app.current_tab = 0;
5757 app.service_selected = true;
5758 app.current_service = Service::CloudWatchLogGroups;
5759
5760 assert_eq!(app.tabs[0].breadcrumb, "CloudWatch > Log groups");
5762
5763 app.log_groups_state
5765 .log_groups
5766 .items
5767 .push(rusticity_core::LogGroup {
5768 name: "/aws/lambda/test".to_string(),
5769 creation_time: None,
5770 stored_bytes: Some(1024),
5771 retention_days: None,
5772 log_class: None,
5773 arn: None,
5774 log_group_arn: None,
5775 deletion_protection_enabled: None,
5776 });
5777 app.log_groups_state.log_groups.reset();
5778 app.view_mode = ViewMode::Detail;
5779 app.update_current_tab_breadcrumb();
5780
5781 assert_eq!(
5783 app.tabs[0].breadcrumb,
5784 "CloudWatch > Log groups > /aws/lambda/test"
5785 );
5786 }
5787
5788 #[test]
5789 fn test_s3_bucket_column_selector_navigation() {
5790 let mut app = test_app();
5791 app.current_service = Service::S3Buckets;
5792 app.mode = Mode::ColumnSelector;
5793 app.column_selector_index = 0;
5794
5795 app.handle_action(Action::NextItem);
5797 assert_eq!(app.column_selector_index, 1);
5798
5799 app.handle_action(Action::NextItem);
5800 assert_eq!(app.column_selector_index, 2);
5801
5802 app.handle_action(Action::NextItem);
5803 assert_eq!(app.column_selector_index, 3);
5804
5805 for _ in 0..10 {
5807 app.handle_action(Action::NextItem);
5808 }
5809 assert_eq!(app.column_selector_index, 9);
5810
5811 app.handle_action(Action::PrevItem);
5813 assert_eq!(app.column_selector_index, 8);
5814
5815 app.handle_action(Action::PrevItem);
5816 assert_eq!(app.column_selector_index, 7);
5817
5818 for _ in 0..10 {
5820 app.handle_action(Action::PrevItem);
5821 }
5822 assert_eq!(app.column_selector_index, 0);
5823 }
5824
5825 #[test]
5826 fn test_cloudwatch_alarms_state_initialized() {
5827 let app = test_app();
5828
5829 assert_eq!(app.alarms_state.table.items.len(), 0);
5831 assert_eq!(app.alarms_state.table.selected, 0);
5832 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
5833 assert!(!app.alarms_state.table.loading);
5834 assert_eq!(app.alarms_state.view_as, AlarmViewMode::Table);
5835 assert_eq!(app.alarms_state.table.page_size, PageSize::Fifty);
5836 }
5837
5838 #[test]
5839 fn test_cloudwatch_alarms_service_selection() {
5840 let mut app = test_app();
5841
5842 app.current_service = Service::CloudWatchAlarms;
5844 app.service_selected = true;
5845
5846 assert_eq!(app.current_service, Service::CloudWatchAlarms);
5847 assert!(app.service_selected);
5848 }
5849
5850 #[test]
5851 fn test_cloudwatch_alarms_column_preferences() {
5852 let app = test_app();
5853
5854 assert!(!app.cw_alarm_column_ids.is_empty());
5856 assert!(!app.cw_alarm_visible_column_ids.is_empty());
5857
5858 assert!(app
5860 .cw_alarm_visible_column_ids
5861 .contains(&AlarmColumn::Name.id()));
5862 assert!(app
5863 .cw_alarm_visible_column_ids
5864 .contains(&AlarmColumn::State.id()));
5865 }
5866
5867 #[test]
5868 fn test_s3_bucket_navigation_without_expansion() {
5869 let mut app = test_app();
5870 app.current_service = Service::S3Buckets;
5871 app.service_selected = true;
5872 app.mode = Mode::Normal;
5873
5874 app.s3_state.buckets.items = vec![
5876 S3Bucket {
5877 name: "bucket1".to_string(),
5878 region: "us-east-1".to_string(),
5879 creation_date: "2024-01-01T00:00:00Z".to_string(),
5880 },
5881 S3Bucket {
5882 name: "bucket2".to_string(),
5883 region: "us-east-1".to_string(),
5884 creation_date: "2024-01-02T00:00:00Z".to_string(),
5885 },
5886 S3Bucket {
5887 name: "bucket3".to_string(),
5888 region: "us-east-1".to_string(),
5889 creation_date: "2024-01-03T00:00:00Z".to_string(),
5890 },
5891 ];
5892 app.s3_state.selected_row = 0;
5893
5894 app.handle_action(Action::NextItem);
5896 assert_eq!(app.s3_state.selected_row, 1);
5897
5898 app.handle_action(Action::NextItem);
5899 assert_eq!(app.s3_state.selected_row, 2);
5900
5901 app.handle_action(Action::NextItem);
5903 assert_eq!(app.s3_state.selected_row, 2);
5904
5905 app.handle_action(Action::PrevItem);
5907 assert_eq!(app.s3_state.selected_row, 1);
5908
5909 app.handle_action(Action::PrevItem);
5910 assert_eq!(app.s3_state.selected_row, 0);
5911
5912 app.handle_action(Action::PrevItem);
5914 assert_eq!(app.s3_state.selected_row, 0);
5915 }
5916
5917 #[test]
5918 fn test_s3_bucket_navigation_with_expansion() {
5919 let mut app = test_app();
5920 app.current_service = Service::S3Buckets;
5921 app.service_selected = true;
5922 app.mode = Mode::Normal;
5923
5924 app.s3_state.buckets.items = vec![
5926 S3Bucket {
5927 name: "bucket1".to_string(),
5928 region: "us-east-1".to_string(),
5929 creation_date: "2024-01-01T00:00:00Z".to_string(),
5930 },
5931 S3Bucket {
5932 name: "bucket2".to_string(),
5933 region: "us-east-1".to_string(),
5934 creation_date: "2024-01-02T00:00:00Z".to_string(),
5935 },
5936 ];
5937
5938 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
5940 app.s3_state.bucket_preview.insert(
5941 "bucket1".to_string(),
5942 vec![
5943 S3Object {
5944 key: "file1.txt".to_string(),
5945 size: 100,
5946 last_modified: "2024-01-01T00:00:00Z".to_string(),
5947 is_prefix: false,
5948 storage_class: "STANDARD".to_string(),
5949 },
5950 S3Object {
5951 key: "folder/".to_string(),
5952 size: 0,
5953 last_modified: "2024-01-01T00:00:00Z".to_string(),
5954 is_prefix: true,
5955 storage_class: String::new(),
5956 },
5957 ],
5958 );
5959
5960 app.s3_state.selected_row = 0;
5961
5962 app.handle_action(Action::NextItem);
5965 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
5968 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
5971 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
5975 assert_eq!(app.s3_state.selected_row, 3);
5976 }
5977
5978 #[test]
5979 fn test_s3_bucket_navigation_with_nested_expansion() {
5980 let mut app = test_app();
5981 app.current_service = Service::S3Buckets;
5982 app.service_selected = true;
5983 app.mode = Mode::Normal;
5984
5985 app.s3_state.buckets.items = vec![S3Bucket {
5987 name: "bucket1".to_string(),
5988 region: "us-east-1".to_string(),
5989 creation_date: "2024-01-01T00:00:00Z".to_string(),
5990 }];
5991
5992 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
5994 app.s3_state.bucket_preview.insert(
5995 "bucket1".to_string(),
5996 vec![S3Object {
5997 key: "folder/".to_string(),
5998 size: 0,
5999 last_modified: "2024-01-01T00:00:00Z".to_string(),
6000 is_prefix: true,
6001 storage_class: String::new(),
6002 }],
6003 );
6004
6005 app.s3_state.expanded_prefixes.insert("folder/".to_string());
6007 app.s3_state.prefix_preview.insert(
6008 "folder/".to_string(),
6009 vec![
6010 S3Object {
6011 key: "folder/file1.txt".to_string(),
6012 size: 100,
6013 last_modified: "2024-01-01T00:00:00Z".to_string(),
6014 is_prefix: false,
6015 storage_class: "STANDARD".to_string(),
6016 },
6017 S3Object {
6018 key: "folder/file2.txt".to_string(),
6019 size: 200,
6020 last_modified: "2024-01-01T00:00:00Z".to_string(),
6021 is_prefix: false,
6022 storage_class: "STANDARD".to_string(),
6023 },
6024 ],
6025 );
6026
6027 app.s3_state.selected_row = 0;
6028
6029 app.handle_action(Action::NextItem);
6031 assert_eq!(app.s3_state.selected_row, 1); app.handle_action(Action::NextItem);
6034 assert_eq!(app.s3_state.selected_row, 2); app.handle_action(Action::NextItem);
6037 assert_eq!(app.s3_state.selected_row, 3); app.handle_action(Action::NextItem);
6041 assert_eq!(app.s3_state.selected_row, 3);
6042 }
6043
6044 #[test]
6045 fn test_calculate_total_bucket_rows() {
6046 let mut app = test_app();
6047
6048 assert_eq!(app.calculate_total_bucket_rows(), 0);
6050
6051 app.s3_state.buckets.items = vec![
6053 S3Bucket {
6054 name: "bucket1".to_string(),
6055 region: "us-east-1".to_string(),
6056 creation_date: "2024-01-01T00:00:00Z".to_string(),
6057 },
6058 S3Bucket {
6059 name: "bucket2".to_string(),
6060 region: "us-east-1".to_string(),
6061 creation_date: "2024-01-02T00:00:00Z".to_string(),
6062 },
6063 ];
6064 assert_eq!(app.calculate_total_bucket_rows(), 2);
6065
6066 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
6068 app.s3_state.bucket_preview.insert(
6069 "bucket1".to_string(),
6070 vec![
6071 S3Object {
6072 key: "file1.txt".to_string(),
6073 size: 100,
6074 last_modified: "2024-01-01T00:00:00Z".to_string(),
6075 is_prefix: false,
6076 storage_class: "STANDARD".to_string(),
6077 },
6078 S3Object {
6079 key: "file2.txt".to_string(),
6080 size: 200,
6081 last_modified: "2024-01-01T00:00:00Z".to_string(),
6082 is_prefix: false,
6083 storage_class: "STANDARD".to_string(),
6084 },
6085 S3Object {
6086 key: "folder/".to_string(),
6087 size: 0,
6088 last_modified: "2024-01-01T00:00:00Z".to_string(),
6089 is_prefix: true,
6090 storage_class: String::new(),
6091 },
6092 ],
6093 );
6094 assert_eq!(app.calculate_total_bucket_rows(), 5); app.s3_state.expanded_prefixes.insert("folder/".to_string());
6098 app.s3_state.prefix_preview.insert(
6099 "folder/".to_string(),
6100 vec![
6101 S3Object {
6102 key: "folder/nested1.txt".to_string(),
6103 size: 50,
6104 last_modified: "2024-01-01T00:00:00Z".to_string(),
6105 is_prefix: false,
6106 storage_class: "STANDARD".to_string(),
6107 },
6108 S3Object {
6109 key: "folder/nested2.txt".to_string(),
6110 size: 75,
6111 last_modified: "2024-01-01T00:00:00Z".to_string(),
6112 is_prefix: false,
6113 storage_class: "STANDARD".to_string(),
6114 },
6115 ],
6116 );
6117 assert_eq!(app.calculate_total_bucket_rows(), 7); }
6119
6120 #[test]
6121 fn test_calculate_total_object_rows() {
6122 let mut app = test_app();
6123 app.s3_state.current_bucket = Some("test-bucket".to_string());
6124
6125 assert_eq!(app.calculate_total_object_rows(), 0);
6127
6128 app.s3_state.objects = vec![
6130 S3Object {
6131 key: "file1.txt".to_string(),
6132 size: 100,
6133 last_modified: "2024-01-01T00:00:00Z".to_string(),
6134 is_prefix: false,
6135 storage_class: "STANDARD".to_string(),
6136 },
6137 S3Object {
6138 key: "folder/".to_string(),
6139 size: 0,
6140 last_modified: "2024-01-01T00:00:00Z".to_string(),
6141 is_prefix: true,
6142 storage_class: String::new(),
6143 },
6144 ];
6145 assert_eq!(app.calculate_total_object_rows(), 2);
6146
6147 app.s3_state.expanded_prefixes.insert("folder/".to_string());
6149 app.s3_state.prefix_preview.insert(
6150 "folder/".to_string(),
6151 vec![
6152 S3Object {
6153 key: "folder/file2.txt".to_string(),
6154 size: 200,
6155 last_modified: "2024-01-01T00:00:00Z".to_string(),
6156 is_prefix: false,
6157 storage_class: "STANDARD".to_string(),
6158 },
6159 S3Object {
6160 key: "folder/subfolder/".to_string(),
6161 size: 0,
6162 last_modified: "2024-01-01T00:00:00Z".to_string(),
6163 is_prefix: true,
6164 storage_class: String::new(),
6165 },
6166 ],
6167 );
6168 assert_eq!(app.calculate_total_object_rows(), 4); app.s3_state
6172 .expanded_prefixes
6173 .insert("folder/subfolder/".to_string());
6174 app.s3_state.prefix_preview.insert(
6175 "folder/subfolder/".to_string(),
6176 vec![S3Object {
6177 key: "folder/subfolder/deep.txt".to_string(),
6178 size: 50,
6179 last_modified: "2024-01-01T00:00:00Z".to_string(),
6180 is_prefix: false,
6181 storage_class: "STANDARD".to_string(),
6182 }],
6183 );
6184 assert_eq!(app.calculate_total_object_rows(), 5); }
6186
6187 #[test]
6188 fn test_s3_object_navigation_with_deep_nesting() {
6189 let mut app = test_app();
6190 app.current_service = Service::S3Buckets;
6191 app.service_selected = true;
6192 app.mode = Mode::Normal;
6193 app.s3_state.current_bucket = Some("test-bucket".to_string());
6194
6195 app.s3_state.objects = vec![S3Object {
6197 key: "folder1/".to_string(),
6198 size: 0,
6199 last_modified: "2024-01-01T00:00:00Z".to_string(),
6200 is_prefix: true,
6201 storage_class: String::new(),
6202 }];
6203
6204 app.s3_state
6206 .expanded_prefixes
6207 .insert("folder1/".to_string());
6208 app.s3_state.prefix_preview.insert(
6209 "folder1/".to_string(),
6210 vec![S3Object {
6211 key: "folder1/folder2/".to_string(),
6212 size: 0,
6213 last_modified: "2024-01-01T00:00:00Z".to_string(),
6214 is_prefix: true,
6215 storage_class: String::new(),
6216 }],
6217 );
6218
6219 app.s3_state
6221 .expanded_prefixes
6222 .insert("folder1/folder2/".to_string());
6223 app.s3_state.prefix_preview.insert(
6224 "folder1/folder2/".to_string(),
6225 vec![S3Object {
6226 key: "folder1/folder2/file.txt".to_string(),
6227 size: 100,
6228 last_modified: "2024-01-01T00:00:00Z".to_string(),
6229 is_prefix: false,
6230 storage_class: "STANDARD".to_string(),
6231 }],
6232 );
6233
6234 app.s3_state.selected_object = 0;
6235
6236 app.handle_action(Action::NextItem);
6238 assert_eq!(app.s3_state.selected_object, 1); app.handle_action(Action::NextItem);
6241 assert_eq!(app.s3_state.selected_object, 2); app.handle_action(Action::NextItem);
6245 assert_eq!(app.s3_state.selected_object, 2);
6246 }
6247
6248 #[test]
6249 fn test_s3_expand_nested_folder_in_objects_view() {
6250 let mut app = test_app();
6251 app.current_service = Service::S3Buckets;
6252 app.service_selected = true;
6253 app.mode = Mode::Normal;
6254 app.s3_state.current_bucket = Some("test-bucket".to_string());
6255
6256 app.s3_state.objects = vec![S3Object {
6258 key: "parent/".to_string(),
6259 size: 0,
6260 last_modified: "2024-01-01T00:00:00Z".to_string(),
6261 is_prefix: true,
6262 storage_class: String::new(),
6263 }];
6264
6265 app.s3_state.expanded_prefixes.insert("parent/".to_string());
6267 app.s3_state.prefix_preview.insert(
6268 "parent/".to_string(),
6269 vec![S3Object {
6270 key: "parent/child/".to_string(),
6271 size: 0,
6272 last_modified: "2024-01-01T00:00:00Z".to_string(),
6273 is_prefix: true,
6274 storage_class: String::new(),
6275 }],
6276 );
6277
6278 app.s3_state.selected_object = 1;
6280
6281 app.handle_action(Action::NextPane);
6283
6284 assert!(app.s3_state.expanded_prefixes.contains("parent/child/"));
6286 assert!(app.s3_state.buckets.loading); }
6288
6289 #[test]
6290 fn test_s3_drill_into_nested_folder() {
6291 let mut app = test_app();
6292 app.current_service = Service::S3Buckets;
6293 app.service_selected = true;
6294 app.mode = Mode::Normal;
6295 app.s3_state.current_bucket = Some("test-bucket".to_string());
6296
6297 app.s3_state.objects = vec![S3Object {
6299 key: "parent/".to_string(),
6300 size: 0,
6301 last_modified: "2024-01-01T00:00:00Z".to_string(),
6302 is_prefix: true,
6303 storage_class: String::new(),
6304 }];
6305
6306 app.s3_state.expanded_prefixes.insert("parent/".to_string());
6308 app.s3_state.prefix_preview.insert(
6309 "parent/".to_string(),
6310 vec![S3Object {
6311 key: "parent/child/".to_string(),
6312 size: 0,
6313 last_modified: "2024-01-01T00:00:00Z".to_string(),
6314 is_prefix: true,
6315 storage_class: String::new(),
6316 }],
6317 );
6318
6319 app.s3_state.selected_object = 1;
6321
6322 app.handle_action(Action::Select);
6324
6325 assert_eq!(app.s3_state.prefix_stack, vec!["parent/child/".to_string()]);
6327 assert!(app.s3_state.buckets.loading); }
6329
6330 #[test]
6331 fn test_s3_esc_pops_navigation_stack() {
6332 let mut app = test_app();
6333 app.current_service = Service::S3Buckets;
6334 app.s3_state.current_bucket = Some("test-bucket".to_string());
6335 app.s3_state.prefix_stack = vec!["level1/".to_string(), "level1/level2/".to_string()];
6336
6337 app.handle_action(Action::GoBack);
6339 assert_eq!(app.s3_state.prefix_stack, vec!["level1/".to_string()]);
6340 assert!(app.s3_state.buckets.loading);
6341
6342 app.s3_state.buckets.loading = false;
6344 app.handle_action(Action::GoBack);
6345 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
6346 assert!(app.s3_state.buckets.loading);
6347
6348 app.s3_state.buckets.loading = false;
6350 app.handle_action(Action::GoBack);
6351 assert_eq!(app.s3_state.current_bucket, None);
6352 }
6353
6354 #[test]
6355 fn test_s3_esc_from_bucket_root_exits() {
6356 let mut app = test_app();
6357 app.current_service = Service::S3Buckets;
6358 app.s3_state.current_bucket = Some("test-bucket".to_string());
6359 app.s3_state.prefix_stack = vec![];
6360
6361 app.handle_action(Action::GoBack);
6363 assert_eq!(app.s3_state.current_bucket, None);
6364 assert_eq!(app.s3_state.objects.len(), 0);
6365 }
6366
6367 #[test]
6368 fn test_s3_drill_into_nested_prefix_from_bucket_list() {
6369 let mut app = test_app();
6370 app.current_service = Service::S3Buckets;
6371 app.service_selected = true;
6372 app.mode = Mode::Normal;
6373
6374 app.s3_state.buckets.items = vec![S3Bucket {
6376 name: "test-bucket".to_string(),
6377 region: "us-east-1".to_string(),
6378 creation_date: "2024-01-01".to_string(),
6379 }];
6380
6381 app.s3_state
6383 .expanded_prefixes
6384 .insert("test-bucket".to_string());
6385 app.s3_state.bucket_preview.insert(
6386 "test-bucket".to_string(),
6387 vec![S3Object {
6388 key: "parent/".to_string(),
6389 size: 0,
6390 last_modified: "2024-01-01".to_string(),
6391 is_prefix: true,
6392 storage_class: String::new(),
6393 }],
6394 );
6395
6396 app.s3_state.expanded_prefixes.insert("parent/".to_string());
6398 app.s3_state.prefix_preview.insert(
6399 "parent/".to_string(),
6400 vec![S3Object {
6401 key: "parent/child/".to_string(),
6402 size: 0,
6403 last_modified: "2024-01-01".to_string(),
6404 is_prefix: true,
6405 storage_class: String::new(),
6406 }],
6407 );
6408
6409 app.s3_state.selected_row = 2;
6411
6412 app.handle_action(Action::Select);
6414
6415 assert_eq!(
6417 app.s3_state.prefix_stack,
6418 vec!["parent/".to_string(), "parent/child/".to_string()]
6419 );
6420 assert_eq!(app.s3_state.current_bucket, Some("test-bucket".to_string()));
6421 assert!(app.s3_state.buckets.loading);
6422
6423 app.s3_state.buckets.loading = false;
6425 app.handle_action(Action::GoBack);
6426 assert_eq!(app.s3_state.prefix_stack, vec!["parent/".to_string()]);
6427 assert!(app.s3_state.buckets.loading);
6428
6429 app.s3_state.buckets.loading = false;
6431 app.handle_action(Action::GoBack);
6432 assert_eq!(app.s3_state.prefix_stack, Vec::<String>::new());
6433 assert!(app.s3_state.buckets.loading);
6434
6435 app.s3_state.buckets.loading = false;
6437 app.handle_action(Action::GoBack);
6438 assert_eq!(app.s3_state.current_bucket, None);
6439 }
6440
6441 #[test]
6442 fn test_region_picker_fuzzy_filter() {
6443 let mut app = test_app();
6444 app.region_latencies.insert("us-east-1".to_string(), 10);
6445 app.region_filter = "vir".to_string();
6446 let filtered = app.get_filtered_regions();
6447 assert!(filtered.iter().any(|r| r.code == "us-east-1"));
6448 }
6449
6450 #[test]
6451 fn test_profile_picker_loads_profiles() {
6452 let profiles = App::load_aws_profiles();
6453 assert!(profiles.is_empty() || profiles.iter().any(|p| p.name == "default"));
6455 }
6456
6457 #[test]
6458 fn test_profile_with_region_uses_it() {
6459 let mut app = test_app_no_region();
6460 app.available_profiles = vec![AwsProfile {
6461 name: "test-profile".to_string(),
6462 region: Some("eu-west-1".to_string()),
6463 account: Some("123456789".to_string()),
6464 role_arn: None,
6465 source_profile: None,
6466 }];
6467 app.profile_picker_selected = 0;
6468 app.mode = Mode::ProfilePicker;
6469
6470 let filtered = app.get_filtered_profiles();
6472 if let Some(profile) = filtered.first() {
6473 let profile_name = profile.name.clone();
6474 let profile_region = profile.region.clone();
6475
6476 app.profile = profile_name;
6477 if let Some(region) = profile_region {
6478 app.region = region;
6479 }
6480 }
6481
6482 assert_eq!(app.profile, "test-profile");
6483 assert_eq!(app.region, "eu-west-1");
6484 }
6485
6486 #[test]
6487 fn test_profile_without_region_keeps_unknown() {
6488 let mut app = test_app_no_region();
6489 let initial_region = app.region.clone();
6490
6491 app.available_profiles = vec![AwsProfile {
6492 name: "test-profile".to_string(),
6493 region: None,
6494 account: None,
6495 role_arn: None,
6496 source_profile: None,
6497 }];
6498 app.profile_picker_selected = 0;
6499 app.mode = Mode::ProfilePicker;
6500
6501 let filtered = app.get_filtered_profiles();
6502 if let Some(profile) = filtered.first() {
6503 let profile_name = profile.name.clone();
6504 let profile_region = profile.region.clone();
6505
6506 app.profile = profile_name;
6507 if let Some(region) = profile_region {
6508 app.region = region;
6509 }
6510 }
6511
6512 assert_eq!(app.profile, "test-profile");
6513 assert_eq!(app.region, initial_region); }
6515
6516 #[test]
6517 fn test_region_selection_closes_all_tabs() {
6518 let mut app = test_app();
6519
6520 app.tabs.push(Tab {
6522 service: Service::CloudWatchLogGroups,
6523 title: "CloudWatch".to_string(),
6524 breadcrumb: "CloudWatch".to_string(),
6525 });
6526 app.tabs.push(Tab {
6527 service: Service::S3Buckets,
6528 title: "S3".to_string(),
6529 breadcrumb: "S3".to_string(),
6530 });
6531 app.service_selected = true;
6532 app.current_tab = 1;
6533
6534 app.region_latencies.insert("eu-west-1".to_string(), 50);
6536
6537 app.mode = Mode::RegionPicker;
6539 app.region_picker_selected = 0;
6540
6541 let filtered = app.get_filtered_regions();
6542 if let Some(region) = filtered.first() {
6543 app.region = region.code.to_string();
6544 app.tabs.clear();
6545 app.current_tab = 0;
6546 app.service_selected = false;
6547 app.mode = Mode::Normal;
6548 }
6549
6550 assert_eq!(app.tabs.len(), 0);
6551 assert_eq!(app.current_tab, 0);
6552 assert!(!app.service_selected);
6553 assert_eq!(app.region, "eu-west-1");
6554 }
6555
6556 #[test]
6557 fn test_region_picker_can_be_closed_without_selection() {
6558 let mut app = test_app();
6559 let initial_region = app.region.clone();
6560
6561 app.mode = Mode::RegionPicker;
6562
6563 app.mode = Mode::Normal;
6565
6566 assert_eq!(app.region, initial_region);
6568 }
6569
6570 #[test]
6571 fn test_session_filter_works() {
6572 let mut app = test_app();
6573
6574 app.sessions = vec![
6575 Session {
6576 id: "1".to_string(),
6577 timestamp: "2024-01-01".to_string(),
6578 profile: "prod-profile".to_string(),
6579 region: "us-east-1".to_string(),
6580 account_id: "123456789".to_string(),
6581 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
6582 tabs: vec![],
6583 },
6584 Session {
6585 id: "2".to_string(),
6586 timestamp: "2024-01-02".to_string(),
6587 profile: "dev-profile".to_string(),
6588 region: "eu-west-1".to_string(),
6589 account_id: "987654321".to_string(),
6590 role_arn: "arn:aws:iam::987654321:role/dev".to_string(),
6591 tabs: vec![],
6592 },
6593 ];
6594
6595 app.session_filter = "prod".to_string();
6597 let filtered = app.get_filtered_sessions();
6598 assert_eq!(filtered.len(), 1);
6599 assert_eq!(filtered[0].profile, "prod-profile");
6600
6601 app.session_filter = "eu".to_string();
6603 let filtered = app.get_filtered_sessions();
6604 assert_eq!(filtered.len(), 1);
6605 assert_eq!(filtered[0].region, "eu-west-1");
6606
6607 app.session_filter.clear();
6609 let filtered = app.get_filtered_sessions();
6610 assert_eq!(filtered.len(), 2);
6611 }
6612
6613 #[test]
6614 fn test_profile_picker_shows_account() {
6615 let mut app = test_app_no_region();
6616 app.available_profiles = vec![AwsProfile {
6617 name: "test-profile".to_string(),
6618 region: Some("us-east-1".to_string()),
6619 account: Some("123456789".to_string()),
6620 role_arn: None,
6621 source_profile: None,
6622 }];
6623
6624 let filtered = app.get_filtered_profiles();
6625 assert_eq!(filtered.len(), 1);
6626 assert_eq!(filtered[0].account, Some("123456789".to_string()));
6627 }
6628
6629 #[test]
6630 fn test_profile_without_account() {
6631 let mut app = test_app_no_region();
6632 app.available_profiles = vec![AwsProfile {
6633 name: "test-profile".to_string(),
6634 region: Some("us-east-1".to_string()),
6635 account: None,
6636 role_arn: None,
6637 source_profile: None,
6638 }];
6639
6640 let filtered = app.get_filtered_profiles();
6641 assert_eq!(filtered.len(), 1);
6642 assert_eq!(filtered[0].account, None);
6643 }
6644
6645 #[test]
6646 fn test_profile_with_all_fields() {
6647 let mut app = test_app_no_region();
6648 app.available_profiles = vec![AwsProfile {
6649 name: "prod-profile".to_string(),
6650 region: Some("us-west-2".to_string()),
6651 account: Some("123456789".to_string()),
6652 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
6653 source_profile: Some("base-profile".to_string()),
6654 }];
6655
6656 let filtered = app.get_filtered_profiles();
6657 assert_eq!(filtered.len(), 1);
6658 assert_eq!(filtered[0].name, "prod-profile");
6659 assert_eq!(filtered[0].region, Some("us-west-2".to_string()));
6660 assert_eq!(filtered[0].account, Some("123456789".to_string()));
6661 assert_eq!(
6662 filtered[0].role_arn,
6663 Some("arn:aws:iam::123456789:role/AdminRole".to_string())
6664 );
6665 assert_eq!(filtered[0].source_profile, Some("base-profile".to_string()));
6666 }
6667
6668 #[test]
6669 fn test_profile_filter_by_source_profile() {
6670 let mut app = test_app_no_region();
6671 app.available_profiles = vec![
6672 AwsProfile {
6673 name: "profile1".to_string(),
6674 region: None,
6675 account: None,
6676 role_arn: None,
6677 source_profile: Some("base".to_string()),
6678 },
6679 AwsProfile {
6680 name: "profile2".to_string(),
6681 region: None,
6682 account: None,
6683 role_arn: None,
6684 source_profile: Some("other".to_string()),
6685 },
6686 ];
6687
6688 app.profile_filter = "base".to_string();
6689 let filtered = app.get_filtered_profiles();
6690 assert_eq!(filtered.len(), 1);
6691 assert_eq!(filtered[0].name, "profile1");
6692 }
6693
6694 #[test]
6695 fn test_profile_filter_by_role() {
6696 let mut app = test_app_no_region();
6697 app.available_profiles = vec![
6698 AwsProfile {
6699 name: "admin-profile".to_string(),
6700 region: None,
6701 account: None,
6702 role_arn: Some("arn:aws:iam::123:role/AdminRole".to_string()),
6703 source_profile: None,
6704 },
6705 AwsProfile {
6706 name: "dev-profile".to_string(),
6707 region: None,
6708 account: None,
6709 role_arn: Some("arn:aws:iam::123:role/DevRole".to_string()),
6710 source_profile: None,
6711 },
6712 ];
6713
6714 app.profile_filter = "Admin".to_string();
6715 let filtered = app.get_filtered_profiles();
6716 assert_eq!(filtered.len(), 1);
6717 assert_eq!(filtered[0].name, "admin-profile");
6718 }
6719
6720 #[test]
6721 fn test_profiles_sorted_by_name() {
6722 let mut app = test_app_no_region();
6723 app.available_profiles = vec![
6724 AwsProfile {
6725 name: "zebra-profile".to_string(),
6726 region: None,
6727 account: None,
6728 role_arn: None,
6729 source_profile: None,
6730 },
6731 AwsProfile {
6732 name: "alpha-profile".to_string(),
6733 region: None,
6734 account: None,
6735 role_arn: None,
6736 source_profile: None,
6737 },
6738 AwsProfile {
6739 name: "beta-profile".to_string(),
6740 region: None,
6741 account: None,
6742 role_arn: None,
6743 source_profile: None,
6744 },
6745 ];
6746
6747 let filtered = app.get_filtered_profiles();
6748 assert_eq!(filtered.len(), 3);
6749 assert_eq!(filtered[0].name, "alpha-profile");
6750 assert_eq!(filtered[1].name, "beta-profile");
6751 assert_eq!(filtered[2].name, "zebra-profile");
6752 }
6753
6754 #[test]
6755 fn test_profile_with_role_arn() {
6756 let mut app = test_app_no_region();
6757 app.available_profiles = vec![AwsProfile {
6758 name: "role-profile".to_string(),
6759 region: Some("us-east-1".to_string()),
6760 account: Some("123456789".to_string()),
6761 role_arn: Some("arn:aws:iam::123456789:role/AdminRole".to_string()),
6762 source_profile: None,
6763 }];
6764
6765 let filtered = app.get_filtered_profiles();
6766 assert_eq!(filtered.len(), 1);
6767 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":role/"));
6768 }
6769
6770 #[test]
6771 fn test_profile_with_user_arn() {
6772 let mut app = test_app_no_region();
6773 app.available_profiles = vec![AwsProfile {
6774 name: "user-profile".to_string(),
6775 region: Some("us-east-1".to_string()),
6776 account: Some("123456789".to_string()),
6777 role_arn: Some("arn:aws:iam::123456789:user/john-doe".to_string()),
6778 source_profile: None,
6779 }];
6780
6781 let filtered = app.get_filtered_profiles();
6782 assert_eq!(filtered.len(), 1);
6783 assert!(filtered[0].role_arn.as_ref().unwrap().contains(":user/"));
6784 }
6785
6786 #[test]
6787 fn test_filtered_profiles_also_sorted() {
6788 let mut app = test_app_no_region();
6789 app.available_profiles = vec![
6790 AwsProfile {
6791 name: "prod-zebra".to_string(),
6792 region: Some("us-east-1".to_string()),
6793 account: None,
6794 role_arn: None,
6795 source_profile: None,
6796 },
6797 AwsProfile {
6798 name: "prod-alpha".to_string(),
6799 region: Some("us-east-1".to_string()),
6800 account: None,
6801 role_arn: None,
6802 source_profile: None,
6803 },
6804 AwsProfile {
6805 name: "dev-profile".to_string(),
6806 region: Some("us-west-2".to_string()),
6807 account: None,
6808 role_arn: None,
6809 source_profile: None,
6810 },
6811 ];
6812
6813 app.profile_filter = "prod".to_string();
6814 let filtered = app.get_filtered_profiles();
6815 assert_eq!(filtered.len(), 2);
6816 assert_eq!(filtered[0].name, "prod-alpha");
6817 assert_eq!(filtered[1].name, "prod-zebra");
6818 }
6819
6820 #[test]
6821 fn test_profile_picker_has_all_columns() {
6822 let mut app = test_app_no_region();
6823 app.available_profiles = vec![AwsProfile {
6824 name: "test".to_string(),
6825 region: Some("us-east-1".to_string()),
6826 account: Some("123456789".to_string()),
6827 role_arn: Some("arn:aws:iam::123456789:role/Admin".to_string()),
6828 source_profile: Some("base".to_string()),
6829 }];
6830
6831 let filtered = app.get_filtered_profiles();
6832 assert_eq!(filtered.len(), 1);
6833 assert!(filtered[0].name == "test");
6834 assert!(filtered[0].region.is_some());
6835 assert!(filtered[0].account.is_some());
6836 assert!(filtered[0].role_arn.is_some());
6837 assert!(filtered[0].source_profile.is_some());
6838 }
6839
6840 #[test]
6841 fn test_session_picker_shows_tab_count() {
6842 let mut app = test_app_no_region();
6843 app.sessions = vec![Session {
6844 id: "1".to_string(),
6845 timestamp: "2024-01-01".to_string(),
6846 profile: "test".to_string(),
6847 region: "us-east-1".to_string(),
6848 account_id: "123".to_string(),
6849 role_arn: String::new(),
6850 tabs: vec![
6851 SessionTab {
6852 service: "CloudWatch".to_string(),
6853 title: "Logs".to_string(),
6854 breadcrumb: String::new(),
6855 filter: None,
6856 selected_item: None,
6857 },
6858 SessionTab {
6859 service: "S3".to_string(),
6860 title: "Buckets".to_string(),
6861 breadcrumb: String::new(),
6862 filter: None,
6863 selected_item: None,
6864 },
6865 ],
6866 }];
6867
6868 let filtered = app.get_filtered_sessions();
6869 assert_eq!(filtered.len(), 1);
6870 assert_eq!(filtered[0].tabs.len(), 2);
6871 }
6872
6873 #[test]
6874 fn test_start_background_data_fetch_loads_profiles() {
6875 let mut app = test_app_no_region();
6876 assert!(app.available_profiles.is_empty());
6877
6878 app.available_profiles = App::load_aws_profiles();
6880
6881 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
6883 }
6884
6885 #[test]
6886 fn test_refresh_in_profile_picker() {
6887 let mut app = test_app_no_region();
6888 app.mode = Mode::ProfilePicker;
6889 app.available_profiles = vec![AwsProfile {
6890 name: "test".to_string(),
6891 region: None,
6892 account: None,
6893 role_arn: None,
6894 source_profile: None,
6895 }];
6896
6897 app.handle_action(Action::Refresh);
6898
6899 assert!(app.log_groups_state.loading);
6901 assert!(
6902 app.log_groups_state.loading_message.is_empty()
6903 || !app.log_groups_state.loading_message.contains("Refreshing"),
6904 "loading_message must not say 'Refreshing...' as it leaks into other services"
6905 );
6906 }
6907
6908 #[test]
6909 fn test_refresh_sets_loading_for_profile_picker() {
6910 let mut app = test_app_no_region();
6911 app.mode = Mode::ProfilePicker;
6912
6913 assert!(!app.log_groups_state.loading);
6914
6915 app.handle_action(Action::Refresh);
6916
6917 assert!(app.log_groups_state.loading);
6918 }
6919
6920 #[test]
6921 fn test_profiles_loaded_on_demand() {
6922 let mut app = test_app_no_region();
6923
6924 assert!(app.available_profiles.is_empty());
6926
6927 app.available_profiles = App::load_aws_profiles();
6929
6930 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
6932 }
6933
6934 #[test]
6935 fn test_profile_accounts_not_fetched_automatically() {
6936 let mut app = test_app_no_region();
6937 app.available_profiles = App::load_aws_profiles();
6938
6939 for profile in &app.available_profiles {
6941 assert!(profile.account.is_none() || profile.account.is_some());
6944 }
6945 }
6946
6947 #[test]
6948 fn test_ctrl_r_triggers_account_fetch() {
6949 let mut app = test_app_no_region();
6950 app.mode = Mode::ProfilePicker;
6951 app.available_profiles = vec![AwsProfile {
6952 name: "test".to_string(),
6953 region: Some("us-east-1".to_string()),
6954 account: None,
6955 role_arn: None,
6956 source_profile: None,
6957 }];
6958
6959 assert!(app.available_profiles[0].account.is_none());
6961
6962 app.handle_action(Action::Refresh);
6964
6965 assert!(app.log_groups_state.loading);
6967 }
6968
6969 #[test]
6970 fn test_refresh_in_region_picker() {
6971 let mut app = test_app_no_region();
6972 app.mode = Mode::RegionPicker;
6973
6974 let initial_latencies = app.region_latencies.len();
6975 app.handle_action(Action::Refresh);
6976
6977 assert!(app.region_latencies.is_empty() || app.region_latencies.len() >= initial_latencies);
6979 }
6980
6981 #[test]
6982 fn test_refresh_in_session_picker() {
6983 let mut app = test_app_no_region();
6984 app.mode = Mode::SessionPicker;
6985 app.sessions = vec![];
6986
6987 app.handle_action(Action::Refresh);
6988
6989 assert!(app.sessions.is_empty() || !app.sessions.is_empty());
6991 }
6992
6993 #[test]
6994 fn test_session_picker_selection() {
6995 let mut app = test_app();
6996
6997 app.sessions = vec![Session {
6998 id: "1".to_string(),
6999 timestamp: "2024-01-01".to_string(),
7000 profile: "prod-profile".to_string(),
7001 region: "us-west-2".to_string(),
7002 account_id: "123456789".to_string(),
7003 role_arn: "arn:aws:iam::123456789:role/admin".to_string(),
7004 tabs: vec![SessionTab {
7005 service: "CloudWatchLogGroups".to_string(),
7006 title: "Log Groups".to_string(),
7007 breadcrumb: "CloudWatch › Log Groups".to_string(),
7008 filter: Some("test".to_string()),
7009 selected_item: None,
7010 }],
7011 }];
7012
7013 app.mode = Mode::SessionPicker;
7014 app.session_picker_selected = 0;
7015
7016 app.handle_action(Action::Select);
7018
7019 assert_eq!(app.mode, Mode::Normal);
7020 assert_eq!(app.profile, "prod-profile");
7021 assert_eq!(app.region, "us-west-2");
7022 assert_eq!(app.config.account_id, "123456789");
7023 assert_eq!(app.tabs.len(), 1);
7024 assert_eq!(app.tabs[0].title, "Log Groups");
7025 }
7026
7027 #[test]
7028 fn test_save_session_creates_session() {
7029 let mut app =
7030 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7031 app.config.account_id = "123456789".to_string();
7032 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7033
7034 app.tabs.push(Tab {
7035 service: Service::CloudWatchLogGroups,
7036 title: "Log Groups".to_string(),
7037 breadcrumb: "CloudWatch › Log Groups".to_string(),
7038 });
7039
7040 app.save_current_session();
7041
7042 assert!(app.current_session.is_some());
7043 let session = app.current_session.clone().unwrap();
7044 assert_eq!(session.profile, "test-profile");
7045 assert_eq!(session.region, "us-east-1");
7046 assert_eq!(session.account_id, "123456789");
7047 assert_eq!(session.tabs.len(), 1);
7048
7049 let _ = session.delete();
7051 }
7052
7053 #[test]
7054 fn test_save_session_updates_existing() {
7055 let mut app =
7056 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7057 app.config.account_id = "123456789".to_string();
7058 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7059
7060 app.current_session = Some(Session {
7061 id: "existing".to_string(),
7062 timestamp: "2024-01-01".to_string(),
7063 profile: "test-profile".to_string(),
7064 region: "us-east-1".to_string(),
7065 account_id: "123456789".to_string(),
7066 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
7067 tabs: vec![],
7068 });
7069
7070 app.tabs.push(Tab {
7071 service: Service::CloudWatchLogGroups,
7072 title: "Log Groups".to_string(),
7073 breadcrumb: "CloudWatch › Log Groups".to_string(),
7074 });
7075
7076 app.save_current_session();
7077
7078 let session = app.current_session.clone().unwrap();
7079 assert_eq!(session.id, "existing");
7080 assert_eq!(session.tabs.len(), 1);
7081
7082 let _ = session.delete();
7084 }
7085
7086 #[test]
7087 fn test_save_session_skips_empty_tabs() {
7088 let mut app =
7089 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7090 app.config.account_id = "123456789".to_string();
7091
7092 app.save_current_session();
7093
7094 assert!(app.current_session.is_none());
7095 }
7096
7097 #[test]
7098 fn test_save_session_deletes_when_tabs_closed() {
7099 let mut app =
7100 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7101 app.config.account_id = "123456789".to_string();
7102 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7103
7104 app.current_session = Some(Session {
7106 id: "test_delete".to_string(),
7107 timestamp: "2024-01-01 10:00:00 UTC".to_string(),
7108 profile: "test-profile".to_string(),
7109 region: "us-east-1".to_string(),
7110 account_id: "123456789".to_string(),
7111 role_arn: "arn:aws:iam::123456789:role/test".to_string(),
7112 tabs: vec![],
7113 });
7114
7115 app.save_current_session();
7117
7118 assert!(app.current_session.is_none());
7119 }
7120
7121 #[test]
7122 fn test_closing_all_tabs_deletes_session() {
7123 let mut app =
7124 App::new_without_client("test-profile".to_string(), Some("us-east-1".to_string()));
7125 app.config.account_id = "123456789".to_string();
7126 app.config.role_arn = "arn:aws:iam::123456789:role/test".to_string();
7127
7128 app.tabs.push(Tab {
7130 service: Service::CloudWatchLogGroups,
7131 title: "Log Groups".to_string(),
7132 breadcrumb: "CloudWatch › Log Groups".to_string(),
7133 });
7134
7135 app.save_current_session();
7137 assert!(app.current_session.is_some());
7138 let session_id = app.current_session.as_ref().unwrap().id.clone();
7139
7140 app.tabs.clear();
7142
7143 app.save_current_session();
7145 assert!(app.current_session.is_none());
7146
7147 let _ = Session::load(&session_id).map(|s| s.delete());
7149 }
7150
7151 #[test]
7152 fn test_credential_error_opens_profile_picker() {
7153 let mut app = App::new_without_client("default".to_string(), None);
7155 let error_str = "Unable to load credentials from any source";
7156
7157 if error_str.contains("credentials") {
7158 app.available_profiles = App::load_aws_profiles();
7159 app.mode = Mode::ProfilePicker;
7160 }
7161
7162 assert_eq!(app.mode, Mode::ProfilePicker);
7163 assert!(!app.available_profiles.is_empty() || app.available_profiles.is_empty());
7165 }
7166
7167 #[test]
7168 fn test_non_credential_error_shows_error_modal() {
7169 let mut app = App::new_without_client("default".to_string(), None);
7170 let error_str = "Network timeout";
7171
7172 if !error_str.contains("credentials") {
7173 app.error_message = Some(error_str.to_string());
7174 app.mode = Mode::ErrorModal;
7175 }
7176
7177 assert_eq!(app.mode, Mode::ErrorModal);
7178 assert!(app.error_message.is_some());
7179 }
7180
7181 #[tokio::test]
7182 async fn test_profile_selection_loads_credentials() {
7183 std::env::set_var("AWS_PROFILE", "default");
7185
7186 let result = App::new(Some("default".to_string()), Some("us-east-1".to_string())).await;
7188
7189 if let Ok(app) = result {
7190 assert!(!app.config.account_id.is_empty());
7192 assert!(!app.config.role_arn.is_empty());
7193 assert_eq!(app.profile, "default");
7194 assert_eq!(app.config.region, "us-east-1");
7195 }
7196 }
7198
7199 #[test]
7200 fn test_new_app_shows_service_picker_with_no_tabs() {
7201 let app = App::new_without_client("default".to_string(), Some("us-east-1".to_string()));
7202
7203 assert!(!app.service_selected);
7205 assert_eq!(app.mode, Mode::ServicePicker);
7207 assert!(app.tabs.is_empty());
7209 }
7210
7211 #[tokio::test]
7212 async fn test_aws_profile_env_var_read_before_config_load() {
7213 std::env::set_var("AWS_PROFILE", "test-profile");
7215
7216 let profile_name = None
7218 .or_else(|| std::env::var("AWS_PROFILE").ok())
7219 .unwrap_or_else(|| "default".to_string());
7220
7221 assert_eq!(profile_name, "test-profile");
7223
7224 std::env::set_var("AWS_PROFILE", &profile_name);
7226
7227 assert_eq!(std::env::var("AWS_PROFILE").unwrap(), "test-profile");
7229
7230 std::env::remove_var("AWS_PROFILE");
7231 }
7232
7233 #[test]
7234 fn test_next_preferences_cloudformation() {
7235 let mut app = test_app();
7236 app.current_service = Service::CloudFormationStacks;
7237 app.mode = Mode::ColumnSelector;
7238 app.column_selector_index = 0;
7239
7240 let page_size_idx = app.cfn_column_ids.len() + 2;
7242 app.handle_action(Action::NextPreferences);
7243 assert_eq!(app.column_selector_index, page_size_idx);
7244
7245 app.handle_action(Action::NextPreferences);
7247 assert_eq!(app.column_selector_index, 0);
7248 }
7249
7250 #[test]
7251 fn test_s3_preferences_tab_cycling() {
7252 let mut app = test_app();
7253 app.current_service = Service::S3Buckets;
7254 app.mode = Mode::ColumnSelector;
7255 app.column_selector_index = 0;
7256
7257 let page_size_idx = app.s3_bucket_column_ids.len() + 2;
7258
7259 app.handle_action(Action::NextPreferences);
7261 assert_eq!(app.column_selector_index, page_size_idx);
7262
7263 app.handle_action(Action::NextPreferences);
7265 assert_eq!(app.column_selector_index, 0);
7266
7267 app.handle_action(Action::PrevPreferences);
7269 assert_eq!(app.column_selector_index, page_size_idx);
7270
7271 app.handle_action(Action::PrevPreferences);
7273 assert_eq!(app.column_selector_index, 0);
7274 }
7275
7276 #[test]
7277 fn test_s3_filter_resets_selection() {
7278 let mut app = test_app();
7279 app.current_service = Service::S3Buckets;
7280 app.service_selected = true;
7281
7282 app.s3_state.buckets.items = vec![
7284 S3Bucket {
7285 name: "bucket-1".to_string(),
7286 region: "us-east-1".to_string(),
7287 creation_date: "2023-01-01".to_string(),
7288 },
7289 S3Bucket {
7290 name: "bucket-2".to_string(),
7291 region: "us-east-1".to_string(),
7292 creation_date: "2023-01-02".to_string(),
7293 },
7294 S3Bucket {
7295 name: "other-bucket".to_string(),
7296 region: "us-east-1".to_string(),
7297 creation_date: "2023-01-03".to_string(),
7298 },
7299 ];
7300
7301 app.s3_state.selected_row = 1;
7303 app.s3_state.bucket_scroll_offset = 1;
7304
7305 app.mode = Mode::FilterInput;
7307 app.apply_filter_operation(|f| f.push_str("bucket-"));
7308
7309 assert_eq!(app.s3_state.selected_row, 0);
7311 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
7312 assert_eq!(app.s3_state.buckets.filter, "bucket-");
7313 }
7314
7315 #[test]
7316 fn test_s3_navigation_respects_filter() {
7317 let mut app = test_app();
7318 app.current_service = Service::S3Buckets;
7319 app.service_selected = true;
7320 app.mode = Mode::Normal;
7321
7322 app.s3_state.buckets.items = vec![
7324 S3Bucket {
7325 name: "prod-bucket".to_string(),
7326 region: "us-east-1".to_string(),
7327 creation_date: "2023-01-01".to_string(),
7328 },
7329 S3Bucket {
7330 name: "dev-bucket".to_string(),
7331 region: "us-east-1".to_string(),
7332 creation_date: "2023-01-02".to_string(),
7333 },
7334 S3Bucket {
7335 name: "prod-logs".to_string(),
7336 region: "us-east-1".to_string(),
7337 creation_date: "2023-01-03".to_string(),
7338 },
7339 ];
7340
7341 app.s3_state.buckets.filter = "prod".to_string();
7343
7344 assert_eq!(app.s3_state.selected_row, 0);
7346
7347 app.handle_action(Action::NextItem);
7349 assert_eq!(app.s3_state.selected_row, 1);
7350
7351 app.handle_action(Action::NextItem);
7353 assert_eq!(app.s3_state.selected_row, 1);
7354
7355 app.handle_action(Action::PrevItem);
7357 assert_eq!(app.s3_state.selected_row, 0);
7358 }
7359
7360 #[test]
7361 fn test_next_preferences_lambda_functions() {
7362 let mut app = test_app();
7363 app.current_service = Service::LambdaFunctions;
7364 app.mode = Mode::ColumnSelector;
7365 app.column_selector_index = 0;
7366
7367 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
7368 app.handle_action(Action::NextPreferences);
7369 assert_eq!(app.column_selector_index, page_size_idx);
7370
7371 app.handle_action(Action::NextPreferences);
7372 assert_eq!(app.column_selector_index, 0);
7373 }
7374
7375 #[test]
7376 fn test_next_preferences_lambda_applications() {
7377 let mut app = test_app();
7378 app.current_service = Service::LambdaApplications;
7379 app.mode = Mode::ColumnSelector;
7380 app.column_selector_index = 0;
7381
7382 let page_size_idx = app.lambda_application_column_ids.len() + 2;
7383 app.handle_action(Action::NextPreferences);
7384 assert_eq!(app.column_selector_index, page_size_idx);
7385
7386 app.handle_action(Action::NextPreferences);
7387 assert_eq!(app.column_selector_index, 0);
7388 }
7389
7390 #[test]
7391 fn test_next_preferences_ecr_images() {
7392 let mut app = test_app();
7393 app.current_service = Service::EcrRepositories;
7394 app.ecr_state.current_repository = Some("test-repo".to_string());
7395 app.mode = Mode::ColumnSelector;
7396 app.column_selector_index = 0;
7397
7398 let page_size_idx = app.ecr_image_column_ids.len() + 2;
7399 app.handle_action(Action::NextPreferences);
7400 assert_eq!(app.column_selector_index, page_size_idx);
7401
7402 app.handle_action(Action::NextPreferences);
7403 assert_eq!(app.column_selector_index, 0);
7404 }
7405
7406 #[test]
7407 fn test_next_preferences_cloudwatch_log_groups() {
7408 let mut app = test_app();
7409 app.current_service = Service::CloudWatchLogGroups;
7410 app.view_mode = ViewMode::List;
7411 app.mode = Mode::ColumnSelector;
7412 app.column_selector_index = 0;
7413
7414 let page_size_idx = app.cw_log_group_column_ids.len() + 2;
7416 app.handle_action(Action::NextPreferences);
7417 assert_eq!(app.column_selector_index, page_size_idx);
7418
7419 app.handle_action(Action::NextPreferences);
7421 assert_eq!(app.column_selector_index, 0);
7422
7423 app.handle_action(Action::PrevPreferences);
7425 assert_eq!(app.column_selector_index, page_size_idx);
7426
7427 app.handle_action(Action::PrevPreferences);
7429 assert_eq!(app.column_selector_index, 0);
7430 }
7431
7432 #[test]
7433 fn test_next_preferences_cloudwatch_log_streams() {
7434 let mut app = test_app();
7435 app.current_service = Service::CloudWatchLogGroups;
7436 app.view_mode = ViewMode::Detail;
7437 app.mode = Mode::ColumnSelector;
7438 app.column_selector_index = 0;
7439
7440 let page_size_idx = app.cw_log_stream_column_ids.len() + 2;
7442 app.handle_action(Action::NextPreferences);
7443 assert_eq!(app.column_selector_index, page_size_idx);
7444
7445 app.handle_action(Action::NextPreferences);
7447 assert_eq!(app.column_selector_index, 0);
7448 }
7449
7450 #[test]
7451 fn test_cloudformation_next_item() {
7452 let mut app = test_app();
7453 app.current_service = Service::CloudFormationStacks;
7454 app.service_selected = true;
7455 app.mode = Mode::Normal;
7456 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7457 app.cfn_state.table.items = vec![
7458 CfnStack {
7459 name: "stack1".to_string(),
7460 stack_id: "id1".to_string(),
7461 status: "CREATE_COMPLETE".to_string(),
7462 created_time: "2024-01-01".to_string(),
7463 updated_time: String::new(),
7464 deleted_time: String::new(),
7465 drift_status: String::new(),
7466 last_drift_check_time: String::new(),
7467 status_reason: String::new(),
7468 description: String::new(),
7469 detailed_status: String::new(),
7470 root_stack: String::new(),
7471 parent_stack: String::new(),
7472 termination_protection: false,
7473 iam_role: String::new(),
7474 tags: Vec::new(),
7475 stack_policy: String::new(),
7476 rollback_monitoring_time: String::new(),
7477 rollback_alarms: Vec::new(),
7478 notification_arns: Vec::new(),
7479 },
7480 CfnStack {
7481 name: "stack2".to_string(),
7482 stack_id: "id2".to_string(),
7483 status: "UPDATE_COMPLETE".to_string(),
7484 created_time: "2024-01-02".to_string(),
7485 updated_time: String::new(),
7486 deleted_time: String::new(),
7487 drift_status: String::new(),
7488 last_drift_check_time: String::new(),
7489 status_reason: String::new(),
7490 description: String::new(),
7491 detailed_status: String::new(),
7492 root_stack: String::new(),
7493 parent_stack: String::new(),
7494 termination_protection: false,
7495 iam_role: String::new(),
7496 tags: Vec::new(),
7497 stack_policy: String::new(),
7498 rollback_monitoring_time: String::new(),
7499 rollback_alarms: Vec::new(),
7500 notification_arns: Vec::new(),
7501 },
7502 ];
7503 app.cfn_state.table.reset();
7504
7505 app.handle_action(Action::NextItem);
7506 assert_eq!(app.cfn_state.table.selected, 1);
7507
7508 app.handle_action(Action::NextItem);
7509 assert_eq!(app.cfn_state.table.selected, 1); }
7511
7512 #[test]
7513 fn test_cloudformation_prev_item() {
7514 let mut app = test_app();
7515 app.current_service = Service::CloudFormationStacks;
7516 app.service_selected = true;
7517 app.mode = Mode::Normal;
7518 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7519 app.cfn_state.table.items = vec![
7520 CfnStack {
7521 name: "stack1".to_string(),
7522 stack_id: "id1".to_string(),
7523 status: "CREATE_COMPLETE".to_string(),
7524 created_time: "2024-01-01".to_string(),
7525 updated_time: String::new(),
7526 deleted_time: String::new(),
7527 drift_status: String::new(),
7528 last_drift_check_time: String::new(),
7529 status_reason: String::new(),
7530 description: String::new(),
7531 detailed_status: String::new(),
7532 root_stack: String::new(),
7533 parent_stack: String::new(),
7534 termination_protection: false,
7535 iam_role: String::new(),
7536 tags: Vec::new(),
7537 stack_policy: String::new(),
7538 rollback_monitoring_time: String::new(),
7539 rollback_alarms: Vec::new(),
7540 notification_arns: Vec::new(),
7541 },
7542 CfnStack {
7543 name: "stack2".to_string(),
7544 stack_id: "id2".to_string(),
7545 status: "UPDATE_COMPLETE".to_string(),
7546 created_time: "2024-01-02".to_string(),
7547 updated_time: String::new(),
7548 deleted_time: String::new(),
7549 drift_status: String::new(),
7550 last_drift_check_time: String::new(),
7551 status_reason: String::new(),
7552 description: String::new(),
7553 detailed_status: String::new(),
7554 root_stack: String::new(),
7555 parent_stack: String::new(),
7556 termination_protection: false,
7557 iam_role: String::new(),
7558 tags: Vec::new(),
7559 stack_policy: String::new(),
7560 rollback_monitoring_time: String::new(),
7561 rollback_alarms: Vec::new(),
7562 notification_arns: Vec::new(),
7563 },
7564 ];
7565 app.cfn_state.table.selected = 1;
7566
7567 app.handle_action(Action::PrevItem);
7568 assert_eq!(app.cfn_state.table.selected, 0);
7569
7570 app.handle_action(Action::PrevItem);
7571 assert_eq!(app.cfn_state.table.selected, 0); }
7573
7574 #[test]
7575 fn test_cloudformation_page_down() {
7576 let mut app = test_app();
7577 app.current_service = Service::CloudFormationStacks;
7578 app.service_selected = true;
7579 app.mode = Mode::Normal;
7580 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7581
7582 for i in 0..20 {
7584 app.cfn_state.table.items.push(CfnStack {
7585 name: format!("stack{}", i),
7586 stack_id: format!("id{}", i),
7587 status: "CREATE_COMPLETE".to_string(),
7588 created_time: format!("2024-01-{:02}", i + 1),
7589 updated_time: String::new(),
7590 deleted_time: String::new(),
7591 drift_status: String::new(),
7592 last_drift_check_time: String::new(),
7593 status_reason: String::new(),
7594 description: String::new(),
7595 detailed_status: String::new(),
7596 root_stack: String::new(),
7597 parent_stack: String::new(),
7598 termination_protection: false,
7599 iam_role: String::new(),
7600 tags: Vec::new(),
7601 stack_policy: String::new(),
7602 rollback_monitoring_time: String::new(),
7603 rollback_alarms: Vec::new(),
7604 notification_arns: Vec::new(),
7605 });
7606 }
7607 app.cfn_state.table.reset();
7608
7609 app.handle_action(Action::PageDown);
7610 assert_eq!(app.cfn_state.table.selected, 10);
7611
7612 app.handle_action(Action::PageDown);
7613 assert_eq!(app.cfn_state.table.selected, 19); }
7615
7616 #[test]
7617 fn test_cloudformation_page_up() {
7618 let mut app = test_app();
7619 app.current_service = Service::CloudFormationStacks;
7620 app.service_selected = true;
7621 app.mode = Mode::Normal;
7622 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7623
7624 for i in 0..20 {
7626 app.cfn_state.table.items.push(CfnStack {
7627 name: format!("stack{}", i),
7628 stack_id: format!("id{}", i),
7629 status: "CREATE_COMPLETE".to_string(),
7630 created_time: format!("2024-01-{:02}", i + 1),
7631 updated_time: String::new(),
7632 deleted_time: String::new(),
7633 drift_status: String::new(),
7634 last_drift_check_time: String::new(),
7635 status_reason: String::new(),
7636 description: String::new(),
7637 detailed_status: String::new(),
7638 root_stack: String::new(),
7639 parent_stack: String::new(),
7640 termination_protection: false,
7641 iam_role: String::new(),
7642 tags: Vec::new(),
7643 stack_policy: String::new(),
7644 rollback_monitoring_time: String::new(),
7645 rollback_alarms: Vec::new(),
7646 notification_arns: Vec::new(),
7647 });
7648 }
7649 app.cfn_state.table.selected = 15;
7650
7651 app.handle_action(Action::PageUp);
7652 assert_eq!(app.cfn_state.table.selected, 5);
7653
7654 app.handle_action(Action::PageUp);
7655 assert_eq!(app.cfn_state.table.selected, 0); }
7657
7658 #[test]
7659 fn test_cloudformation_filter_input() {
7660 let mut app = test_app();
7661 app.current_service = Service::CloudFormationStacks;
7662 app.service_selected = true;
7663 app.mode = Mode::Normal;
7664
7665 app.handle_action(Action::StartFilter);
7666 assert_eq!(app.mode, Mode::FilterInput);
7667
7668 app.cfn_state.table.filter = "test".to_string();
7670 assert_eq!(app.cfn_state.table.filter, "test");
7671 }
7672
7673 #[test]
7674 fn test_cloudformation_filter_applies() {
7675 let mut app = test_app();
7676 app.current_service = Service::CloudFormationStacks;
7677 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7678 app.cfn_state.table.items = vec![
7679 CfnStack {
7680 name: "prod-stack".to_string(),
7681 stack_id: "id1".to_string(),
7682 status: "CREATE_COMPLETE".to_string(),
7683 created_time: "2024-01-01".to_string(),
7684 updated_time: String::new(),
7685 deleted_time: String::new(),
7686 drift_status: String::new(),
7687 last_drift_check_time: String::new(),
7688 status_reason: String::new(),
7689 description: "Production stack".to_string(),
7690 detailed_status: String::new(),
7691 root_stack: String::new(),
7692 parent_stack: String::new(),
7693 termination_protection: false,
7694 iam_role: String::new(),
7695 tags: Vec::new(),
7696 stack_policy: String::new(),
7697 rollback_monitoring_time: String::new(),
7698 rollback_alarms: Vec::new(),
7699 notification_arns: Vec::new(),
7700 },
7701 CfnStack {
7702 name: "dev-stack".to_string(),
7703 stack_id: "id2".to_string(),
7704 status: "UPDATE_COMPLETE".to_string(),
7705 created_time: "2024-01-02".to_string(),
7706 updated_time: String::new(),
7707 deleted_time: String::new(),
7708 drift_status: String::new(),
7709 last_drift_check_time: String::new(),
7710 status_reason: String::new(),
7711 description: "Development stack".to_string(),
7712 detailed_status: String::new(),
7713 root_stack: String::new(),
7714 parent_stack: String::new(),
7715 termination_protection: false,
7716 iam_role: String::new(),
7717 tags: Vec::new(),
7718 stack_policy: String::new(),
7719 rollback_monitoring_time: String::new(),
7720 rollback_alarms: Vec::new(),
7721 notification_arns: Vec::new(),
7722 },
7723 ];
7724 app.cfn_state.table.filter = "prod".to_string();
7725
7726 let filtered = filtered_cloudformation_stacks(&app);
7727 assert_eq!(filtered.len(), 1);
7728 assert_eq!(filtered[0].name, "prod-stack");
7729 }
7730
7731 #[test]
7732 fn test_cloudformation_right_arrow_expands() {
7733 let mut app = test_app();
7734 app.current_service = Service::CloudFormationStacks;
7735 app.service_selected = true;
7736 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7737 app.cfn_state.table.items = vec![CfnStack {
7738 name: "test-stack".to_string(),
7739 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
7740 .to_string(),
7741 status: "CREATE_COMPLETE".to_string(),
7742 created_time: "2024-01-01".to_string(),
7743 updated_time: String::new(),
7744 deleted_time: String::new(),
7745 drift_status: String::new(),
7746 last_drift_check_time: String::new(),
7747 status_reason: String::new(),
7748 description: "Test stack".to_string(),
7749 detailed_status: String::new(),
7750 root_stack: String::new(),
7751 parent_stack: String::new(),
7752 termination_protection: false,
7753 iam_role: String::new(),
7754 tags: Vec::new(),
7755 stack_policy: String::new(),
7756 rollback_monitoring_time: String::new(),
7757 rollback_alarms: Vec::new(),
7758 notification_arns: Vec::new(),
7759 }];
7760 app.cfn_state.table.reset();
7761
7762 assert_eq!(app.cfn_state.table.expanded_item, None);
7763
7764 app.handle_action(Action::NextPane);
7765 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
7766 }
7767
7768 #[test]
7769 fn test_cloudformation_left_arrow_collapses() {
7770 let mut app = test_app();
7771 app.current_service = Service::CloudFormationStacks;
7772 app.service_selected = true;
7773 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7774 app.cfn_state.table.items = vec![CfnStack {
7775 name: "test-stack".to_string(),
7776 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
7777 .to_string(),
7778 status: "CREATE_COMPLETE".to_string(),
7779 created_time: "2024-01-01".to_string(),
7780 updated_time: String::new(),
7781 deleted_time: String::new(),
7782 drift_status: String::new(),
7783 last_drift_check_time: String::new(),
7784 status_reason: String::new(),
7785 description: "Test stack".to_string(),
7786 detailed_status: String::new(),
7787 root_stack: String::new(),
7788 parent_stack: String::new(),
7789 termination_protection: false,
7790 iam_role: String::new(),
7791 tags: Vec::new(),
7792 stack_policy: String::new(),
7793 rollback_monitoring_time: String::new(),
7794 rollback_alarms: Vec::new(),
7795 notification_arns: Vec::new(),
7796 }];
7797 app.cfn_state.table.reset();
7798 app.cfn_state.table.expanded_item = Some(0);
7799
7800 app.handle_action(Action::PrevPane);
7801 assert_eq!(app.cfn_state.table.expanded_item, None);
7802 }
7803
7804 #[test]
7805 fn test_cloudformation_enter_drills_into_stack() {
7806 let mut app = test_app();
7807 app.current_service = Service::CloudFormationStacks;
7808 app.service_selected = true;
7809 app.mode = Mode::Normal;
7810 app.tabs = vec![Tab {
7811 service: Service::CloudFormationStacks,
7812 title: "CloudFormation › Stacks".to_string(),
7813 breadcrumb: "CloudFormation › Stacks".to_string(),
7814 }];
7815 app.current_tab = 0;
7816 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7817 app.cfn_state.table.items = vec![CfnStack {
7818 name: "test-stack".to_string(),
7819 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
7820 .to_string(),
7821 status: "CREATE_COMPLETE".to_string(),
7822 created_time: "2024-01-01".to_string(),
7823 updated_time: String::new(),
7824 deleted_time: String::new(),
7825 drift_status: String::new(),
7826 last_drift_check_time: String::new(),
7827 status_reason: String::new(),
7828 description: "Test stack".to_string(),
7829 detailed_status: String::new(),
7830 root_stack: String::new(),
7831 parent_stack: String::new(),
7832 termination_protection: false,
7833 iam_role: String::new(),
7834 tags: Vec::new(),
7835 stack_policy: String::new(),
7836 rollback_monitoring_time: String::new(),
7837 rollback_alarms: Vec::new(),
7838 notification_arns: Vec::new(),
7839 }];
7840 app.cfn_state.table.reset();
7841
7842 let filtered = filtered_cloudformation_stacks(&app);
7844 assert_eq!(filtered.len(), 1);
7845 assert_eq!(filtered[0].name, "test-stack");
7846
7847 assert_eq!(app.cfn_state.current_stack, None);
7848
7849 app.handle_action(Action::Select);
7851 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
7852 }
7853
7854 #[test]
7855 fn test_cloudformation_copy_to_clipboard() {
7856 let mut app = test_app();
7857 app.current_service = Service::CloudFormationStacks;
7858 app.service_selected = true;
7859 app.mode = Mode::Normal;
7860 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7861 app.cfn_state.table.items = vec![
7862 CfnStack {
7863 name: "stack1".to_string(),
7864 stack_id: "id1".to_string(),
7865 status: "CREATE_COMPLETE".to_string(),
7866 created_time: "2024-01-01".to_string(),
7867 updated_time: String::new(),
7868 deleted_time: String::new(),
7869 drift_status: String::new(),
7870 last_drift_check_time: String::new(),
7871 status_reason: String::new(),
7872 description: String::new(),
7873 detailed_status: String::new(),
7874 root_stack: String::new(),
7875 parent_stack: String::new(),
7876 termination_protection: false,
7877 iam_role: String::new(),
7878 tags: Vec::new(),
7879 stack_policy: String::new(),
7880 rollback_monitoring_time: String::new(),
7881 rollback_alarms: Vec::new(),
7882 notification_arns: Vec::new(),
7883 },
7884 CfnStack {
7885 name: "stack2".to_string(),
7886 stack_id: "id2".to_string(),
7887 status: "UPDATE_COMPLETE".to_string(),
7888 created_time: "2024-01-02".to_string(),
7889 updated_time: String::new(),
7890 deleted_time: String::new(),
7891 drift_status: String::new(),
7892 last_drift_check_time: String::new(),
7893 status_reason: String::new(),
7894 description: String::new(),
7895 detailed_status: String::new(),
7896 root_stack: String::new(),
7897 parent_stack: String::new(),
7898 termination_protection: false,
7899 iam_role: String::new(),
7900 tags: Vec::new(),
7901 stack_policy: String::new(),
7902 rollback_monitoring_time: String::new(),
7903 rollback_alarms: Vec::new(),
7904 notification_arns: Vec::new(),
7905 },
7906 ];
7907
7908 assert!(!app.clipboard.snapshot_requested());
7909 app.handle_action(Action::CopyToClipboard);
7910
7911 assert!(app.clipboard.snapshot_requested());
7913 }
7914
7915 #[test]
7916 fn test_cloudformation_expansion_shows_all_visible_columns() {
7917 let mut app = test_app();
7918 app.current_service = Service::CloudFormationStacks;
7919 app.cfn_state.status_filter = CfnStatusFilter::Complete;
7920 app.cfn_state.table.items = vec![CfnStack {
7921 name: "test-stack".to_string(),
7922 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
7923 .to_string(),
7924 status: "CREATE_COMPLETE".to_string(),
7925 created_time: "2024-01-01".to_string(),
7926 updated_time: "2024-01-02".to_string(),
7927 deleted_time: String::new(),
7928 drift_status: "IN_SYNC".to_string(),
7929 last_drift_check_time: "2024-01-03".to_string(),
7930 status_reason: String::new(),
7931 description: "Test description".to_string(),
7932 detailed_status: String::new(),
7933 root_stack: String::new(),
7934 parent_stack: String::new(),
7935 termination_protection: false,
7936 iam_role: String::new(),
7937 tags: Vec::new(),
7938 stack_policy: String::new(),
7939 rollback_monitoring_time: String::new(),
7940 rollback_alarms: Vec::new(),
7941 notification_arns: Vec::new(),
7942 }];
7943
7944 app.cfn_visible_column_ids = [
7946 CfnColumn::Name,
7947 CfnColumn::Status,
7948 CfnColumn::CreatedTime,
7949 CfnColumn::Description,
7950 ]
7951 .iter()
7952 .map(|c| c.id())
7953 .collect();
7954
7955 app.cfn_state.table.expanded_item = Some(0);
7956
7957 assert_eq!(app.cfn_visible_column_ids.len(), 4);
7960 assert!(app.cfn_state.table.has_expanded_item());
7961 }
7962
7963 #[test]
7964 fn test_cloudformation_empty_list_shows_page_1() {
7965 let mut app = test_app();
7966 app.current_service = Service::CloudFormationStacks;
7967 app.cfn_state.table.items = vec![];
7968
7969 let filtered = filtered_cloudformation_stacks(&app);
7970 assert_eq!(filtered.len(), 0);
7971
7972 let page_size = app.cfn_state.table.page_size.value();
7974 let total_pages = filtered.len().div_ceil(page_size);
7975 assert_eq!(total_pages, 0);
7976
7977 }
7980}
7981
7982impl App {
7983 pub fn get_filtered_regions(&self) -> Vec<AwsRegion> {
7984 let mut all = AwsRegion::all();
7985
7986 for region in &mut all {
7988 region.latency_ms = self.region_latencies.get(region.code).copied();
7989 }
7990
7991 let filtered: Vec<AwsRegion> = if self.region_filter.is_empty() {
7993 all
7994 } else {
7995 let filter_lower = self.region_filter.to_lowercase();
7996 all.into_iter()
7997 .filter(|r| {
7998 r.name.to_lowercase().contains(&filter_lower)
7999 || r.code.to_lowercase().contains(&filter_lower)
8000 || r.group.to_lowercase().contains(&filter_lower)
8001 })
8002 .collect()
8003 };
8004
8005 let mut sorted = filtered;
8007 sorted.sort_by_key(|r| r.latency_ms.unwrap_or(1000));
8008 sorted
8009 }
8010
8011 pub fn measure_region_latencies(&mut self) {
8012 use std::time::Instant;
8013 self.region_latencies.clear();
8014
8015 let regions = AwsRegion::all();
8016 let start_all = Instant::now();
8017 tracing::info!("Starting latency measurement for {} regions", regions.len());
8018
8019 let handles: Vec<_> = regions
8020 .iter()
8021 .map(|region| {
8022 let code = region.code.to_string();
8023 std::thread::spawn(move || {
8024 let endpoint = format!("https://sts.{}.amazonaws.com", code);
8026 let start = Instant::now();
8027
8028 match ureq::get(&endpoint)
8029 .config()
8030 .timeout_global(Some(std::time::Duration::from_secs(2)))
8031 .build()
8032 .call()
8033 {
8034 Ok(_) => {
8035 let latency = start.elapsed().as_millis() as u64;
8036 Some((code, latency))
8037 }
8038 Err(e) => {
8039 tracing::debug!("Failed to measure {}: {}", code, e);
8040 Some((code, 9999))
8041 }
8042 }
8043 })
8044 })
8045 .collect();
8046
8047 for handle in handles {
8048 if let Ok(Some((code, latency))) = handle.join() {
8049 self.region_latencies.insert(code, latency);
8050 }
8051 }
8052
8053 tracing::info!(
8054 "Measured {} regions in {:?}",
8055 self.region_latencies.len(),
8056 start_all.elapsed()
8057 );
8058 }
8059
8060 pub fn get_filtered_profiles(&self) -> Vec<&AwsProfile> {
8061 filter_profiles(&self.available_profiles, &self.profile_filter)
8062 }
8063
8064 pub fn get_filtered_sessions(&self) -> Vec<&Session> {
8065 if self.session_filter.is_empty() {
8066 return self.sessions.iter().collect();
8067 }
8068 let filter_lower = self.session_filter.to_lowercase();
8069 self.sessions
8070 .iter()
8071 .filter(|s| {
8072 s.profile.to_lowercase().contains(&filter_lower)
8073 || s.region.to_lowercase().contains(&filter_lower)
8074 || s.account_id.to_lowercase().contains(&filter_lower)
8075 || s.role_arn.to_lowercase().contains(&filter_lower)
8076 })
8077 .collect()
8078 }
8079
8080 pub fn get_filtered_tabs(&self) -> Vec<(usize, &Tab)> {
8081 if self.tab_filter.is_empty() {
8082 return self.tabs.iter().enumerate().collect();
8083 }
8084 let filter_lower = self.tab_filter.to_lowercase();
8085 self.tabs
8086 .iter()
8087 .enumerate()
8088 .filter(|(_, tab)| {
8089 tab.title.to_lowercase().contains(&filter_lower)
8090 || tab.breadcrumb.to_lowercase().contains(&filter_lower)
8091 })
8092 .collect()
8093 }
8094
8095 pub fn load_aws_profiles() -> Vec<AwsProfile> {
8096 AwsProfile::load_all()
8097 }
8098
8099 pub async fn fetch_profile_accounts(&mut self) {
8100 for profile in &mut self.available_profiles {
8101 if profile.account.is_none() {
8102 let region = profile
8103 .region
8104 .clone()
8105 .unwrap_or_else(|| "us-east-1".to_string());
8106 if let Ok(account) =
8107 rusticity_core::AwsConfig::get_account_for_profile(&profile.name, ®ion).await
8108 {
8109 profile.account = Some(account);
8110 }
8111 }
8112 }
8113 }
8114
8115 fn save_current_session(&mut self) {
8116 if self.tabs.is_empty() {
8118 if let Some(ref session) = self.current_session {
8119 let _ = session.delete();
8120 self.current_session = None;
8121 }
8122 return;
8123 }
8124
8125 let session = if let Some(ref mut current) = self.current_session {
8126 current.tabs = self
8128 .tabs
8129 .iter()
8130 .map(|t| SessionTab {
8131 service: format!("{:?}", t.service),
8132 title: t.title.clone(),
8133 breadcrumb: t.breadcrumb.clone(),
8134 filter: match t.service {
8135 Service::CloudWatchLogGroups => {
8136 Some(self.log_groups_state.log_groups.filter.clone())
8137 }
8138 _ => None,
8139 },
8140 selected_item: None,
8141 })
8142 .collect();
8143 current.clone()
8144 } else {
8145 let mut session = Session::new(
8147 self.profile.clone(),
8148 self.region.clone(),
8149 self.config.account_id.clone(),
8150 self.config.role_arn.clone(),
8151 );
8152 session.tabs = self
8153 .tabs
8154 .iter()
8155 .map(|t| SessionTab {
8156 service: format!("{:?}", t.service),
8157 title: t.title.clone(),
8158 breadcrumb: t.breadcrumb.clone(),
8159 filter: match t.service {
8160 Service::CloudWatchLogGroups => {
8161 Some(self.log_groups_state.log_groups.filter.clone())
8162 }
8163 _ => None,
8164 },
8165 selected_item: None,
8166 })
8167 .collect();
8168 self.current_session = Some(session.clone());
8169 session
8170 };
8171
8172 let _ = session.save();
8173 }
8174}
8175
8176#[cfg(test)]
8177mod iam_policy_view_tests {
8178 use super::*;
8179 use test_helpers::*;
8180
8181 #[test]
8182 fn test_enter_opens_policy_view() {
8183 let mut app = test_app();
8184 app.current_service = Service::IamRoles;
8185 app.service_selected = true;
8186 app.mode = Mode::Normal;
8187 app.view_mode = ViewMode::Detail;
8188 app.iam_state.current_role = Some("TestRole".to_string());
8189 app.iam_state.policies.items = vec![IamPolicy {
8190 policy_name: "TestPolicy".to_string(),
8191 policy_type: "Inline".to_string(),
8192 attached_via: "Direct".to_string(),
8193 attached_entities: "1".to_string(),
8194 description: "Test".to_string(),
8195 creation_time: "2023-01-01".to_string(),
8196 edited_time: "2023-01-01".to_string(),
8197 policy_arn: None,
8198 }];
8199 app.iam_state.policies.reset();
8200
8201 app.handle_action(Action::Select);
8202
8203 assert_eq!(app.view_mode, ViewMode::PolicyView);
8204 assert_eq!(app.iam_state.current_policy, Some("TestPolicy".to_string()));
8205 assert_eq!(app.iam_state.policy_scroll, 0);
8206 assert!(app.iam_state.policies.loading);
8207 }
8208
8209 #[test]
8210 fn test_escape_closes_policy_view() {
8211 let mut app = test_app();
8212 app.current_service = Service::IamRoles;
8213 app.service_selected = true;
8214 app.mode = Mode::Normal;
8215 app.view_mode = ViewMode::PolicyView;
8216 app.iam_state.current_role = Some("TestRole".to_string());
8217 app.iam_state.current_policy = Some("TestPolicy".to_string());
8218 app.iam_state.policy_document = "{\n \"test\": \"value\"\n}".to_string();
8219 app.iam_state.policy_scroll = 5;
8220
8221 app.handle_action(Action::PrevPane);
8222
8223 assert_eq!(app.view_mode, ViewMode::Detail);
8224 assert_eq!(app.iam_state.current_policy, None);
8225 assert_eq!(app.iam_state.policy_document, "");
8226 assert_eq!(app.iam_state.policy_scroll, 0);
8227 }
8228
8229 #[test]
8230 fn test_ctrl_d_scrolls_down_in_policy_view() {
8231 let mut app = test_app();
8232 app.current_service = Service::IamRoles;
8233 app.service_selected = true;
8234 app.mode = Mode::Normal;
8235 app.view_mode = ViewMode::PolicyView;
8236 app.iam_state.current_role = Some("TestRole".to_string());
8237 app.iam_state.current_policy = Some("TestPolicy".to_string());
8238 app.iam_state.policy_document = (0..100)
8239 .map(|i| format!("line {}", i))
8240 .collect::<Vec<_>>()
8241 .join("\n");
8242 app.iam_state.policy_scroll = 0;
8243
8244 app.handle_action(Action::ScrollDown);
8245
8246 assert_eq!(app.iam_state.policy_scroll, 10);
8247
8248 app.handle_action(Action::ScrollDown);
8249
8250 assert_eq!(app.iam_state.policy_scroll, 20);
8251 }
8252
8253 #[test]
8254 fn test_ctrl_u_scrolls_up_in_policy_view() {
8255 let mut app = test_app();
8256 app.current_service = Service::IamRoles;
8257 app.service_selected = true;
8258 app.mode = Mode::Normal;
8259 app.view_mode = ViewMode::PolicyView;
8260 app.iam_state.current_role = Some("TestRole".to_string());
8261 app.iam_state.current_policy = Some("TestPolicy".to_string());
8262 app.iam_state.policy_document = (0..100)
8263 .map(|i| format!("line {}", i))
8264 .collect::<Vec<_>>()
8265 .join("\n");
8266 app.iam_state.policy_scroll = 30;
8267
8268 app.handle_action(Action::ScrollUp);
8269
8270 assert_eq!(app.iam_state.policy_scroll, 20);
8271
8272 app.handle_action(Action::ScrollUp);
8273
8274 assert_eq!(app.iam_state.policy_scroll, 10);
8275 }
8276
8277 #[test]
8278 fn test_scroll_does_not_go_negative() {
8279 let mut app = test_app();
8280 app.current_service = Service::IamRoles;
8281 app.service_selected = true;
8282 app.mode = Mode::Normal;
8283 app.view_mode = ViewMode::PolicyView;
8284 app.iam_state.current_role = Some("TestRole".to_string());
8285 app.iam_state.current_policy = Some("TestPolicy".to_string());
8286 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
8287 app.iam_state.policy_scroll = 0;
8288
8289 app.handle_action(Action::ScrollUp);
8290
8291 assert_eq!(app.iam_state.policy_scroll, 0);
8292 }
8293
8294 #[test]
8295 fn test_scroll_does_not_exceed_max() {
8296 let mut app = test_app();
8297 app.current_service = Service::IamRoles;
8298 app.service_selected = true;
8299 app.mode = Mode::Normal;
8300 app.view_mode = ViewMode::PolicyView;
8301 app.iam_state.current_role = Some("TestRole".to_string());
8302 app.iam_state.current_policy = Some("TestPolicy".to_string());
8303 app.iam_state.policy_document = "line 1\nline 2\nline 3".to_string();
8304 app.iam_state.policy_scroll = 0;
8305
8306 app.handle_action(Action::ScrollDown);
8307
8308 assert_eq!(app.iam_state.policy_scroll, 2); }
8310
8311 #[test]
8312 fn test_policy_view_console_url() {
8313 let mut app = test_app();
8314 app.current_service = Service::IamRoles;
8315 app.service_selected = true;
8316 app.view_mode = ViewMode::PolicyView;
8317 app.iam_state.current_role = Some("TestRole".to_string());
8318 app.iam_state.current_policy = Some("TestPolicy".to_string());
8319
8320 let url = app.get_console_url();
8321
8322 assert!(url.contains("us-east-1.console.aws.amazon.com"));
8323 assert!(url.contains("/roles/details/TestRole"));
8324 assert!(url.contains("/editPolicy/TestPolicy"));
8325 assert!(url.contains("step=addPermissions"));
8326 }
8327
8328 #[test]
8329 fn test_esc_from_policy_view_goes_to_role_detail() {
8330 let mut app = test_app();
8331 app.current_service = Service::IamRoles;
8332 app.service_selected = true;
8333 app.mode = Mode::Normal;
8334 app.view_mode = ViewMode::PolicyView;
8335 app.iam_state.current_role = Some("TestRole".to_string());
8336 app.iam_state.current_policy = Some("TestPolicy".to_string());
8337 app.iam_state.policy_document = "test".to_string();
8338 app.iam_state.policy_scroll = 5;
8339
8340 app.handle_action(Action::GoBack);
8341
8342 assert_eq!(app.view_mode, ViewMode::Detail);
8343 assert_eq!(app.iam_state.current_policy, None);
8344 assert_eq!(app.iam_state.policy_document, "");
8345 assert_eq!(app.iam_state.policy_scroll, 0);
8346 assert_eq!(app.iam_state.current_role, Some("TestRole".to_string()));
8347 }
8348
8349 #[test]
8350 fn test_esc_from_role_detail_goes_to_role_list() {
8351 let mut app = test_app();
8352 app.current_service = Service::IamRoles;
8353 app.service_selected = true;
8354 app.mode = Mode::Normal;
8355 app.view_mode = ViewMode::Detail;
8356 app.iam_state.current_role = Some("TestRole".to_string());
8357
8358 app.handle_action(Action::GoBack);
8359
8360 assert_eq!(app.iam_state.current_role, None);
8361 }
8362
8363 #[test]
8364 fn test_right_arrow_expands_policy_row() {
8365 let mut app = test_app();
8366 app.current_service = Service::IamRoles;
8367 app.service_selected = true;
8368 app.mode = Mode::Normal;
8369 app.view_mode = ViewMode::Detail;
8370 app.iam_state.current_role = Some("TestRole".to_string());
8371 app.iam_state.policies.items = vec![IamPolicy {
8372 policy_name: "TestPolicy".to_string(),
8373 policy_type: "Inline".to_string(),
8374 attached_via: "Direct".to_string(),
8375 attached_entities: "1".to_string(),
8376 description: "Test".to_string(),
8377 creation_time: "2023-01-01".to_string(),
8378 edited_time: "2023-01-01".to_string(),
8379 policy_arn: None,
8380 }];
8381 app.iam_state.policies.reset();
8382
8383 app.handle_action(Action::NextPane);
8384
8385 assert_eq!(app.view_mode, ViewMode::Detail);
8387 assert_eq!(app.iam_state.current_policy, None);
8388 assert_eq!(app.iam_state.policies.expanded_item, Some(0));
8389 }
8390
8391 #[test]
8392 fn test_managed_policy_has_creation_and_edit_time() {
8393 use crate::iam::Policy as IamPolicy;
8397 let managed = IamPolicy {
8398 policy_name: "MyManagedPolicy".to_string(),
8399 policy_type: "Managed".to_string(),
8400 attached_via: "Direct".to_string(),
8401 attached_entities: "-".to_string(),
8402 description: "-".to_string(),
8403 creation_time: "2024-01-01 00:00:00 (UTC)".to_string(),
8404 edited_time: "2024-06-01 00:00:00 (UTC)".to_string(),
8405 policy_arn: Some("arn:aws:iam::123:policy/MyManagedPolicy".to_string()),
8406 };
8407 assert_ne!(
8408 managed.creation_time, "-",
8409 "Managed policy must have a real creation time, not '-'"
8410 );
8411 assert_ne!(
8412 managed.edited_time, "-",
8413 "Managed policy must have a real edit time, not '-'"
8414 );
8415
8416 let inline = IamPolicy {
8418 policy_name: "InlinePolicy".to_string(),
8419 policy_type: "Inline".to_string(),
8420 attached_via: "Direct".to_string(),
8421 attached_entities: "-".to_string(),
8422 description: "-".to_string(),
8423 creation_time: "-".to_string(),
8424 edited_time: "-".to_string(),
8425 policy_arn: None,
8426 };
8427 assert_eq!(
8428 inline.creation_time, "-",
8429 "Inline policies have no creation time"
8430 );
8431 }
8432
8433 #[test]
8434 fn test_yank_in_policy_view_copies_document_not_arn() {
8435 let mut app = test_app();
8437 app.current_service = Service::IamRoles;
8438 app.service_selected = true;
8439 app.mode = Mode::Normal;
8440 app.view_mode = ViewMode::PolicyView;
8441 app.iam_state.current_role = Some("TestRole".to_string());
8442 app.iam_state.current_policy = Some("TestPolicy".to_string());
8443
8444 let policy_json = "{\n \"Version\": \"2012-10-17\",\n \"Statement\": []\n}".to_string();
8445 app.iam_state.policy_document = policy_json.clone();
8446
8447 use crate::iam::actions::yank_roles;
8449 yank_roles(&app);
8450
8451 assert_eq!(app.iam_state.policy_document, policy_json);
8453 assert_eq!(app.view_mode, ViewMode::PolicyView);
8454 }
8455}
8456
8457#[cfg(test)]
8458mod tab_filter_tests {
8459 use super::*;
8460 use test_helpers::*;
8461
8462 #[test]
8463 fn test_space_t_opens_tab_picker() {
8464 let mut app = test_app();
8465 app.tabs = vec![
8466 Tab {
8467 service: Service::CloudWatchLogGroups,
8468 title: "Tab 1".to_string(),
8469 breadcrumb: "CloudWatch > Log groups".to_string(),
8470 },
8471 Tab {
8472 service: Service::S3Buckets,
8473 title: "Tab 2".to_string(),
8474 breadcrumb: "S3 › Buckets".to_string(),
8475 },
8476 ];
8477 app.current_tab = 0;
8478
8479 app.handle_action(Action::OpenTabPicker);
8480
8481 assert_eq!(app.mode, Mode::TabPicker);
8482 assert_eq!(app.tab_picker_selected, 0);
8483 }
8484
8485 #[test]
8486 fn test_tab_filter_works() {
8487 let mut app = test_app();
8488 app.tabs = vec![
8489 Tab {
8490 service: Service::CloudWatchLogGroups,
8491 title: "CloudWatch Logs".to_string(),
8492 breadcrumb: "CloudWatch > Log groups".to_string(),
8493 },
8494 Tab {
8495 service: Service::S3Buckets,
8496 title: "S3 Buckets".to_string(),
8497 breadcrumb: "S3 › Buckets".to_string(),
8498 },
8499 Tab {
8500 service: Service::CloudWatchAlarms,
8501 title: "CloudWatch Alarms".to_string(),
8502 breadcrumb: "CloudWatch › Alarms".to_string(),
8503 },
8504 ];
8505 app.mode = Mode::TabPicker;
8506
8507 app.handle_action(Action::FilterInput('s'));
8509 app.handle_action(Action::FilterInput('3'));
8510
8511 let filtered = app.get_filtered_tabs();
8512 assert_eq!(filtered.len(), 1);
8513 assert_eq!(filtered[0].1.title, "S3 Buckets");
8514 }
8515
8516 #[test]
8517 fn test_tab_filter_by_breadcrumb() {
8518 let mut app = test_app();
8519 app.tabs = vec![
8520 Tab {
8521 service: Service::CloudWatchLogGroups,
8522 title: "Tab 1".to_string(),
8523 breadcrumb: "CloudWatch > Log groups".to_string(),
8524 },
8525 Tab {
8526 service: Service::S3Buckets,
8527 title: "Tab 2".to_string(),
8528 breadcrumb: "S3 › Buckets".to_string(),
8529 },
8530 ];
8531 app.mode = Mode::TabPicker;
8532
8533 app.handle_action(Action::FilterInput('c'));
8535 app.handle_action(Action::FilterInput('l'));
8536 app.handle_action(Action::FilterInput('o'));
8537 app.handle_action(Action::FilterInput('u'));
8538 app.handle_action(Action::FilterInput('d'));
8539
8540 let filtered = app.get_filtered_tabs();
8541 assert_eq!(filtered.len(), 1);
8542 assert_eq!(filtered[0].1.breadcrumb, "CloudWatch > Log groups");
8543 }
8544
8545 #[test]
8546 fn test_tab_filter_backspace() {
8547 let mut app = test_app();
8548 app.tabs = vec![
8549 Tab {
8550 service: Service::CloudWatchLogGroups,
8551 title: "CloudWatch Logs".to_string(),
8552 breadcrumb: "CloudWatch > Log groups".to_string(),
8553 },
8554 Tab {
8555 service: Service::S3Buckets,
8556 title: "S3 Buckets".to_string(),
8557 breadcrumb: "S3 › Buckets".to_string(),
8558 },
8559 ];
8560 app.mode = Mode::TabPicker;
8561
8562 app.handle_action(Action::FilterInput('s'));
8563 app.handle_action(Action::FilterInput('3'));
8564 assert_eq!(app.tab_filter, "s3");
8565
8566 app.handle_action(Action::FilterBackspace);
8567 assert_eq!(app.tab_filter, "s");
8568
8569 let filtered = app.get_filtered_tabs();
8570 assert_eq!(filtered.len(), 2); }
8572
8573 #[test]
8574 fn test_tab_selection_with_filter() {
8575 let mut app = test_app();
8576 app.tabs = vec![
8577 Tab {
8578 service: Service::CloudWatchLogGroups,
8579 title: "CloudWatch Logs".to_string(),
8580 breadcrumb: "CloudWatch > Log groups".to_string(),
8581 },
8582 Tab {
8583 service: Service::S3Buckets,
8584 title: "S3 Buckets".to_string(),
8585 breadcrumb: "S3 › Buckets".to_string(),
8586 },
8587 ];
8588 app.mode = Mode::TabPicker;
8589 app.current_tab = 0;
8590
8591 app.handle_action(Action::FilterInput('s'));
8593 app.handle_action(Action::FilterInput('3'));
8594
8595 app.handle_action(Action::Select);
8597
8598 assert_eq!(app.current_tab, 1); assert_eq!(app.mode, Mode::Normal);
8600 assert_eq!(app.tab_filter, ""); }
8602}
8603
8604#[cfg(test)]
8605mod region_latency_tests {
8606 use super::*;
8607 use test_helpers::*;
8608
8609 #[test]
8610 fn test_regions_sorted_by_latency() {
8611 let mut app = test_app();
8612
8613 app.region_latencies.insert("us-west-2".to_string(), 50);
8615 app.region_latencies.insert("us-east-1".to_string(), 10);
8616 app.region_latencies.insert("eu-west-1".to_string(), 100);
8617
8618 let filtered = app.get_filtered_regions();
8619
8620 let with_latency: Vec<_> = filtered.iter().filter(|r| r.latency_ms.is_some()).collect();
8622
8623 assert!(with_latency.len() >= 3);
8624 assert_eq!(with_latency[0].code, "us-east-1");
8625 assert_eq!(with_latency[0].latency_ms, Some(10));
8626 assert_eq!(with_latency[1].code, "us-west-2");
8627 assert_eq!(with_latency[1].latency_ms, Some(50));
8628 assert_eq!(with_latency[2].code, "eu-west-1");
8629 assert_eq!(with_latency[2].latency_ms, Some(100));
8630 }
8631
8632 #[test]
8633 fn test_regions_with_latency_before_without() {
8634 let mut app = test_app();
8635
8636 app.region_latencies.insert("eu-west-1".to_string(), 100);
8638
8639 let filtered = app.get_filtered_regions();
8640
8641 assert_eq!(filtered[0].code, "eu-west-1");
8643 assert_eq!(filtered[0].latency_ms, Some(100));
8644
8645 for region in &filtered[1..] {
8647 assert!(region.latency_ms.is_none());
8648 }
8649 }
8650
8651 #[test]
8652 fn test_region_filter_with_latency() {
8653 let mut app = test_app();
8654
8655 app.region_latencies.insert("us-east-1".to_string(), 10);
8656 app.region_latencies.insert("us-west-2".to_string(), 50);
8657 app.region_filter = "us".to_string();
8658
8659 let filtered = app.get_filtered_regions();
8660
8661 assert!(filtered.iter().all(|r| r.code.starts_with("us-")));
8663 assert_eq!(filtered[0].code, "us-east-1");
8664 assert_eq!(filtered[1].code, "us-west-2");
8665 }
8666
8667 #[test]
8668 fn test_latency_persists_across_filters() {
8669 let mut app = test_app();
8670
8671 app.region_latencies.insert("us-east-1".to_string(), 10);
8672
8673 app.region_filter = "eu".to_string();
8675 let filtered = app.get_filtered_regions();
8676 assert!(filtered.iter().all(|r| !r.code.starts_with("us-")));
8677
8678 app.region_filter.clear();
8680 let all = app.get_filtered_regions();
8681
8682 let us_east = all.iter().find(|r| r.code == "us-east-1").unwrap();
8684 assert_eq!(us_east.latency_ms, Some(10));
8685 }
8686
8687 #[test]
8688 fn test_measure_region_latencies_clears_previous() {
8689 let mut app = test_app();
8690
8691 app.region_latencies.insert("us-east-1".to_string(), 100);
8693 app.region_latencies.insert("eu-west-1".to_string(), 200);
8694
8695 app.measure_region_latencies();
8697
8698 assert!(
8700 app.region_latencies.is_empty() || !app.region_latencies.contains_key("fake-region")
8701 );
8702 }
8703
8704 #[test]
8705 fn test_regions_with_latency_sorted_first() {
8706 let mut app = test_app();
8707
8708 app.region_latencies.insert("us-east-1".to_string(), 50);
8710 app.region_latencies.insert("eu-west-1".to_string(), 500);
8711
8712 let filtered = app.get_filtered_regions();
8713
8714 assert!(filtered.len() > 2);
8716
8717 assert_eq!(filtered[0].code, "us-east-1");
8719 assert_eq!(filtered[0].latency_ms, Some(50));
8720 assert_eq!(filtered[1].code, "eu-west-1");
8721 assert_eq!(filtered[1].latency_ms, Some(500));
8722
8723 for region in &filtered[2..] {
8725 assert!(region.latency_ms.is_none());
8726 }
8727 }
8728
8729 #[test]
8730 fn test_regions_without_latency_sorted_as_1000ms() {
8731 let mut app = test_app();
8732
8733 app.region_latencies
8735 .insert("ap-southeast-2".to_string(), 1500);
8736 app.region_latencies.insert("us-east-1".to_string(), 50);
8738
8739 let filtered = app.get_filtered_regions();
8740
8741 assert_eq!(filtered[0].code, "us-east-1");
8743 assert_eq!(filtered[0].latency_ms, Some(50));
8744
8745 let slow_region_idx = filtered
8747 .iter()
8748 .position(|r| r.code == "ap-southeast-2")
8749 .unwrap();
8750 assert!(slow_region_idx > 1); for region in filtered.iter().take(slow_region_idx).skip(1) {
8754 assert!(region.latency_ms.is_none());
8755 }
8756 }
8757
8758 #[test]
8759 fn test_region_picker_opens_with_latencies() {
8760 let mut app = test_app();
8761
8762 app.region_filter.clear();
8764 app.region_picker_selected = 0;
8765 app.measure_region_latencies();
8766
8767 assert!(app.region_latencies.is_empty() || !app.region_latencies.is_empty());
8770 }
8771
8772 #[test]
8773 fn test_region_picker_enter_in_filter_mode_exits_insert_not_selects() {
8774 let mut app = test_app();
8778 app.mode = Mode::RegionPicker;
8779 app.region_filter_active = true; app.region_filter = "us".to_string();
8781 let original_region = app.region.clone();
8782
8783 app.handle_action(Action::Select);
8785
8786 assert!(
8787 !app.region_filter_active,
8788 "Filter must be deactivated (switch to NORMAL/table focus)"
8789 );
8790 assert_eq!(
8791 app.mode,
8792 Mode::RegionPicker,
8793 "Must stay in RegionPicker, not close it"
8794 );
8795 assert_eq!(
8796 app.region, original_region,
8797 "Region must NOT change when Enter is pressed in INSERT mode"
8798 );
8799 }
8800
8801 #[test]
8802 fn test_region_picker_enter_in_normal_mode_selects_region() {
8803 let mut app = test_app();
8805 app.mode = Mode::RegionPicker;
8806 app.region_filter_active = false; app.region_filter.clear();
8808
8809 app.handle_action(Action::Select);
8810
8811 assert_eq!(
8813 app.mode,
8814 Mode::Normal,
8815 "Must close picker after selecting region in NORMAL mode"
8816 );
8817 }
8818
8819 #[test]
8820 fn test_region_filter_can_type_any_character_in_insert_mode() {
8821 use crate::keymap::{handle_key, Mode as KMode};
8825 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
8826
8827 let mut app = test_app();
8828 app.mode = Mode::RegionPicker;
8829 app.region_filter_active = true;
8830 app.region_filter.clear();
8831
8832 let t_key = KeyEvent::new(KeyCode::Char('t'), KeyModifiers::NONE);
8834 let o_key = KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE);
8835 let k_key = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE);
8836
8837 if let Some(action) = handle_key(t_key, KMode::RegionPicker) {
8839 app.handle_action(action);
8840 }
8841 if let Some(action) = handle_key(o_key, KMode::RegionPicker) {
8842 app.handle_action(action);
8843 }
8844 if let Some(action) = handle_key(k_key, KMode::RegionPicker) {
8845 app.handle_action(action);
8846 }
8847
8848 assert_eq!(
8849 app.region_filter, "tok",
8850 "'k' must be typed into filter (not navigate up) when filter is active"
8851 );
8852 }
8853
8854 #[test]
8855 fn test_ecr_tab_next() {
8856 assert_eq!(EcrTab::Private.next(), EcrTab::Public);
8857 assert_eq!(EcrTab::Public.next(), EcrTab::Private);
8858 }
8859
8860 #[test]
8861 fn test_ecr_tab_switching() {
8862 let mut app = test_app();
8863 app.current_service = Service::EcrRepositories;
8864 app.service_selected = true;
8865 app.ecr_state.tab = EcrTab::Private;
8866
8867 app.handle_action(Action::NextDetailTab);
8868 assert_eq!(app.ecr_state.tab, EcrTab::Public);
8869 assert_eq!(app.ecr_state.repositories.selected, 0);
8870
8871 app.handle_action(Action::NextDetailTab);
8872 assert_eq!(app.ecr_state.tab, EcrTab::Private);
8873 }
8874
8875 #[test]
8876 fn test_ecr_navigation() {
8877 let mut app = test_app();
8878 app.current_service = Service::EcrRepositories;
8879 app.service_selected = true;
8880 app.mode = Mode::Normal;
8881 app.ecr_state.repositories.items = vec![
8882 EcrRepository {
8883 name: "repo1".to_string(),
8884 uri: "uri1".to_string(),
8885 created_at: "2023-01-01".to_string(),
8886 tag_immutability: "MUTABLE".to_string(),
8887 encryption_type: "AES256".to_string(),
8888 },
8889 EcrRepository {
8890 name: "repo2".to_string(),
8891 uri: "uri2".to_string(),
8892 created_at: "2023-01-02".to_string(),
8893 tag_immutability: "IMMUTABLE".to_string(),
8894 encryption_type: "KMS".to_string(),
8895 },
8896 ];
8897
8898 app.handle_action(Action::NextItem);
8899 assert_eq!(app.ecr_state.repositories.selected, 1);
8900
8901 app.handle_action(Action::PrevItem);
8902 assert_eq!(app.ecr_state.repositories.selected, 0);
8903 }
8904
8905 #[test]
8906 fn test_ecr_filter() {
8907 let mut app = test_app();
8908 app.current_service = Service::EcrRepositories;
8909 app.service_selected = true;
8910 app.ecr_state.repositories.items = vec![
8911 EcrRepository {
8912 name: "my-app".to_string(),
8913 uri: "uri1".to_string(),
8914 created_at: "2023-01-01".to_string(),
8915 tag_immutability: "MUTABLE".to_string(),
8916 encryption_type: "AES256".to_string(),
8917 },
8918 EcrRepository {
8919 name: "other-service".to_string(),
8920 uri: "uri2".to_string(),
8921 created_at: "2023-01-02".to_string(),
8922 tag_immutability: "IMMUTABLE".to_string(),
8923 encryption_type: "KMS".to_string(),
8924 },
8925 ];
8926
8927 app.ecr_state.repositories.filter = "app".to_string();
8928 let filtered = filtered_ecr_repositories(&app);
8929 assert_eq!(filtered.len(), 1);
8930 assert_eq!(filtered[0].name, "my-app");
8931 }
8932
8933 #[test]
8934 fn test_ecr_filter_input() {
8935 let mut app = test_app();
8936 app.current_service = Service::EcrRepositories;
8937 app.service_selected = true;
8938 app.mode = Mode::FilterInput;
8939
8940 app.handle_action(Action::FilterInput('t'));
8941 app.handle_action(Action::FilterInput('e'));
8942 app.handle_action(Action::FilterInput('s'));
8943 app.handle_action(Action::FilterInput('t'));
8944 assert_eq!(app.ecr_state.repositories.filter, "test");
8945
8946 app.handle_action(Action::FilterBackspace);
8947 assert_eq!(app.ecr_state.repositories.filter, "tes");
8948 }
8949
8950 #[test]
8951 fn test_ecr_filter_resets_selection() {
8952 let mut app = test_app();
8953 app.current_service = Service::EcrRepositories;
8954 app.service_selected = true;
8955 app.mode = Mode::FilterInput;
8956 app.ecr_state.repositories.items = vec![
8957 EcrRepository {
8958 name: "repo1".to_string(),
8959 uri: "uri1".to_string(),
8960 created_at: "2023-01-01".to_string(),
8961 tag_immutability: "MUTABLE".to_string(),
8962 encryption_type: "AES256".to_string(),
8963 },
8964 EcrRepository {
8965 name: "repo2".to_string(),
8966 uri: "uri2".to_string(),
8967 created_at: "2023-01-02".to_string(),
8968 tag_immutability: "IMMUTABLE".to_string(),
8969 encryption_type: "KMS".to_string(),
8970 },
8971 EcrRepository {
8972 name: "repo3".to_string(),
8973 uri: "uri3".to_string(),
8974 created_at: "2023-01-03".to_string(),
8975 tag_immutability: "MUTABLE".to_string(),
8976 encryption_type: "AES256".to_string(),
8977 },
8978 ];
8979
8980 app.ecr_state.repositories.selected = 2;
8982 assert_eq!(app.ecr_state.repositories.selected, 2);
8983
8984 app.handle_action(Action::FilterInput('t'));
8986 assert_eq!(app.ecr_state.repositories.filter, "t");
8987 assert_eq!(app.ecr_state.repositories.selected, 0);
8988
8989 app.ecr_state.repositories.selected = 1;
8991
8992 app.handle_action(Action::FilterBackspace);
8994 assert_eq!(app.ecr_state.repositories.filter, "");
8995 assert_eq!(app.ecr_state.repositories.selected, 0);
8996 }
8997
8998 #[test]
8999 fn test_ecr_images_filter_resets_selection() {
9000 let mut app = test_app();
9001 app.current_service = Service::EcrRepositories;
9002 app.service_selected = true;
9003 app.mode = Mode::FilterInput;
9004 app.ecr_state.current_repository = Some("test-repo".to_string());
9005 app.ecr_state.images.items = vec![
9006 EcrImage {
9007 tag: "v1.0.0".to_string(),
9008 artifact_type: "container".to_string(),
9009 digest: "sha256:abc123".to_string(),
9010 pushed_at: "2023-01-01".to_string(),
9011 size_bytes: 1000,
9012 uri: "uri1".to_string(),
9013 last_pull_time: "".to_string(),
9014 },
9015 EcrImage {
9016 tag: "v2.0.0".to_string(),
9017 artifact_type: "container".to_string(),
9018 digest: "sha256:def456".to_string(),
9019 pushed_at: "2023-01-02".to_string(),
9020 size_bytes: 2000,
9021 uri: "uri2".to_string(),
9022 last_pull_time: "".to_string(),
9023 },
9024 ];
9025
9026 app.ecr_state.images.selected = 1;
9028 assert_eq!(app.ecr_state.images.selected, 1);
9029
9030 app.handle_action(Action::FilterInput('v'));
9032 assert_eq!(app.ecr_state.images.filter, "v");
9033 assert_eq!(app.ecr_state.images.selected, 0);
9034 }
9035
9036 #[test]
9037 fn test_iam_users_filter_input() {
9038 let mut app = test_app();
9039 app.current_service = Service::IamUsers;
9040 app.service_selected = true;
9041 app.mode = Mode::FilterInput;
9042
9043 app.handle_action(Action::FilterInput('a'));
9044 app.handle_action(Action::FilterInput('d'));
9045 app.handle_action(Action::FilterInput('m'));
9046 app.handle_action(Action::FilterInput('i'));
9047 app.handle_action(Action::FilterInput('n'));
9048 assert_eq!(app.iam_state.users.filter, "admin");
9049
9050 app.handle_action(Action::FilterBackspace);
9051 assert_eq!(app.iam_state.users.filter, "admi");
9052 }
9053
9054 #[test]
9055 fn test_iam_policies_filter_input() {
9056 let mut app = test_app();
9057 app.current_service = Service::IamUsers;
9058 app.service_selected = true;
9059 app.iam_state.current_user = Some("testuser".to_string());
9060 app.mode = Mode::FilterInput;
9061
9062 app.handle_action(Action::FilterInput('r'));
9063 app.handle_action(Action::FilterInput('e'));
9064 app.handle_action(Action::FilterInput('a'));
9065 app.handle_action(Action::FilterInput('d'));
9066 assert_eq!(app.iam_state.policies.filter, "read");
9067
9068 app.handle_action(Action::FilterBackspace);
9069 assert_eq!(app.iam_state.policies.filter, "rea");
9070 }
9071
9072 #[test]
9073 fn test_iam_start_filter() {
9074 let mut app = test_app();
9075 app.current_service = Service::IamUsers;
9076 app.service_selected = true;
9077 app.mode = Mode::Normal;
9078
9079 app.handle_action(Action::StartFilter);
9080 assert_eq!(app.mode, Mode::FilterInput);
9081 }
9082
9083 #[test]
9084 fn test_iam_roles_filter_input() {
9085 let mut app = test_app();
9086 app.current_service = Service::IamRoles;
9087 app.service_selected = true;
9088 app.mode = Mode::FilterInput;
9089
9090 app.handle_action(Action::FilterInput('a'));
9091 app.handle_action(Action::FilterInput('d'));
9092 app.handle_action(Action::FilterInput('m'));
9093 app.handle_action(Action::FilterInput('i'));
9094 app.handle_action(Action::FilterInput('n'));
9095 assert_eq!(app.iam_state.roles.filter, "admin");
9096
9097 app.handle_action(Action::FilterBackspace);
9098 assert_eq!(app.iam_state.roles.filter, "admi");
9099 }
9100
9101 #[test]
9102 fn test_iam_roles_start_filter() {
9103 let mut app = test_app();
9104 app.current_service = Service::IamRoles;
9105 app.service_selected = true;
9106 app.mode = Mode::Normal;
9107
9108 app.handle_action(Action::StartFilter);
9109 assert_eq!(app.mode, Mode::FilterInput);
9110 }
9111
9112 #[test]
9113 fn test_iam_roles_navigation() {
9114 let mut app = test_app();
9115 app.current_service = Service::IamRoles;
9116 app.service_selected = true;
9117 app.mode = Mode::Normal;
9118 app.iam_state.roles.items = (0..10)
9119 .map(|i| IamRole {
9120 role_name: format!("role{}", i),
9121 path: "/".to_string(),
9122 trusted_entities: String::new(),
9123 last_activity: String::new(),
9124 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
9125 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9126 description: String::new(),
9127 max_session_duration: Some(3600),
9128 })
9129 .collect();
9130
9131 assert_eq!(app.iam_state.roles.selected, 0);
9132
9133 app.handle_action(Action::NextItem);
9134 assert_eq!(app.iam_state.roles.selected, 1);
9135
9136 app.handle_action(Action::NextItem);
9137 assert_eq!(app.iam_state.roles.selected, 2);
9138
9139 app.handle_action(Action::PrevItem);
9140 assert_eq!(app.iam_state.roles.selected, 1);
9141 }
9142
9143 #[test]
9144 fn test_iam_roles_page_hotkey() {
9145 let mut app = test_app();
9146 app.current_service = Service::IamRoles;
9147 app.service_selected = true;
9148 app.mode = Mode::Normal;
9149 app.iam_state.roles.page_size = PageSize::Ten;
9150 app.iam_state.roles.items = (0..100)
9151 .map(|i| IamRole {
9152 role_name: format!("role{}", i),
9153 path: "/".to_string(),
9154 trusted_entities: String::new(),
9155 last_activity: String::new(),
9156 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
9157 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9158 description: String::new(),
9159 max_session_duration: Some(3600),
9160 })
9161 .collect();
9162
9163 app.handle_action(Action::FilterInput('2'));
9164 app.handle_action(Action::OpenColumnSelector);
9165 assert_eq!(app.iam_state.roles.selected, 10); }
9167
9168 #[test]
9169 fn test_iam_users_page_hotkey() {
9170 let mut app = test_app();
9171 app.current_service = Service::IamUsers;
9172 app.service_selected = true;
9173 app.mode = Mode::Normal;
9174 app.iam_state.users.page_size = PageSize::Ten;
9175 app.iam_state.users.items = (0..100)
9176 .map(|i| IamUser {
9177 user_name: format!("user{}", i),
9178 path: "/".to_string(),
9179 groups: String::new(),
9180 last_activity: String::new(),
9181 mfa: String::new(),
9182 password_age: String::new(),
9183 console_last_sign_in: String::new(),
9184 access_key_id: String::new(),
9185 active_key_age: String::new(),
9186 access_key_last_used: String::new(),
9187 arn: format!("arn:aws:iam::123456789012:user/user{}", i),
9188 creation_time: "2025-01-01 00:00:00 (UTC)".to_string(),
9189 console_access: String::new(),
9190 signing_certs: String::new(),
9191 })
9192 .collect();
9193
9194 app.handle_action(Action::FilterInput('3'));
9195 app.handle_action(Action::OpenColumnSelector);
9196 assert_eq!(app.iam_state.users.selected, 20); }
9198
9199 #[test]
9200 fn test_ecr_scroll_navigation() {
9201 let mut app = test_app();
9202 app.current_service = Service::EcrRepositories;
9203 app.service_selected = true;
9204 app.ecr_state.repositories.items = (0..20)
9205 .map(|i| EcrRepository {
9206 name: format!("repo{}", i),
9207 uri: format!("uri{}", i),
9208 created_at: "2023-01-01".to_string(),
9209 tag_immutability: "MUTABLE".to_string(),
9210 encryption_type: "AES256".to_string(),
9211 })
9212 .collect();
9213
9214 app.handle_action(Action::ScrollDown);
9215 assert_eq!(app.ecr_state.repositories.selected, 10);
9216
9217 app.handle_action(Action::ScrollUp);
9218 assert_eq!(app.ecr_state.repositories.selected, 0);
9219 }
9220
9221 #[test]
9222 fn test_ecr_tab_switching_triggers_reload() {
9223 let mut app = test_app();
9224 app.current_service = Service::EcrRepositories;
9225 app.service_selected = true;
9226 app.ecr_state.tab = EcrTab::Private;
9227 app.ecr_state.repositories.loading = false;
9228 app.ecr_state.repositories.items = vec![EcrRepository {
9229 name: "private-repo".to_string(),
9230 uri: "uri".to_string(),
9231 created_at: "2023-01-01".to_string(),
9232 tag_immutability: "MUTABLE".to_string(),
9233 encryption_type: "AES256".to_string(),
9234 }];
9235
9236 app.handle_action(Action::NextDetailTab);
9237 assert_eq!(app.ecr_state.tab, EcrTab::Public);
9238 assert!(app.ecr_state.repositories.loading);
9239 assert_eq!(app.ecr_state.repositories.selected, 0);
9240 }
9241
9242 #[test]
9243 fn test_ecr_tab_cycles_between_private_and_public() {
9244 let mut app = test_app();
9245 app.current_service = Service::EcrRepositories;
9246 app.service_selected = true;
9247 app.ecr_state.tab = EcrTab::Private;
9248
9249 app.handle_action(Action::NextDetailTab);
9250 assert_eq!(app.ecr_state.tab, EcrTab::Public);
9251
9252 app.handle_action(Action::NextDetailTab);
9253 assert_eq!(app.ecr_state.tab, EcrTab::Private);
9254 }
9255
9256 #[test]
9257 fn test_page_size_values() {
9258 assert_eq!(PageSize::Ten.value(), 10);
9259 assert_eq!(PageSize::TwentyFive.value(), 25);
9260 assert_eq!(PageSize::Fifty.value(), 50);
9261 assert_eq!(PageSize::OneHundred.value(), 100);
9262 }
9263
9264 #[test]
9265 fn test_page_size_next() {
9266 assert_eq!(PageSize::Ten.next(), PageSize::TwentyFive);
9267 assert_eq!(PageSize::TwentyFive.next(), PageSize::Fifty);
9268 assert_eq!(PageSize::Fifty.next(), PageSize::OneHundred);
9269 assert_eq!(PageSize::OneHundred.next(), PageSize::Ten);
9270 }
9271
9272 #[test]
9273 fn test_ecr_enter_drills_into_repository() {
9274 let mut app = test_app();
9275 app.current_service = Service::EcrRepositories;
9276 app.service_selected = true;
9277 app.mode = Mode::Normal;
9278 app.ecr_state.repositories.items = vec![EcrRepository {
9279 name: "my-repo".to_string(),
9280 uri: "uri".to_string(),
9281 created_at: "2023-01-01".to_string(),
9282 tag_immutability: "MUTABLE".to_string(),
9283 encryption_type: "AES256".to_string(),
9284 }];
9285
9286 app.handle_action(Action::Select);
9287 assert_eq!(
9288 app.ecr_state.current_repository,
9289 Some("my-repo".to_string())
9290 );
9291 assert!(app.ecr_state.repositories.loading);
9292 }
9293
9294 #[test]
9295 fn test_ecr_repository_expansion() {
9296 let mut app = test_app();
9297 app.current_service = Service::EcrRepositories;
9298 app.service_selected = true;
9299 app.ecr_state.repositories.items = vec![EcrRepository {
9300 name: "my-repo".to_string(),
9301 uri: "uri".to_string(),
9302 created_at: "2023-01-01".to_string(),
9303 tag_immutability: "MUTABLE".to_string(),
9304 encryption_type: "AES256".to_string(),
9305 }];
9306 app.ecr_state.repositories.selected = 0;
9307
9308 assert_eq!(app.ecr_state.repositories.expanded_item, None);
9309
9310 app.handle_action(Action::NextPane);
9311 assert_eq!(app.ecr_state.repositories.expanded_item, Some(0));
9312
9313 app.handle_action(Action::PrevPane);
9314 assert_eq!(app.ecr_state.repositories.expanded_item, None);
9315 }
9316
9317 #[test]
9318 fn test_ecr_ctrl_d_scrolls_down() {
9319 let mut app = test_app();
9320 app.current_service = Service::EcrRepositories;
9321 app.service_selected = true;
9322 app.mode = Mode::Normal;
9323 app.ecr_state.repositories.items = (0..30)
9324 .map(|i| EcrRepository {
9325 name: format!("repo{}", i),
9326 uri: format!("uri{}", i),
9327 created_at: "2023-01-01".to_string(),
9328 tag_immutability: "MUTABLE".to_string(),
9329 encryption_type: "AES256".to_string(),
9330 })
9331 .collect();
9332 app.ecr_state.repositories.selected = 0;
9333
9334 app.handle_action(Action::PageDown);
9335 assert_eq!(app.ecr_state.repositories.selected, 10);
9336 }
9337
9338 #[test]
9339 fn test_ecr_ctrl_u_scrolls_up() {
9340 let mut app = test_app();
9341 app.current_service = Service::EcrRepositories;
9342 app.service_selected = true;
9343 app.mode = Mode::Normal;
9344 app.ecr_state.repositories.items = (0..30)
9345 .map(|i| EcrRepository {
9346 name: format!("repo{}", i),
9347 uri: format!("uri{}", i),
9348 created_at: "2023-01-01".to_string(),
9349 tag_immutability: "MUTABLE".to_string(),
9350 encryption_type: "AES256".to_string(),
9351 })
9352 .collect();
9353 app.ecr_state.repositories.selected = 15;
9354
9355 app.handle_action(Action::PageUp);
9356 assert_eq!(app.ecr_state.repositories.selected, 5);
9357 }
9358
9359 #[test]
9360 fn test_ecr_images_ctrl_d_scrolls_down() {
9361 let mut app = test_app();
9362 app.current_service = Service::EcrRepositories;
9363 app.service_selected = true;
9364 app.mode = Mode::Normal;
9365 app.ecr_state.current_repository = Some("repo".to_string());
9366 app.ecr_state.images.items = (0..30)
9367 .map(|i| EcrImage {
9368 tag: format!("tag{}", i),
9369 artifact_type: "container".to_string(),
9370 pushed_at: "2023-01-01T12:00:00Z".to_string(),
9371 size_bytes: 104857600,
9372 uri: format!("uri{}", i),
9373 digest: format!("sha256:{}", i),
9374 last_pull_time: String::new(),
9375 })
9376 .collect();
9377 app.ecr_state.images.selected = 0;
9378
9379 app.handle_action(Action::PageDown);
9380 assert_eq!(app.ecr_state.images.selected, 10);
9381 }
9382
9383 #[test]
9384 fn test_ecr_esc_goes_back_from_images_to_repos() {
9385 let mut app = test_app();
9386 app.current_service = Service::EcrRepositories;
9387 app.service_selected = true;
9388 app.mode = Mode::Normal;
9389 app.ecr_state.current_repository = Some("my-repo".to_string());
9390 app.ecr_state.images.items = vec![EcrImage {
9391 tag: "latest".to_string(),
9392 artifact_type: "container".to_string(),
9393 pushed_at: "2023-01-01T12:00:00Z".to_string(),
9394 size_bytes: 104857600,
9395 uri: "uri".to_string(),
9396 digest: "sha256:abc".to_string(),
9397 last_pull_time: String::new(),
9398 }];
9399
9400 app.handle_action(Action::GoBack);
9401 assert_eq!(app.ecr_state.current_repository, None);
9402 assert!(app.ecr_state.images.items.is_empty());
9403 }
9404
9405 #[test]
9406 fn test_ecr_esc_collapses_expanded_image_first() {
9407 let mut app = test_app();
9408 app.current_service = Service::EcrRepositories;
9409 app.service_selected = true;
9410 app.mode = Mode::Normal;
9411 app.ecr_state.current_repository = Some("my-repo".to_string());
9412 app.ecr_state.images.expanded_item = Some(0);
9413
9414 app.handle_action(Action::GoBack);
9415 assert_eq!(app.ecr_state.images.expanded_item, None);
9416 assert_eq!(
9417 app.ecr_state.current_repository,
9418 Some("my-repo".to_string())
9419 );
9420 }
9421
9422 #[test]
9423 fn test_pagination_with_lowercase_p() {
9424 let mut app = test_app();
9425 app.current_service = Service::EcrRepositories;
9426 app.service_selected = true;
9427 app.mode = Mode::Normal;
9428 app.ecr_state.repositories.items = (0..100)
9429 .map(|i| EcrRepository {
9430 name: format!("repo{}", i),
9431 uri: format!("uri{}", i),
9432 created_at: "2023-01-01".to_string(),
9433 tag_immutability: "MUTABLE".to_string(),
9434 encryption_type: "AES256".to_string(),
9435 })
9436 .collect();
9437
9438 app.handle_action(Action::FilterInput('2'));
9440 assert_eq!(app.page_input, "2");
9441
9442 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.ecr_state.repositories.selected, 50); assert_eq!(app.page_input, ""); }
9446
9447 #[test]
9448 fn test_lowercase_p_without_number_opens_preferences() {
9449 let mut app = test_app();
9450 app.current_service = Service::EcrRepositories;
9451 app.service_selected = true;
9452 app.mode = Mode::Normal;
9453
9454 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.mode, Mode::ColumnSelector);
9456 }
9457
9458 #[test]
9459 fn test_ctrl_o_generates_correct_console_url() {
9460 let mut app = test_app();
9461 app.current_service = Service::EcrRepositories;
9462 app.service_selected = true;
9463 app.mode = Mode::Normal;
9464 app.config.account_id = "123456789012".to_string();
9465
9466 let url = app.get_console_url();
9468 assert!(url.contains("ecr/private-registry/repositories"));
9469 assert!(url.contains("region=us-east-1"));
9470
9471 app.ecr_state.current_repository = Some("my-repo".to_string());
9473 let url = app.get_console_url();
9474 assert!(url.contains("ecr/repositories/private/123456789012/my-repo"));
9475 assert!(url.contains("region=us-east-1"));
9476 }
9477
9478 #[test]
9479 fn test_page_input_display_and_reset() {
9480 let mut app = test_app();
9481 app.current_service = Service::EcrRepositories;
9482 app.service_selected = true;
9483 app.mode = Mode::Normal;
9484 app.ecr_state.repositories.items = (0..100)
9485 .map(|i| EcrRepository {
9486 name: format!("repo{}", i),
9487 uri: format!("uri{}", i),
9488 created_at: "2023-01-01".to_string(),
9489 tag_immutability: "MUTABLE".to_string(),
9490 encryption_type: "AES256".to_string(),
9491 })
9492 .collect();
9493
9494 app.handle_action(Action::FilterInput('2'));
9496 assert_eq!(app.page_input, "2");
9497
9498 app.handle_action(Action::OpenColumnSelector);
9500 assert_eq!(app.page_input, ""); assert_eq!(app.ecr_state.repositories.selected, 50); }
9503
9504 #[test]
9505 fn test_page_navigation_updates_scroll_offset_for_cfn() {
9506 let mut app = test_app();
9507 app.current_service = Service::CloudFormationStacks;
9508 app.service_selected = true;
9509 app.mode = Mode::Normal;
9510 app.cfn_state.table.items = (0..100)
9511 .map(|i| CfnStack {
9512 name: format!("stack-{}", i),
9513 stack_id: format!(
9514 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
9515 i
9516 ),
9517 status: "CREATE_COMPLETE".to_string(),
9518 created_time: "2023-01-01T00:00:00Z".to_string(),
9519 updated_time: "2023-01-01T00:00:00Z".to_string(),
9520 deleted_time: String::new(),
9521 drift_status: "IN_SYNC".to_string(),
9522 last_drift_check_time: String::new(),
9523 status_reason: String::new(),
9524 description: String::new(),
9525 detailed_status: String::new(),
9526 root_stack: String::new(),
9527 parent_stack: String::new(),
9528 termination_protection: false,
9529 iam_role: String::new(),
9530 tags: vec![],
9531 stack_policy: String::new(),
9532 rollback_monitoring_time: String::new(),
9533 rollback_alarms: vec![],
9534 notification_arns: vec![],
9535 })
9536 .collect();
9537
9538 app.handle_action(Action::FilterInput('2'));
9540 assert_eq!(app.page_input, "2");
9541
9542 app.handle_action(Action::OpenColumnSelector); assert_eq!(app.page_input, ""); let page_size = app.cfn_state.table.page_size.value();
9547 let expected_offset = page_size; assert_eq!(app.cfn_state.table.selected, expected_offset);
9549 assert_eq!(app.cfn_state.table.scroll_offset, expected_offset);
9550
9551 let current_page = app.cfn_state.table.scroll_offset / page_size;
9553 assert_eq!(
9554 current_page, 1,
9555 "2p should go to page 2 (0-indexed as 1), not page 3"
9556 ); }
9558
9559 #[test]
9560 fn test_3p_goes_to_page_3_not_page_5() {
9561 let mut app = test_app();
9562 app.current_service = Service::CloudFormationStacks;
9563 app.service_selected = true;
9564 app.mode = Mode::Normal;
9565 app.cfn_state.table.items = (0..200)
9566 .map(|i| CfnStack {
9567 name: format!("stack-{}", i),
9568 stack_id: format!(
9569 "arn:aws:cloudformation:us-east-1:123456789012:stack/stack-{}/id",
9570 i
9571 ),
9572 status: "CREATE_COMPLETE".to_string(),
9573 created_time: "2023-01-01T00:00:00Z".to_string(),
9574 updated_time: "2023-01-01T00:00:00Z".to_string(),
9575 deleted_time: String::new(),
9576 drift_status: "IN_SYNC".to_string(),
9577 last_drift_check_time: String::new(),
9578 status_reason: String::new(),
9579 description: String::new(),
9580 detailed_status: String::new(),
9581 root_stack: String::new(),
9582 parent_stack: String::new(),
9583 termination_protection: false,
9584 iam_role: String::new(),
9585 tags: vec![],
9586 stack_policy: String::new(),
9587 rollback_monitoring_time: String::new(),
9588 rollback_alarms: vec![],
9589 notification_arns: vec![],
9590 })
9591 .collect();
9592
9593 app.handle_action(Action::FilterInput('3'));
9595 app.handle_action(Action::OpenColumnSelector);
9596
9597 let page_size = app.cfn_state.table.page_size.value();
9598 let current_page = app.cfn_state.table.scroll_offset / page_size;
9599 assert_eq!(
9600 current_page, 2,
9601 "3p should go to page 3 (0-indexed as 2), not page 5"
9602 );
9603 assert_eq!(app.cfn_state.table.scroll_offset, 2 * page_size);
9604 }
9605
9606 #[test]
9607 fn test_log_streams_page_navigation_uses_correct_page_size() {
9608 let mut app = test_app();
9609 app.current_service = Service::CloudWatchLogGroups;
9610 app.view_mode = ViewMode::Detail;
9611 app.service_selected = true;
9612 app.mode = Mode::Normal;
9613 app.log_groups_state.log_streams = (0..100)
9614 .map(|i| LogStream {
9615 name: format!("stream-{}", i),
9616 creation_time: None,
9617 last_event_time: None,
9618 })
9619 .collect();
9620
9621 app.handle_action(Action::FilterInput('2'));
9623 app.handle_action(Action::OpenColumnSelector);
9624
9625 assert_eq!(app.log_groups_state.stream_current_page, 1);
9627 assert_eq!(app.log_groups_state.selected_stream, 0);
9628
9629 assert_eq!(
9631 app.log_groups_state.stream_current_page, 1,
9632 "2p should go to page 2 (0-indexed as 1), not page 3"
9633 );
9634 }
9635
9636 #[test]
9637 fn test_ecr_repositories_page_navigation_uses_configurable_page_size() {
9638 let mut app = test_app();
9639 app.current_service = Service::EcrRepositories;
9640 app.service_selected = true;
9641 app.mode = Mode::Normal;
9642 app.ecr_state.repositories.page_size = PageSize::TwentyFive; app.ecr_state.repositories.items = (0..100)
9644 .map(|i| EcrRepository {
9645 name: format!("repo{}", i),
9646 uri: format!("uri{}", i),
9647 created_at: "2023-01-01".to_string(),
9648 tag_immutability: "MUTABLE".to_string(),
9649 encryption_type: "AES256".to_string(),
9650 })
9651 .collect();
9652
9653 app.handle_action(Action::FilterInput('3'));
9655 app.handle_action(Action::OpenColumnSelector);
9656
9657 assert_eq!(app.ecr_state.repositories.selected, 50);
9659
9660 let page_size = app.ecr_state.repositories.page_size.value();
9661 let current_page = app.ecr_state.repositories.selected / page_size;
9662 assert_eq!(
9663 current_page, 2,
9664 "3p with page_size=25 should go to page 3 (0-indexed as 2)"
9665 );
9666 }
9667
9668 #[test]
9669 fn test_page_navigation_updates_scroll_offset_for_alarms() {
9670 let mut app = test_app();
9671 app.current_service = Service::CloudWatchAlarms;
9672 app.service_selected = true;
9673 app.mode = Mode::Normal;
9674 app.alarms_state.table.items = (0..100)
9675 .map(|i| Alarm {
9676 name: format!("alarm-{}", i),
9677 state: "OK".to_string(),
9678 state_updated_timestamp: "2023-01-01T00:00:00Z".to_string(),
9679 description: String::new(),
9680 metric_name: "CPUUtilization".to_string(),
9681 namespace: "AWS/EC2".to_string(),
9682 statistic: "Average".to_string(),
9683 period: 300,
9684 comparison_operator: "GreaterThanThreshold".to_string(),
9685 threshold: 80.0,
9686 actions_enabled: true,
9687 state_reason: String::new(),
9688 resource: String::new(),
9689 dimensions: String::new(),
9690 expression: String::new(),
9691 alarm_type: "MetricAlarm".to_string(),
9692 cross_account: String::new(),
9693 ..Default::default()
9694 })
9695 .collect();
9696
9697 app.handle_action(Action::FilterInput('2'));
9699 app.handle_action(Action::OpenColumnSelector);
9700
9701 let page_size = app.alarms_state.table.page_size.value();
9703 let expected_offset = page_size; assert_eq!(app.alarms_state.table.selected, expected_offset);
9705 assert_eq!(app.alarms_state.table.scroll_offset, expected_offset);
9706 }
9707
9708 #[test]
9709 fn test_ecr_pagination_with_65_repos() {
9710 let mut app = test_app();
9711 app.current_service = Service::EcrRepositories;
9712 app.service_selected = true;
9713 app.mode = Mode::Normal;
9714 app.ecr_state.repositories.items = (0..65)
9715 .map(|i| EcrRepository {
9716 name: format!("repo{:02}", i),
9717 uri: format!("uri{}", i),
9718 created_at: "2023-01-01".to_string(),
9719 tag_immutability: "MUTABLE".to_string(),
9720 encryption_type: "AES256".to_string(),
9721 })
9722 .collect();
9723
9724 assert_eq!(app.ecr_state.repositories.selected, 0);
9726 let page_size = 50;
9727 let current_page = app.ecr_state.repositories.selected / page_size;
9728 assert_eq!(current_page, 0);
9729
9730 app.handle_action(Action::FilterInput('2'));
9732 app.handle_action(Action::OpenColumnSelector);
9733 assert_eq!(app.ecr_state.repositories.selected, 50);
9734
9735 let current_page = app.ecr_state.repositories.selected / page_size;
9737 assert_eq!(current_page, 1);
9738 }
9739
9740 #[test]
9741 fn test_ecr_repos_input_focus_tab_cycling() {
9742 let mut app = test_app();
9743 app.current_service = Service::EcrRepositories;
9744 app.service_selected = true;
9745 app.mode = Mode::FilterInput;
9746 app.ecr_state.input_focus = InputFocus::Filter;
9747
9748 app.handle_action(Action::NextFilterFocus);
9750 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
9751
9752 app.handle_action(Action::NextFilterFocus);
9754 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
9755
9756 app.handle_action(Action::PrevFilterFocus);
9758 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
9759
9760 app.handle_action(Action::PrevFilterFocus);
9762 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
9763 }
9764
9765 #[test]
9766 fn test_ecr_images_column_toggle_not_off_by_one() {
9767 use crate::ecr::image::Column as ImageColumn;
9768 let mut app = test_app();
9769 app.current_service = Service::EcrRepositories;
9770 app.service_selected = true;
9771 app.mode = Mode::ColumnSelector;
9772 app.ecr_state.current_repository = Some("test-repo".to_string());
9773
9774 app.ecr_image_visible_column_ids = ImageColumn::ids();
9776 let initial_count = app.ecr_image_visible_column_ids.len();
9777
9778 app.column_selector_index = 0;
9780 app.handle_action(Action::ToggleColumn);
9781 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
9782
9783 app.column_selector_index = 1;
9785 app.handle_action(Action::ToggleColumn);
9786
9787 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count - 1);
9789 assert!(!app
9790 .ecr_image_visible_column_ids
9791 .contains(&ImageColumn::Tag.id()));
9792
9793 app.handle_action(Action::ToggleColumn);
9795 assert_eq!(app.ecr_image_visible_column_ids.len(), initial_count);
9796 assert!(app
9797 .ecr_image_visible_column_ids
9798 .contains(&ImageColumn::Tag.id()));
9799 }
9800
9801 #[test]
9802 fn test_ecr_repos_column_toggle_works() {
9803 let mut app = test_app();
9804 app.current_service = Service::EcrRepositories;
9805 app.service_selected = true;
9806 app.mode = Mode::ColumnSelector;
9807 app.ecr_state.current_repository = None;
9808
9809 app.ecr_repo_visible_column_ids = EcrColumn::ids();
9811 let initial_count = app.ecr_repo_visible_column_ids.len();
9812
9813 app.column_selector_index = 1;
9815 app.handle_action(Action::ToggleColumn);
9816
9817 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count - 1);
9819 assert!(!app
9820 .ecr_repo_visible_column_ids
9821 .contains(&EcrColumn::Name.id()));
9822
9823 app.handle_action(Action::ToggleColumn);
9825 assert_eq!(app.ecr_repo_visible_column_ids.len(), initial_count);
9826 assert!(app
9827 .ecr_repo_visible_column_ids
9828 .contains(&EcrColumn::Name.id()));
9829 }
9830
9831 #[test]
9832 fn test_ecr_repos_pagination_left_right_navigation() {
9833 use crate::ecr::repo::Repository as EcrRepository;
9834 let mut app = test_app();
9835 app.current_service = Service::EcrRepositories;
9836 app.service_selected = true;
9837 app.mode = Mode::FilterInput;
9838 app.ecr_state.input_focus = InputFocus::Pagination;
9839
9840 app.ecr_state.repositories.items = (0..150)
9842 .map(|i| EcrRepository {
9843 name: format!("repo{:03}", i),
9844 uri: format!("uri{}", i),
9845 created_at: "2023-01-01".to_string(),
9846 tag_immutability: "MUTABLE".to_string(),
9847 encryption_type: "AES256".to_string(),
9848 })
9849 .collect();
9850
9851 app.ecr_state.repositories.selected = 0;
9853 eprintln!(
9854 "Initial: selected={}, focus={:?}, mode={:?}",
9855 app.ecr_state.repositories.selected, app.ecr_state.input_focus, app.mode
9856 );
9857
9858 app.handle_action(Action::PageDown);
9860 eprintln!(
9861 "After PageDown: selected={}",
9862 app.ecr_state.repositories.selected
9863 );
9864 assert_eq!(app.ecr_state.repositories.selected, 50);
9865
9866 app.handle_action(Action::PageDown);
9868 eprintln!(
9869 "After 2nd PageDown: selected={}",
9870 app.ecr_state.repositories.selected
9871 );
9872 assert_eq!(app.ecr_state.repositories.selected, 100);
9873
9874 app.handle_action(Action::PageDown);
9876 eprintln!(
9877 "After 3rd PageDown: selected={}",
9878 app.ecr_state.repositories.selected
9879 );
9880 assert_eq!(app.ecr_state.repositories.selected, 100);
9881
9882 app.handle_action(Action::PageUp);
9884 eprintln!(
9885 "After PageUp: selected={}",
9886 app.ecr_state.repositories.selected
9887 );
9888 assert_eq!(app.ecr_state.repositories.selected, 50);
9889
9890 app.handle_action(Action::PageUp);
9892 eprintln!(
9893 "After 2nd PageUp: selected={}",
9894 app.ecr_state.repositories.selected
9895 );
9896 assert_eq!(app.ecr_state.repositories.selected, 0);
9897
9898 app.handle_action(Action::PageUp);
9900 eprintln!(
9901 "After 3rd PageUp: selected={}",
9902 app.ecr_state.repositories.selected
9903 );
9904 assert_eq!(app.ecr_state.repositories.selected, 0);
9905 }
9906
9907 #[test]
9908 fn test_ecr_repos_filter_input_when_input_focused() {
9909 use crate::ecr::repo::Repository as EcrRepository;
9910 let mut app = test_app();
9911 app.current_service = Service::EcrRepositories;
9912 app.service_selected = true;
9913 app.mode = Mode::FilterInput;
9914 app.ecr_state.input_focus = InputFocus::Filter;
9915
9916 app.ecr_state.repositories.items = vec![
9918 EcrRepository {
9919 name: "test-repo".to_string(),
9920 uri: "uri1".to_string(),
9921 created_at: "2023-01-01".to_string(),
9922 tag_immutability: "MUTABLE".to_string(),
9923 encryption_type: "AES256".to_string(),
9924 },
9925 EcrRepository {
9926 name: "prod-repo".to_string(),
9927 uri: "uri2".to_string(),
9928 created_at: "2023-01-01".to_string(),
9929 tag_immutability: "MUTABLE".to_string(),
9930 encryption_type: "AES256".to_string(),
9931 },
9932 ];
9933
9934 assert_eq!(app.ecr_state.repositories.filter, "");
9936 app.handle_action(Action::FilterInput('t'));
9937 assert_eq!(app.ecr_state.repositories.filter, "t");
9938 app.handle_action(Action::FilterInput('e'));
9939 assert_eq!(app.ecr_state.repositories.filter, "te");
9940 app.handle_action(Action::FilterInput('s'));
9941 assert_eq!(app.ecr_state.repositories.filter, "tes");
9942 app.handle_action(Action::FilterInput('t'));
9943 assert_eq!(app.ecr_state.repositories.filter, "test");
9944 }
9945
9946 #[test]
9947 fn test_ecr_repos_digit_input_when_pagination_focused() {
9948 use crate::ecr::repo::Repository as EcrRepository;
9949 let mut app = test_app();
9950 app.current_service = Service::EcrRepositories;
9951 app.service_selected = true;
9952 app.mode = Mode::FilterInput;
9953 app.ecr_state.input_focus = InputFocus::Pagination;
9954
9955 app.ecr_state.repositories.items = vec![EcrRepository {
9957 name: "test-repo".to_string(),
9958 uri: "uri1".to_string(),
9959 created_at: "2023-01-01".to_string(),
9960 tag_immutability: "MUTABLE".to_string(),
9961 encryption_type: "AES256".to_string(),
9962 }];
9963
9964 assert_eq!(app.ecr_state.repositories.filter, "");
9966 assert_eq!(app.page_input, "");
9967 app.handle_action(Action::FilterInput('2'));
9968 assert_eq!(app.ecr_state.repositories.filter, "");
9969 assert_eq!(app.page_input, "2");
9970
9971 app.handle_action(Action::FilterInput('a'));
9973 assert_eq!(app.ecr_state.repositories.filter, "");
9974 assert_eq!(app.page_input, "2");
9975 }
9976
9977 #[test]
9978 fn test_ecr_repos_left_right_scrolls_table_when_input_focused() {
9979 use crate::ecr::repo::Repository as EcrRepository;
9980 let mut app = test_app();
9981 app.current_service = Service::EcrRepositories;
9982 app.service_selected = true;
9983 app.mode = Mode::FilterInput;
9984 app.ecr_state.input_focus = InputFocus::Filter;
9985
9986 app.ecr_state.repositories.items = (0..150)
9988 .map(|i| EcrRepository {
9989 name: format!("repo{:03}", i),
9990 uri: format!("uri{}", i),
9991 created_at: "2023-01-01".to_string(),
9992 tag_immutability: "MUTABLE".to_string(),
9993 encryption_type: "AES256".to_string(),
9994 })
9995 .collect();
9996
9997 app.ecr_state.repositories.selected = 0;
9999
10000 app.handle_action(Action::PageDown);
10002 assert_eq!(
10003 app.ecr_state.repositories.selected, 10,
10004 "Should scroll down by 10"
10005 );
10006
10007 app.handle_action(Action::PageUp);
10008 assert_eq!(
10009 app.ecr_state.repositories.selected, 0,
10010 "Should scroll back up"
10011 );
10012 }
10013
10014 #[test]
10015 fn test_ecr_repos_pagination_control_actually_works() {
10016 use crate::ecr::repo::Repository as EcrRepository;
10017
10018 let mut app = test_app();
10020 app.current_service = Service::EcrRepositories;
10021 app.service_selected = true;
10022 app.mode = Mode::FilterInput;
10023 app.ecr_state.current_repository = None;
10024 app.ecr_state.input_focus = InputFocus::Pagination;
10025
10026 app.ecr_state.repositories.items = (0..100)
10028 .map(|i| EcrRepository {
10029 name: format!("repo{:03}", i),
10030 uri: format!("uri{}", i),
10031 created_at: "2023-01-01".to_string(),
10032 tag_immutability: "MUTABLE".to_string(),
10033 encryption_type: "AES256".to_string(),
10034 })
10035 .collect();
10036
10037 app.ecr_state.repositories.selected = 0;
10038
10039 assert_eq!(app.mode, Mode::FilterInput);
10041 assert_eq!(app.current_service, Service::EcrRepositories);
10042 assert_eq!(app.ecr_state.current_repository, None);
10043 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10044
10045 app.handle_action(Action::PageDown);
10047 assert_eq!(
10048 app.ecr_state.repositories.selected, 50,
10049 "PageDown should move to page 2"
10050 );
10051
10052 app.handle_action(Action::PageUp);
10053 assert_eq!(
10054 app.ecr_state.repositories.selected, 0,
10055 "PageUp should move back to page 1"
10056 );
10057 }
10058
10059 #[test]
10060 fn test_ecr_repos_can_reach_last_page_when_not_multiple_of_page_size() {
10061 use crate::ecr::repo::Repository as EcrRepository;
10062 let mut app = test_app();
10063 app.current_service = Service::EcrRepositories;
10064 app.service_selected = true;
10065 app.mode = Mode::FilterInput;
10066 app.ecr_state.input_focus = InputFocus::Pagination;
10067
10068 app.ecr_state.repositories.items = (0..754)
10070 .map(|i| EcrRepository {
10071 name: format!("repo{:03}", i),
10072 uri: format!("uri{}", i),
10073 created_at: "2023-01-01".to_string(),
10074 tag_immutability: "MUTABLE".to_string(),
10075 encryption_type: "AES256".to_string(),
10076 })
10077 .collect();
10078
10079 app.ecr_state.repositories.selected = 700;
10081
10082 app.handle_action(Action::PageDown);
10084 assert_eq!(
10085 app.ecr_state.repositories.selected, 750,
10086 "Should reach last page (750) but got {}",
10087 app.ecr_state.repositories.selected
10088 );
10089
10090 app.handle_action(Action::PageDown);
10092 assert_eq!(app.ecr_state.repositories.selected, 750);
10093 }
10094
10095 #[test]
10096 fn test_ecr_repos_start_filter_resets_focus_to_input() {
10097 let mut app = test_app();
10098 app.current_service = Service::EcrRepositories;
10099 app.service_selected = true;
10100 app.mode = Mode::Normal;
10101 app.ecr_state.current_repository = None;
10102
10103 app.ecr_state.input_focus = InputFocus::Pagination;
10105
10106 app.handle_action(Action::StartFilter);
10108
10109 assert_eq!(app.mode, Mode::FilterInput);
10111 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10112 }
10113
10114 #[test]
10115 fn test_ecr_repos_exact_user_flow_i_tab_arrow() {
10116 use crate::ecr::repo::Repository as EcrRepository;
10117
10118 let mut app = test_app();
10119 app.current_service = Service::EcrRepositories;
10120 app.service_selected = true;
10121 app.mode = Mode::Normal;
10122 app.ecr_state.current_repository = None;
10123
10124 app.ecr_state.repositories.items = (0..100)
10126 .map(|i| EcrRepository {
10127 name: format!("repo{:03}", i),
10128 uri: format!("uri{}", i),
10129 created_at: "2023-01-01".to_string(),
10130 tag_immutability: "MUTABLE".to_string(),
10131 encryption_type: "AES256".to_string(),
10132 })
10133 .collect();
10134
10135 app.ecr_state.repositories.selected = 0;
10136
10137 app.handle_action(Action::StartFilter);
10139 assert_eq!(app.mode, Mode::FilterInput);
10140 assert_eq!(app.ecr_state.input_focus, InputFocus::Filter);
10141
10142 app.handle_action(Action::NextFilterFocus);
10144 assert_eq!(app.ecr_state.input_focus, InputFocus::Pagination);
10145
10146 eprintln!("Before PageDown: mode={:?}, service={:?}, current_repo={:?}, input_focus={:?}, selected={}",
10148 app.mode, app.current_service, app.ecr_state.current_repository, app.ecr_state.input_focus, app.ecr_state.repositories.selected);
10149 app.handle_action(Action::PageDown);
10150 eprintln!(
10151 "After PageDown: selected={}",
10152 app.ecr_state.repositories.selected
10153 );
10154
10155 assert_eq!(
10157 app.ecr_state.repositories.selected, 50,
10158 "Right arrow should move to page 2"
10159 );
10160
10161 app.handle_action(Action::PageUp);
10163 assert_eq!(
10164 app.ecr_state.repositories.selected, 0,
10165 "Left arrow should move back to page 1"
10166 );
10167 }
10168
10169 #[test]
10170 fn test_apig_filter_input() {
10171 let mut app = test_app();
10172 app.current_service = Service::ApiGatewayApis;
10173 app.service_selected = true;
10174 app.mode = Mode::FilterInput;
10175
10176 app.handle_action(Action::FilterInput('t'));
10177 app.handle_action(Action::FilterInput('e'));
10178 app.handle_action(Action::FilterInput('s'));
10179 app.handle_action(Action::FilterInput('t'));
10180 assert_eq!(app.apig_state.apis.filter, "test");
10181
10182 app.handle_action(Action::FilterBackspace);
10183 assert_eq!(app.apig_state.apis.filter, "tes");
10184 }
10185
10186 #[test]
10187 fn test_apig_start_filter_enters_filter_mode() {
10188 let mut app = test_app();
10189 app.current_service = Service::ApiGatewayApis;
10190 app.service_selected = true;
10191 app.mode = Mode::Normal;
10192
10193 app.handle_action(Action::StartFilter);
10194 assert_eq!(app.mode, Mode::FilterInput);
10195 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10196 }
10197
10198 #[test]
10199 fn test_apig_input_focus_cycles_with_tab() {
10200 let mut app = test_app();
10201 app.current_service = Service::ApiGatewayApis;
10202 app.service_selected = true;
10203 app.mode = Mode::FilterInput;
10204 app.apig_state.input_focus = InputFocus::Filter;
10205
10206 app.handle_action(Action::NextFilterFocus);
10208 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
10209
10210 app.handle_action(Action::NextFilterFocus);
10212 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10213 }
10214
10215 #[test]
10216 fn test_apig_input_focus_cycles_with_shift_tab() {
10217 let mut app = test_app();
10218 app.current_service = Service::ApiGatewayApis;
10219 app.service_selected = true;
10220 app.mode = Mode::FilterInput;
10221 app.apig_state.input_focus = InputFocus::Filter;
10222
10223 app.handle_action(Action::PrevFilterFocus);
10225 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
10226
10227 app.handle_action(Action::PrevFilterFocus);
10229 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10230 }
10231
10232 #[test]
10233 fn test_apig_exact_user_flow_i_tab_filter() {
10234 let mut app = test_app();
10235 app.current_service = Service::ApiGatewayApis;
10236 app.service_selected = true;
10237 app.mode = Mode::Normal;
10238
10239 app.apig_state.apis.items = vec![
10241 crate::apig::api::RestApi {
10242 id: "api1".to_string(),
10243 name: "test-api".to_string(),
10244 description: "Test API".to_string(),
10245 created_date: "2023-01-01".to_string(),
10246 api_key_source: "HEADER".to_string(),
10247 endpoint_configuration: "REGIONAL".to_string(),
10248 protocol_type: "REST".to_string(),
10249 disable_execute_api_endpoint: false,
10250 status: "AVAILABLE".to_string(),
10251 },
10252 crate::apig::api::RestApi {
10253 id: "api2".to_string(),
10254 name: "prod-api".to_string(),
10255 description: "Production API".to_string(),
10256 created_date: "2023-01-02".to_string(),
10257 api_key_source: "HEADER".to_string(),
10258 endpoint_configuration: "REGIONAL".to_string(),
10259 protocol_type: "REST".to_string(),
10260 disable_execute_api_endpoint: false,
10261 status: "AVAILABLE".to_string(),
10262 },
10263 ];
10264
10265 app.handle_action(Action::StartFilter);
10267 assert_eq!(app.mode, Mode::FilterInput);
10268 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10269
10270 app.handle_action(Action::FilterInput('t'));
10272 app.handle_action(Action::FilterInput('e'));
10273 app.handle_action(Action::FilterInput('s'));
10274 app.handle_action(Action::FilterInput('t'));
10275 assert_eq!(app.apig_state.apis.filter, "test");
10276
10277 app.handle_action(Action::NextFilterFocus);
10279 assert_eq!(app.apig_state.input_focus, InputFocus::Pagination);
10280
10281 app.handle_action(Action::NextFilterFocus);
10283 assert_eq!(app.apig_state.input_focus, InputFocus::Filter);
10284 }
10285
10286 #[test]
10287 fn test_apig_row_expansion() {
10288 let mut app = test_app();
10289 app.current_service = Service::ApiGatewayApis;
10290 app.service_selected = true;
10291 app.apig_state.apis.items = vec![crate::apig::api::RestApi {
10292 id: "api1".to_string(),
10293 name: "test-api".to_string(),
10294 description: "Test API".to_string(),
10295 created_date: "2023-01-01".to_string(),
10296 api_key_source: "HEADER".to_string(),
10297 endpoint_configuration: "REGIONAL".to_string(),
10298 protocol_type: "REST".to_string(),
10299 disable_execute_api_endpoint: false,
10300 status: "AVAILABLE".to_string(),
10301 }];
10302 app.apig_state.apis.selected = 0;
10303
10304 assert_eq!(app.apig_state.apis.expanded_item, None);
10305
10306 app.handle_action(Action::NextPane);
10308 assert_eq!(app.apig_state.apis.expanded_item, None);
10309
10310 app.handle_action(Action::ExpandRow);
10312 assert_eq!(app.apig_state.apis.expanded_item, Some(0));
10313
10314 app.handle_action(Action::NextPane);
10316 assert_eq!(app.apig_state.apis.expanded_item, Some(0));
10317
10318 app.handle_action(Action::PrevPane);
10320 assert_eq!(app.apig_state.apis.expanded_item, None);
10321 }
10322
10323 #[test]
10324 fn test_apig_column_preferences() {
10325 let mut app = test_app();
10326 app.current_service = Service::ApiGatewayApis;
10327 app.service_selected = true;
10328
10329 let initial_count = app.apig_api_visible_column_ids.len();
10331 assert_eq!(initial_count, app.apig_api_column_ids.len());
10332
10333 app.handle_action(Action::OpenColumnSelector);
10335 assert_eq!(app.mode, Mode::ColumnSelector);
10336
10337 app.column_selector_index = 1;
10339 let first_col = app.apig_api_column_ids[0];
10340 assert!(app.apig_api_visible_column_ids.contains(&first_col));
10341
10342 app.handle_action(Action::ToggleColumn);
10343
10344 assert!(!app.apig_api_visible_column_ids.contains(&first_col));
10346 assert_eq!(app.apig_api_visible_column_ids.len(), initial_count - 1);
10347
10348 app.handle_action(Action::ToggleColumn);
10350 assert!(app.apig_api_visible_column_ids.contains(&first_col));
10351 assert_eq!(app.apig_api_visible_column_ids.len(), initial_count);
10352 }
10353
10354 #[test]
10355 fn test_apig_page_size_preferences() {
10356 use crate::common::PageSize;
10357
10358 let mut app = test_app();
10359 app.current_service = Service::ApiGatewayApis;
10360 app.service_selected = true;
10361
10362 assert_eq!(app.apig_state.apis.page_size, PageSize::Fifty);
10364
10365 app.handle_action(Action::OpenColumnSelector);
10367
10368 app.column_selector_index = app.apig_api_column_ids.len() + 3;
10373 app.handle_action(Action::ToggleColumn);
10374 assert_eq!(app.apig_state.apis.page_size, PageSize::Ten);
10375
10376 app.column_selector_index = app.apig_api_column_ids.len() + 6;
10378 app.handle_action(Action::ToggleColumn);
10379 assert_eq!(app.apig_state.apis.page_size, PageSize::OneHundred);
10380 }
10381
10382 #[test]
10383 fn test_apig_preferences_skip_blank_row() {
10384 let mut app = test_app();
10385 app.current_service = Service::ApiGatewayApis;
10386 app.service_selected = true;
10387 app.mode = Mode::ColumnSelector;
10388
10389 app.column_selector_index = 8;
10391
10392 app.handle_action(Action::NextItem);
10394 assert_eq!(app.column_selector_index, 10);
10395
10396 app.handle_action(Action::PrevItem);
10398 assert_eq!(app.column_selector_index, 8);
10399 }
10400
10401 #[test]
10402 fn test_apig_preferences_ctrl_d_skip_blank_row() {
10403 let mut app = test_app();
10404 app.current_service = Service::ApiGatewayApis;
10405 app.service_selected = true;
10406 app.mode = Mode::ColumnSelector;
10407
10408 app.column_selector_index = 0;
10410
10411 app.handle_action(Action::PageDown);
10413 assert_eq!(app.column_selector_index, 10);
10414
10415 app.handle_action(Action::PageUp);
10417 assert_eq!(app.column_selector_index, 0);
10418 }
10419
10420 #[test]
10421 fn test_apig_preferences_ctrl_u_skip_blank_row() {
10422 let mut app = test_app();
10423 app.current_service = Service::ApiGatewayApis;
10424 app.service_selected = true;
10425 app.mode = Mode::ColumnSelector;
10426
10427 app.column_selector_index = 10;
10429
10430 app.handle_action(Action::PageUp);
10432 assert_eq!(app.column_selector_index, 0);
10433
10434 app.column_selector_index = 14; app.handle_action(Action::PageUp);
10439 assert_eq!(app.column_selector_index, 4);
10440 }
10441
10442 #[test]
10443 fn test_apig_preferences_tab_cycles_sections() {
10444 let mut app = test_app();
10445 app.current_service = Service::ApiGatewayApis;
10446 app.service_selected = true;
10447 app.mode = Mode::ColumnSelector;
10448
10449 app.column_selector_index = 0;
10451
10452 app.handle_action(Action::NextPreferences);
10454 assert_eq!(app.column_selector_index, 10);
10455
10456 app.handle_action(Action::NextPreferences);
10458 assert_eq!(app.column_selector_index, 0);
10459 }
10460
10461 #[test]
10462 fn test_apig_preferences_shift_tab_cycles_sections() {
10463 let mut app = test_app();
10464 app.current_service = Service::ApiGatewayApis;
10465 app.service_selected = true;
10466 app.mode = Mode::ColumnSelector;
10467
10468 app.column_selector_index = 0;
10470
10471 app.handle_action(Action::PrevPreferences);
10473 assert_eq!(app.column_selector_index, 10);
10474
10475 app.handle_action(Action::PrevPreferences);
10477 assert_eq!(app.column_selector_index, 0);
10478 }
10479
10480 #[test]
10481 fn test_apig_arrow_navigation() {
10482 let mut app = test_app();
10483 app.current_service = Service::ApiGatewayApis;
10484 app.service_selected = true;
10485 app.mode = Mode::Normal; app.apig_state.apis.filter = String::new(); app.apig_state.apis.items = vec![
10488 crate::apig::api::RestApi {
10489 id: "api1".to_string(),
10490 name: "test-api-1".to_string(),
10491 description: "Test API 1".to_string(),
10492 created_date: "2023-01-01".to_string(),
10493 api_key_source: "HEADER".to_string(),
10494 endpoint_configuration: "REGIONAL".to_string(),
10495 protocol_type: "REST".to_string(),
10496 disable_execute_api_endpoint: false,
10497 status: "AVAILABLE".to_string(),
10498 },
10499 crate::apig::api::RestApi {
10500 id: "api2".to_string(),
10501 name: "test-api-2".to_string(),
10502 description: "Test API 2".to_string(),
10503 created_date: "2023-01-02".to_string(),
10504 api_key_source: "HEADER".to_string(),
10505 endpoint_configuration: "REGIONAL".to_string(),
10506 protocol_type: "HTTP".to_string(),
10507 disable_execute_api_endpoint: false,
10508 status: "AVAILABLE".to_string(),
10509 },
10510 ];
10511 app.apig_state.apis.selected = 0;
10512
10513 app.handle_action(Action::NextItem);
10515 assert_eq!(app.apig_state.apis.selected, 1);
10516
10517 app.handle_action(Action::PrevItem);
10519 assert_eq!(app.apig_state.apis.selected, 0);
10520 }
10521
10522 #[test]
10523 fn test_apig_collapse_row() {
10524 let mut app = test_app();
10525 app.current_service = Service::ApiGatewayApis;
10526 app.service_selected = true;
10527 app.apig_state.apis.items = vec![crate::apig::api::RestApi {
10528 id: "api1".to_string(),
10529 name: "test-api".to_string(),
10530 description: "Test API".to_string(),
10531 created_date: "2023-01-01".to_string(),
10532 api_key_source: "HEADER".to_string(),
10533 endpoint_configuration: "REGIONAL".to_string(),
10534 protocol_type: "REST".to_string(),
10535 disable_execute_api_endpoint: false,
10536 status: "AVAILABLE".to_string(),
10537 }];
10538 app.apig_state.apis.selected = 0;
10539 app.apig_state.apis.expanded_item = Some(0);
10540
10541 app.handle_action(Action::CollapseRow);
10543 assert_eq!(app.apig_state.apis.expanded_item, None);
10544 }
10545
10546 #[test]
10547 fn test_apig_filter_resets_selection() {
10548 let mut app = test_app();
10549 app.current_service = Service::ApiGatewayApis;
10550 app.service_selected = true;
10551 app.mode = Mode::FilterInput;
10552 app.apig_state.input_focus = InputFocus::Filter;
10553 app.apig_state.apis.items = vec![
10554 crate::apig::api::RestApi {
10555 id: "api1".to_string(),
10556 name: "alpha-api".to_string(),
10557 description: "Alpha API".to_string(),
10558 created_date: "2023-01-01".to_string(),
10559 api_key_source: "HEADER".to_string(),
10560 endpoint_configuration: "REGIONAL".to_string(),
10561 protocol_type: "REST".to_string(),
10562 disable_execute_api_endpoint: false,
10563 status: "AVAILABLE".to_string(),
10564 },
10565 crate::apig::api::RestApi {
10566 id: "api2".to_string(),
10567 name: "beta-api".to_string(),
10568 description: "Beta API".to_string(),
10569 created_date: "2023-01-02".to_string(),
10570 api_key_source: "HEADER".to_string(),
10571 endpoint_configuration: "REGIONAL".to_string(),
10572 protocol_type: "HTTP".to_string(),
10573 disable_execute_api_endpoint: false,
10574 status: "AVAILABLE".to_string(),
10575 },
10576 ];
10577
10578 app.apig_state.apis.selected = 1;
10580 app.apig_state.apis.expanded_item = Some(1);
10581
10582 app.handle_action(Action::FilterInput('a'));
10584
10585 assert_eq!(app.apig_state.apis.selected, 0);
10587 assert_eq!(app.apig_state.apis.expanded_item, None);
10588 assert_eq!(app.apig_state.apis.filter, "a");
10589 }
10590
10591 #[test]
10592 fn test_service_picker_starts_in_normal_mode() {
10593 let app = test_app();
10594 assert_eq!(app.mode, Mode::ServicePicker);
10595 assert!(!app.service_picker.filter_active);
10596 }
10597
10598 #[test]
10599 fn test_service_picker_i_key_activates_filter() {
10600 let mut app = test_app();
10601
10602 assert_eq!(app.mode, Mode::ServicePicker);
10604 assert!(!app.service_picker.filter_active);
10605 assert!(app.service_picker.filter.is_empty());
10606
10607 app.handle_action(Action::EnterFilterMode);
10609
10610 assert_eq!(app.mode, Mode::ServicePicker);
10612 assert!(app.service_picker.filter_active);
10613 assert!(app.service_picker.filter.is_empty());
10614
10615 app.handle_action(Action::FilterInput('s'));
10617 assert_eq!(app.service_picker.filter, "s");
10618 }
10619
10620 #[test]
10621 fn test_service_picker_esc_exits_filter_mode() {
10622 let mut app = test_app();
10623 assert_eq!(app.mode, Mode::ServicePicker);
10624 assert!(!app.service_picker.filter_active);
10625
10626 app.handle_action(Action::EnterFilterMode);
10628 assert!(app.service_picker.filter_active);
10629 assert_eq!(app.mode, Mode::ServicePicker);
10630
10631 app.handle_action(Action::FilterInput('s'));
10633 assert_eq!(app.service_picker.filter, "s");
10634
10635 app.handle_action(Action::ExitFilterMode);
10637 assert!(!app.service_picker.filter_active);
10638 assert_eq!(app.mode, Mode::ServicePicker); app.handle_action(Action::ExitFilterMode);
10642 assert_eq!(app.mode, Mode::Normal);
10643 }
10644
10645 #[test]
10646 fn test_service_picker_navigation_only_works_in_normal_mode() {
10647 let mut app = test_app();
10648 app.service_picker.selected = 0;
10649
10650 assert!(!app.service_picker.filter_active);
10652 app.handle_action(Action::NextItem);
10653 assert_eq!(app.service_picker.selected, 1);
10654
10655 app.handle_action(Action::EnterFilterMode);
10657 assert!(app.service_picker.filter_active);
10658
10659 let prev_selected = app.service_picker.selected;
10661 app.handle_action(Action::NextItem);
10662 assert_eq!(app.service_picker.selected, prev_selected); app.handle_action(Action::ExitFilterMode);
10666 assert!(!app.service_picker.filter_active);
10667
10668 app.handle_action(Action::NextItem);
10670 assert_eq!(app.service_picker.selected, prev_selected + 1);
10671 }
10672
10673 #[test]
10674 fn test_service_picker_typing_filters_services() {
10675 let mut app = test_app();
10676
10677 assert_eq!(app.mode, Mode::ServicePicker);
10679 assert!(!app.service_picker.filter_active);
10680
10681 app.handle_action(Action::EnterFilterMode);
10683 assert!(app.service_picker.filter_active);
10684
10685 app.handle_action(Action::FilterInput('s'));
10687 app.handle_action(Action::FilterInput('3'));
10688
10689 assert_eq!(app.service_picker.filter, "s3");
10690 assert_eq!(app.mode, Mode::ServicePicker);
10691 }
10692
10693 #[test]
10694 fn test_service_picker_enter_exits_insert_mode() {
10695 let mut app = test_app();
10696 assert_eq!(app.mode, Mode::ServicePicker);
10697 assert!(!app.service_selected);
10698
10699 app.handle_action(Action::EnterFilterMode);
10701 assert!(app.service_picker.filter_active);
10702
10703 app.handle_action(Action::FilterInput('s'));
10705 app.handle_action(Action::FilterInput('3'));
10706
10707 app.handle_action(Action::Select);
10709 assert!(!app.service_selected, "Should not select service");
10710 assert!(!app.service_picker.filter_active, "Should exit INSERT mode");
10711 assert_eq!(app.mode, Mode::ServicePicker);
10712
10713 app.handle_action(Action::Select);
10715 assert!(app.service_selected, "Should select service in NORMAL mode");
10716 assert_eq!(app.mode, Mode::Normal);
10717 }
10718
10719 #[test]
10720 fn test_service_picker_resets_on_open() {
10721 let mut app = test_app();
10722
10723 app.service_selected = true;
10725 app.mode = Mode::Normal;
10726
10727 app.service_picker.filter = "previous".to_string();
10729 app.service_picker.filter_active = true;
10730 app.service_picker.selected = 5;
10731
10732 app.handle_action(Action::OpenSpaceMenu);
10734
10735 assert_eq!(app.mode, Mode::SpaceMenu);
10737 assert!(app.service_picker.filter.is_empty());
10738 assert!(!app.service_picker.filter_active);
10739 assert_eq!(app.service_picker.selected, 0);
10740 }
10741
10742 #[test]
10743 fn test_kms_in_service_picker_services_list() {
10744 let app = test_app();
10745 assert!(
10746 app.service_picker.services.contains(&"KMS › Managed Keys"),
10747 "KMS › Managed Keys must be in the service picker list"
10748 );
10749 }
10750
10751 #[test]
10752 fn test_kms_service_picker_selects_kms_service() {
10753 let mut app = test_app();
10754 app.mode = Mode::ServicePicker;
10755 app.service_picker.filter_active = true;
10756
10757 for c in ['k', 'm', 's'] {
10759 app.handle_action(Action::FilterInput(c));
10760 }
10761
10762 assert_eq!(app.service_picker.filter, "kms");
10763
10764 let filtered: Vec<_> = app
10766 .service_picker
10767 .services
10768 .iter()
10769 .filter(|s| s.to_lowercase().contains("kms"))
10770 .collect();
10771 assert_eq!(filtered.len(), 1);
10772 assert_eq!(*filtered[0], "KMS › Managed Keys");
10773 }
10774
10775 #[test]
10776 fn test_selecting_kms_in_picker_switches_service() {
10777 let mut app = test_app();
10778 app.mode = Mode::ServicePicker;
10779 app.service_selected = false;
10780
10781 let kms_idx = app
10784 .filtered_services()
10785 .iter()
10786 .position(|&s| s == "KMS › Managed Keys")
10787 .expect("KMS › Managed Keys must be in services list");
10788 app.service_picker.selected = kms_idx;
10789
10790 app.handle_action(Action::Select);
10791
10792 assert_eq!(app.current_service, Service::KmsKeys);
10793 assert!(app.service_selected);
10794 }
10795
10796 #[test]
10797 fn test_kms_picker_creates_session_tab_with_correct_title() {
10798 let mut app = test_app();
10800 app.mode = Mode::ServicePicker;
10801 app.service_selected = false;
10802
10803 let kms_idx = app
10804 .filtered_services()
10805 .iter()
10806 .position(|&s| s == "KMS › Managed Keys")
10807 .unwrap();
10808 app.service_picker.selected = kms_idx;
10809 app.handle_action(Action::Select);
10810
10811 assert!(!app.tabs.is_empty(), "a session tab must be created");
10812 let tab = &app.tabs[app.current_tab];
10813 assert_eq!(
10814 tab.title, "KMS › Managed Keys",
10815 "session tab title must match picker label"
10816 );
10817 assert_eq!(tab.service, Service::KmsKeys);
10818 }
10819
10820 #[test]
10821 fn test_kms_tab_switch_does_not_trigger_reload() {
10822 use crate::ui::kms::Tab as KmsTab;
10825 let mut app = test_app();
10826 app.current_service = Service::KmsKeys;
10827 app.service_selected = true;
10828 app.mode = Mode::Normal;
10829 app.kms_state.tab = KmsTab::AwsManaged;
10830 app.kms_state.keys.loading = false;
10831
10832 app.handle_action(Action::NextDetailTab);
10833
10834 assert_eq!(app.kms_state.tab, KmsTab::CustomerManaged);
10835 assert!(
10836 !app.kms_state.keys.loading,
10837 "tab switch must NOT trigger a reload — keys are filtered client-side"
10838 );
10839 }
10840
10841 #[test]
10842 fn test_kms_tab_switching_cycles() {
10843 use crate::ui::kms::Tab as KmsTab;
10844 let mut app = test_app();
10845 app.current_service = Service::KmsKeys;
10846 app.service_selected = true;
10847 app.mode = Mode::Normal;
10848
10849 assert_eq!(app.kms_state.tab, KmsTab::AwsManaged);
10850
10851 app.handle_action(Action::NextDetailTab);
10852 assert_eq!(app.kms_state.tab, KmsTab::CustomerManaged);
10853
10854 app.handle_action(Action::NextDetailTab);
10855 assert_eq!(app.kms_state.tab, KmsTab::AwsManaged, "must wrap around");
10856
10857 app.handle_action(Action::PrevDetailTab);
10858 assert_eq!(app.kms_state.tab, KmsTab::CustomerManaged);
10859 }
10860
10861 #[test]
10862 fn test_kms_load_condition_triggers_on_loading_flag() {
10863 let mut app = test_app();
10867 app.current_service = Service::KmsKeys;
10868 app.service_selected = true;
10869 app.kms_state.keys.loading = false;
10870
10871 app.kms_state.keys.loading = true;
10873
10874 assert!(
10877 app.kms_state.keys.loading,
10878 "loading flag must be true after region change to trigger reload"
10879 );
10880 }
10881
10882 #[test]
10883 fn test_kms_session_tab_survives_service_switch() {
10884 let mut app = test_app();
10887 app.mode = Mode::ServicePicker;
10888 app.service_selected = false;
10889
10890 let kms_idx = app
10891 .filtered_services()
10892 .iter()
10893 .position(|&s| s == "KMS › Managed Keys")
10894 .unwrap();
10895 app.service_picker.selected = kms_idx;
10896 app.handle_action(Action::Select);
10897
10898 assert_eq!(app.tabs.len(), 1);
10899 assert_eq!(app.tabs[0].title, "KMS › Managed Keys");
10900 assert_eq!(app.tabs[0].service, Service::KmsKeys);
10901 assert_eq!(app.current_tab, 0);
10902 }
10903
10904 #[test]
10905 fn test_no_pii_in_test_data() {
10906 let test_repo = EcrRepository {
10908 name: "test-repo".to_string(),
10909 uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/test-repo".to_string(),
10910 created_at: "2024-01-01".to_string(),
10911 tag_immutability: "MUTABLE".to_string(),
10912 encryption_type: "AES256".to_string(),
10913 };
10914
10915 assert!(test_repo.uri.starts_with("123456789012"));
10917 assert!(!test_repo.uri.contains("123456789013")); }
10919
10920 #[test]
10921 fn test_lambda_versions_tab_triggers_loading() {
10922 let mut app = test_app();
10923 app.current_service = Service::LambdaFunctions;
10924 app.service_selected = true;
10925
10926 app.lambda_state.current_function = Some("test-function".to_string());
10928 app.lambda_state.detail_tab = LambdaDetailTab::Code;
10929
10930 assert!(app.lambda_state.version_table.items.is_empty());
10932
10933 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10935
10936 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
10939 assert!(app.lambda_state.current_function.is_some());
10940 }
10941
10942 #[test]
10943 fn test_lambda_versions_navigation() {
10944 let mut app = test_app();
10945 app.current_service = Service::LambdaFunctions;
10946 app.service_selected = true;
10947 app.lambda_state.current_function = Some("test-function".to_string());
10948 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
10949
10950 app.lambda_state.version_table.items = vec![
10952 LambdaVersion {
10953 version: "3".to_string(),
10954 aliases: "prod".to_string(),
10955 description: "".to_string(),
10956 last_modified: "".to_string(),
10957 architecture: "X86_64".to_string(),
10958 },
10959 LambdaVersion {
10960 version: "2".to_string(),
10961 aliases: "".to_string(),
10962 description: "".to_string(),
10963 last_modified: "".to_string(),
10964 architecture: "X86_64".to_string(),
10965 },
10966 LambdaVersion {
10967 version: "1".to_string(),
10968 aliases: "".to_string(),
10969 description: "".to_string(),
10970 last_modified: "".to_string(),
10971 architecture: "X86_64".to_string(),
10972 },
10973 ];
10974
10975 assert_eq!(app.lambda_state.version_table.items.len(), 3);
10977 assert_eq!(app.lambda_state.version_table.items[0].version, "3");
10978 assert_eq!(app.lambda_state.version_table.items[0].aliases, "prod");
10979
10980 app.lambda_state.version_table.selected = 1;
10982 assert_eq!(app.lambda_state.version_table.selected, 1);
10983 }
10984
10985 #[test]
10986 fn test_lambda_versions_with_aliases() {
10987 let version = LambdaVersion {
10988 version: "35".to_string(),
10989 aliases: "prod, staging".to_string(),
10990 description: "Production version".to_string(),
10991 last_modified: "2024-01-01".to_string(),
10992 architecture: "X86_64".to_string(),
10993 };
10994
10995 assert_eq!(version.aliases, "prod, staging");
10996 assert!(!version.aliases.is_empty());
10997 }
10998
10999 #[test]
11000 fn test_lambda_versions_expansion() {
11001 let mut app = test_app();
11002 app.current_service = Service::LambdaFunctions;
11003 app.service_selected = true;
11004 app.lambda_state.current_function = Some("test-function".to_string());
11005 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11006
11007 app.lambda_state.version_table.items = vec![
11009 LambdaVersion {
11010 version: "2".to_string(),
11011 aliases: "prod".to_string(),
11012 description: "Production".to_string(),
11013 last_modified: "2024-01-01".to_string(),
11014 architecture: "X86_64".to_string(),
11015 },
11016 LambdaVersion {
11017 version: "1".to_string(),
11018 aliases: "".to_string(),
11019 description: "".to_string(),
11020 last_modified: "2024-01-01".to_string(),
11021 architecture: "Arm64".to_string(),
11022 },
11023 ];
11024
11025 app.lambda_state.version_table.selected = 0;
11026
11027 app.lambda_state.version_table.expanded_item = Some(0);
11029 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
11030
11031 app.lambda_state.version_table.selected = 1;
11033 app.lambda_state.version_table.expanded_item = Some(1);
11034 assert_eq!(app.lambda_state.version_table.expanded_item, Some(1));
11035 }
11036
11037 #[test]
11038 fn test_lambda_versions_page_navigation() {
11039 let mut app = test_app();
11040 app.current_service = Service::LambdaFunctions;
11041 app.service_selected = true;
11042 app.lambda_state.current_function = Some("test-function".to_string());
11043 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11044
11045 app.lambda_state.version_table.items = (1..=30)
11047 .map(|i| LambdaVersion {
11048 version: i.to_string(),
11049 aliases: "".to_string(),
11050 description: "".to_string(),
11051 last_modified: "".to_string(),
11052 architecture: "X86_64".to_string(),
11053 })
11054 .collect();
11055
11056 app.lambda_state.version_table.page_size = PageSize::Ten;
11057 app.lambda_state.version_table.selected = 0;
11058
11059 app.page_input = "2".to_string();
11061 app.handle_action(Action::OpenColumnSelector);
11062
11063 assert_eq!(app.lambda_state.version_table.selected, 10);
11065 }
11066
11067 #[test]
11068 fn test_lambda_versions_pagination_arrow_keys() {
11069 let mut app = test_app();
11070 app.current_service = Service::LambdaFunctions;
11071 app.service_selected = true;
11072 app.lambda_state.current_function = Some("test-function".to_string());
11073 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11074 app.mode = Mode::FilterInput;
11075 app.lambda_state.version_input_focus = InputFocus::Pagination;
11076
11077 app.lambda_state.version_table.items = (1..=30)
11079 .map(|i| LambdaVersion {
11080 version: i.to_string(),
11081 aliases: "".to_string(),
11082 description: "".to_string(),
11083 last_modified: "".to_string(),
11084 architecture: "X86_64".to_string(),
11085 })
11086 .collect();
11087
11088 app.lambda_state.version_table.page_size = PageSize::Ten;
11089 app.lambda_state.version_table.selected = 0;
11090
11091 app.handle_action(Action::PageDown);
11093 assert_eq!(app.lambda_state.version_table.selected, 10);
11094
11095 app.handle_action(Action::PageUp);
11097 assert_eq!(app.lambda_state.version_table.selected, 0);
11098 }
11099
11100 #[test]
11101 fn test_lambda_versions_page_input_in_filter_mode() {
11102 let mut app = test_app();
11103 app.current_service = Service::LambdaFunctions;
11104 app.service_selected = true;
11105 app.lambda_state.current_function = Some("test-function".to_string());
11106 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11107 app.mode = Mode::FilterInput;
11108 app.lambda_state.version_input_focus = InputFocus::Pagination;
11109
11110 app.lambda_state.version_table.items = (1..=30)
11112 .map(|i| LambdaVersion {
11113 version: i.to_string(),
11114 aliases: "".to_string(),
11115 description: "".to_string(),
11116 last_modified: "".to_string(),
11117 architecture: "X86_64".to_string(),
11118 })
11119 .collect();
11120
11121 app.lambda_state.version_table.page_size = PageSize::Ten;
11122 app.lambda_state.version_table.selected = 0;
11123
11124 app.handle_action(Action::FilterInput('2'));
11126 assert_eq!(app.page_input, "2");
11127 assert_eq!(app.lambda_state.version_table.filter, ""); app.handle_action(Action::OpenColumnSelector);
11131 assert_eq!(app.lambda_state.version_table.selected, 10);
11132 assert_eq!(app.page_input, ""); }
11134
11135 #[test]
11136 fn test_lambda_versions_filter_input() {
11137 let mut app = test_app();
11138 app.current_service = Service::LambdaFunctions;
11139 app.service_selected = true;
11140 app.lambda_state.current_function = Some("test-function".to_string());
11141 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11142 app.mode = Mode::FilterInput;
11143 app.lambda_state.version_input_focus = InputFocus::Filter;
11144
11145 app.lambda_state.version_table.items = vec![
11147 LambdaVersion {
11148 version: "1".to_string(),
11149 aliases: "prod".to_string(),
11150 description: "Production".to_string(),
11151 last_modified: "".to_string(),
11152 architecture: "X86_64".to_string(),
11153 },
11154 LambdaVersion {
11155 version: "2".to_string(),
11156 aliases: "staging".to_string(),
11157 description: "Staging".to_string(),
11158 last_modified: "".to_string(),
11159 architecture: "X86_64".to_string(),
11160 },
11161 ];
11162
11163 app.handle_action(Action::FilterInput('p'));
11165 app.handle_action(Action::FilterInput('r'));
11166 app.handle_action(Action::FilterInput('o'));
11167 app.handle_action(Action::FilterInput('d'));
11168 assert_eq!(app.lambda_state.version_table.filter, "prod");
11169
11170 app.handle_action(Action::FilterBackspace);
11172 assert_eq!(app.lambda_state.version_table.filter, "pro");
11173 }
11174
11175 #[test]
11176 fn test_lambda_aliases_table_expansion() {
11177 use crate::lambda::Alias;
11178
11179 let mut app = test_app();
11180 app.current_service = Service::LambdaFunctions;
11181 app.service_selected = true;
11182 app.lambda_state.current_function = Some("test-function".to_string());
11183 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
11184 app.mode = Mode::Normal;
11185
11186 app.lambda_state.alias_table.items = vec![
11187 Alias {
11188 name: "prod".to_string(),
11189 versions: "1".to_string(),
11190 description: "Production alias".to_string(),
11191 },
11192 Alias {
11193 name: "staging".to_string(),
11194 versions: "2".to_string(),
11195 description: "Staging alias".to_string(),
11196 },
11197 ];
11198
11199 app.lambda_state.alias_table.selected = 0;
11200
11201 app.handle_action(Action::Select);
11203 assert_eq!(app.lambda_state.current_alias, Some("prod".to_string()));
11204 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11205
11206 app.handle_action(Action::GoBack);
11208 assert_eq!(app.lambda_state.current_alias, None);
11209 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11210
11211 app.lambda_state.alias_table.selected = 1;
11213 app.handle_action(Action::Select);
11214 assert_eq!(app.lambda_state.current_alias, Some("staging".to_string()));
11215 }
11216
11217 #[test]
11218 fn test_lambda_versions_arrow_key_expansion() {
11219 let mut app = test_app();
11220 app.current_service = Service::LambdaFunctions;
11221 app.service_selected = true;
11222 app.lambda_state.current_function = Some("test-function".to_string());
11223 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11224 app.mode = Mode::Normal;
11225
11226 app.lambda_state.version_table.items = vec![LambdaVersion {
11227 version: "1".to_string(),
11228 aliases: "prod".to_string(),
11229 description: "Production".to_string(),
11230 last_modified: "2024-01-01".to_string(),
11231 architecture: "X86_64".to_string(),
11232 }];
11233
11234 app.lambda_state.version_table.selected = 0;
11235
11236 app.handle_action(Action::NextPane);
11238 assert_eq!(app.lambda_state.version_table.expanded_item, Some(0));
11239
11240 app.handle_action(Action::PrevPane);
11242 assert_eq!(app.lambda_state.version_table.expanded_item, None);
11243 }
11244
11245 #[test]
11246 fn test_lambda_version_detail_view() {
11247 use crate::lambda::Function;
11248
11249 let mut app = test_app();
11250 app.current_service = Service::LambdaFunctions;
11251 app.service_selected = true;
11252 app.lambda_state.current_function = Some("test-function".to_string());
11253 app.lambda_state.detail_tab = LambdaDetailTab::Versions;
11254 app.mode = Mode::Normal;
11255
11256 app.lambda_state.table.items = vec![Function {
11257 name: "test-function".to_string(),
11258 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11259 application: None,
11260 description: "Test".to_string(),
11261 package_type: "Zip".to_string(),
11262 runtime: "python3.12".to_string(),
11263 architecture: "X86_64".to_string(),
11264 code_size: 1024,
11265 code_sha256: "hash".to_string(),
11266 memory_mb: 128,
11267 timeout_seconds: 30,
11268 last_modified: "2024-01-01".to_string(),
11269 layers: vec![],
11270 }];
11271
11272 app.lambda_state.version_table.items = vec![LambdaVersion {
11273 version: "1".to_string(),
11274 aliases: "prod".to_string(),
11275 description: "Production".to_string(),
11276 last_modified: "2024-01-01".to_string(),
11277 architecture: "X86_64".to_string(),
11278 }];
11279
11280 app.lambda_state.version_table.selected = 0;
11281
11282 app.handle_action(Action::Select);
11284 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11285 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
11286
11287 app.handle_action(Action::GoBack);
11289 assert_eq!(app.lambda_state.current_version, None);
11290 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11291 }
11292
11293 #[test]
11294 fn test_lambda_version_detail_tabs() {
11295 use crate::lambda::Function;
11296
11297 let mut app = test_app();
11298 app.current_service = Service::LambdaFunctions;
11299 app.service_selected = true;
11300 app.lambda_state.current_function = Some("test-function".to_string());
11301 app.lambda_state.current_version = Some("1".to_string());
11302 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11303 app.mode = Mode::Normal;
11304
11305 app.lambda_state.table.items = vec![Function {
11306 name: "test-function".to_string(),
11307 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11308 application: None,
11309 description: "Test".to_string(),
11310 package_type: "Zip".to_string(),
11311 runtime: "python3.12".to_string(),
11312 architecture: "X86_64".to_string(),
11313 code_size: 1024,
11314 code_sha256: "hash".to_string(),
11315 memory_mb: 128,
11316 timeout_seconds: 30,
11317 last_modified: "2024-01-01".to_string(),
11318 layers: vec![],
11319 }];
11320
11321 app.handle_action(Action::NextDetailTab);
11323 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
11324
11325 app.handle_action(Action::NextDetailTab);
11326 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
11327
11328 app.handle_action(Action::NextDetailTab);
11329 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
11330
11331 app.handle_action(Action::PrevDetailTab);
11333 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
11334
11335 app.handle_action(Action::PrevDetailTab);
11336 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
11337 }
11338
11339 #[test]
11340 fn test_lambda_aliases_arrow_key_expansion() {
11341 use crate::lambda::Alias;
11342
11343 let mut app = test_app();
11344 app.current_service = Service::LambdaFunctions;
11345 app.service_selected = true;
11346 app.lambda_state.current_function = Some("test-function".to_string());
11347 app.lambda_state.detail_tab = LambdaDetailTab::Aliases;
11348 app.mode = Mode::Normal;
11349
11350 app.lambda_state.alias_table.items = vec![Alias {
11351 name: "prod".to_string(),
11352 versions: "1".to_string(),
11353 description: "Production alias".to_string(),
11354 }];
11355
11356 app.lambda_state.alias_table.selected = 0;
11357
11358 app.handle_action(Action::NextPane);
11360 assert_eq!(app.lambda_state.alias_table.expanded_item, Some(0));
11361
11362 app.handle_action(Action::PrevPane);
11364 assert_eq!(app.lambda_state.alias_table.expanded_item, None);
11365 }
11366
11367 #[test]
11368 fn test_lambda_functions_arrow_key_expansion() {
11369 use crate::lambda::Function;
11370
11371 let mut app = test_app();
11372 app.current_service = Service::LambdaFunctions;
11373 app.service_selected = true;
11374 app.mode = Mode::Normal;
11375
11376 app.lambda_state.table.items = vec![Function {
11377 name: "test-function".to_string(),
11378 arn: "arn".to_string(),
11379 application: None,
11380 description: "Test".to_string(),
11381 package_type: "Zip".to_string(),
11382 runtime: "python3.12".to_string(),
11383 architecture: "X86_64".to_string(),
11384 code_size: 1024,
11385 code_sha256: "hash".to_string(),
11386 memory_mb: 128,
11387 timeout_seconds: 30,
11388 last_modified: "2024-01-01".to_string(),
11389 layers: vec![],
11390 }];
11391
11392 app.lambda_state.table.selected = 0;
11393
11394 app.handle_action(Action::NextPane);
11396 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
11397
11398 app.handle_action(Action::PrevPane);
11400 assert_eq!(app.lambda_state.table.expanded_item, None);
11401 }
11402
11403 #[test]
11404 fn test_lambda_version_detail_with_application() {
11405 use crate::lambda::Function;
11406
11407 let mut app = test_app();
11408 app.current_service = Service::LambdaFunctions;
11409 app.service_selected = true;
11410 app.lambda_state.current_function = Some("storefront-studio-beta-api".to_string());
11411 app.lambda_state.current_version = Some("1".to_string());
11412 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11413 app.mode = Mode::Normal;
11414
11415 app.lambda_state.table.items = vec![Function {
11416 name: "storefront-studio-beta-api".to_string(),
11417 arn: "arn:aws:lambda:us-east-1:123456789012:function:storefront-studio-beta-api"
11418 .to_string(),
11419 application: Some("storefront-studio-beta".to_string()),
11420 description: "API function".to_string(),
11421 package_type: "Zip".to_string(),
11422 runtime: "python3.12".to_string(),
11423 architecture: "X86_64".to_string(),
11424 code_size: 1024,
11425 code_sha256: "hash".to_string(),
11426 memory_mb: 128,
11427 timeout_seconds: 30,
11428 last_modified: "2024-01-01".to_string(),
11429 layers: vec![],
11430 }];
11431
11432 assert_eq!(
11434 app.lambda_state.table.items[0].application,
11435 Some("storefront-studio-beta".to_string())
11436 );
11437 assert_eq!(app.lambda_state.current_version, Some("1".to_string()));
11438 }
11439
11440 #[test]
11441 fn test_lambda_layer_navigation() {
11442 use crate::lambda::{Function, Layer};
11443
11444 let mut app = test_app();
11445 app.current_service = Service::LambdaFunctions;
11446 app.service_selected = true;
11447 app.lambda_state.current_function = Some("test-function".to_string());
11448 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11449 app.mode = Mode::Normal;
11450
11451 app.lambda_state.table.items = vec![Function {
11452 name: "test-function".to_string(),
11453 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11454 application: None,
11455 description: "Test".to_string(),
11456 package_type: "Zip".to_string(),
11457 runtime: "python3.12".to_string(),
11458 architecture: "X86_64".to_string(),
11459 code_size: 1024,
11460 code_sha256: "hash".to_string(),
11461 memory_mb: 128,
11462 timeout_seconds: 30,
11463 last_modified: "2024-01-01".to_string(),
11464 layers: vec![
11465 Layer {
11466 merge_order: "1".to_string(),
11467 name: "layer1".to_string(),
11468 layer_version: "1".to_string(),
11469 compatible_runtimes: "python3.9".to_string(),
11470 compatible_architectures: "x86_64".to_string(),
11471 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11472 },
11473 Layer {
11474 merge_order: "2".to_string(),
11475 name: "layer2".to_string(),
11476 layer_version: "2".to_string(),
11477 compatible_runtimes: "python3.9".to_string(),
11478 compatible_architectures: "x86_64".to_string(),
11479 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11480 },
11481 Layer {
11482 merge_order: "3".to_string(),
11483 name: "layer3".to_string(),
11484 layer_version: "3".to_string(),
11485 compatible_runtimes: "python3.9".to_string(),
11486 compatible_architectures: "x86_64".to_string(),
11487 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer3:3".to_string(),
11488 },
11489 ],
11490 }];
11491
11492 assert_eq!(app.lambda_state.layer_selected, 0);
11493
11494 app.handle_action(Action::NextItem);
11495 assert_eq!(app.lambda_state.layer_selected, 1);
11496
11497 app.handle_action(Action::NextItem);
11498 assert_eq!(app.lambda_state.layer_selected, 2);
11499
11500 app.handle_action(Action::NextItem);
11501 assert_eq!(app.lambda_state.layer_selected, 2);
11502
11503 app.handle_action(Action::PrevItem);
11504 assert_eq!(app.lambda_state.layer_selected, 1);
11505
11506 app.handle_action(Action::PrevItem);
11507 assert_eq!(app.lambda_state.layer_selected, 0);
11508
11509 app.handle_action(Action::PrevItem);
11510 assert_eq!(app.lambda_state.layer_selected, 0);
11511 }
11512
11513 #[test]
11514 fn test_lambda_layer_expansion() {
11515 use crate::lambda::{Function, Layer};
11516
11517 let mut app = test_app();
11518 app.current_service = Service::LambdaFunctions;
11519 app.service_selected = true;
11520 app.lambda_state.current_function = Some("test-function".to_string());
11521 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11522 app.mode = Mode::Normal;
11523
11524 app.lambda_state.table.items = vec![Function {
11525 name: "test-function".to_string(),
11526 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11527 application: None,
11528 description: "Test".to_string(),
11529 package_type: "Zip".to_string(),
11530 runtime: "python3.12".to_string(),
11531 architecture: "X86_64".to_string(),
11532 code_size: 1024,
11533 code_sha256: "hash".to_string(),
11534 memory_mb: 128,
11535 timeout_seconds: 30,
11536 last_modified: "2024-01-01".to_string(),
11537 layers: vec![Layer {
11538 merge_order: "1".to_string(),
11539 name: "test-layer".to_string(),
11540 layer_version: "1".to_string(),
11541 compatible_runtimes: "python3.9".to_string(),
11542 compatible_architectures: "x86_64".to_string(),
11543 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:test-layer:1".to_string(),
11544 }],
11545 }];
11546
11547 assert_eq!(app.lambda_state.layer_expanded, None);
11548
11549 app.handle_action(Action::NextPane);
11550 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11551
11552 app.handle_action(Action::PrevPane);
11553 assert_eq!(app.lambda_state.layer_expanded, None);
11554
11555 app.handle_action(Action::NextPane);
11556 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11557
11558 app.handle_action(Action::NextPane);
11559 assert_eq!(app.lambda_state.layer_expanded, None);
11560 }
11561
11562 #[test]
11563 fn test_lambda_layer_selection_and_expansion_workflow() {
11564 use crate::lambda::{Function, Layer};
11565
11566 let mut app = test_app();
11567 app.current_service = Service::LambdaFunctions;
11568 app.service_selected = true;
11569 app.lambda_state.current_function = Some("test-function".to_string());
11570 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11571 app.mode = Mode::Normal;
11572
11573 app.lambda_state.table.items = vec![Function {
11574 name: "test-function".to_string(),
11575 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function".to_string(),
11576 application: None,
11577 description: "Test".to_string(),
11578 package_type: "Zip".to_string(),
11579 runtime: "python3.12".to_string(),
11580 architecture: "X86_64".to_string(),
11581 code_size: 1024,
11582 code_sha256: "hash".to_string(),
11583 memory_mb: 128,
11584 timeout_seconds: 30,
11585 last_modified: "2024-01-01".to_string(),
11586 layers: vec![
11587 Layer {
11588 merge_order: "1".to_string(),
11589 name: "layer1".to_string(),
11590 layer_version: "1".to_string(),
11591 compatible_runtimes: "python3.9".to_string(),
11592 compatible_architectures: "x86_64".to_string(),
11593 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer1:1".to_string(),
11594 },
11595 Layer {
11596 merge_order: "2".to_string(),
11597 name: "layer2".to_string(),
11598 layer_version: "2".to_string(),
11599 compatible_runtimes: "python3.9".to_string(),
11600 compatible_architectures: "x86_64".to_string(),
11601 version_arn: "arn:aws:lambda:us-east-1:123456789012:layer:layer2:2".to_string(),
11602 },
11603 ],
11604 }];
11605
11606 assert_eq!(app.lambda_state.layer_selected, 0);
11608 assert_eq!(app.lambda_state.layer_expanded, None);
11609
11610 app.handle_action(Action::NextPane);
11612 assert_eq!(app.lambda_state.layer_selected, 0);
11613 assert_eq!(app.lambda_state.layer_expanded, Some(0));
11614
11615 app.handle_action(Action::NextItem);
11617 assert_eq!(app.lambda_state.layer_selected, 1);
11618 assert_eq!(app.lambda_state.layer_expanded, Some(0)); app.handle_action(Action::NextPane);
11622 assert_eq!(app.lambda_state.layer_selected, 1);
11623 assert_eq!(app.lambda_state.layer_expanded, Some(1));
11624
11625 app.handle_action(Action::PrevPane);
11627 assert_eq!(app.lambda_state.layer_selected, 1);
11628 assert_eq!(app.lambda_state.layer_expanded, None);
11629
11630 app.handle_action(Action::PrevItem);
11632 assert_eq!(app.lambda_state.layer_selected, 0);
11633 assert_eq!(app.lambda_state.layer_expanded, None);
11634 }
11635
11636 #[test]
11637 fn test_backtab_cycles_detail_tabs_backward() {
11638 let mut app = test_app();
11639 app.mode = Mode::Normal;
11640
11641 app.current_service = Service::LambdaFunctions;
11643 app.service_selected = true;
11644 app.lambda_state.current_function = Some("test-function".to_string());
11645 app.lambda_state.detail_tab = LambdaDetailTab::Code;
11646
11647 app.handle_action(Action::PrevDetailTab);
11648 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Versions);
11649
11650 app.handle_action(Action::PrevDetailTab);
11651 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Aliases);
11652
11653 app.current_service = Service::IamRoles;
11655 app.iam_state.current_role = Some("test-role".to_string());
11656 app.iam_state.role_tab = RoleTab::Permissions;
11657
11658 app.handle_action(Action::PrevDetailTab);
11659 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
11660
11661 app.current_service = Service::IamUsers;
11663 app.iam_state.current_user = Some("test-user".to_string());
11664 app.iam_state.user_tab = UserTab::Permissions;
11665
11666 app.handle_action(Action::PrevDetailTab);
11667 assert_eq!(app.iam_state.user_tab, UserTab::LastAccessed);
11668
11669 app.current_service = Service::IamUserGroups;
11671 app.iam_state.current_group = Some("test-group".to_string());
11672 app.iam_state.group_tab = GroupTab::Permissions;
11673
11674 app.handle_action(Action::PrevDetailTab);
11675 assert_eq!(app.iam_state.group_tab, GroupTab::Users);
11676
11677 app.current_service = Service::S3Buckets;
11679 app.s3_state.current_bucket = Some("test-bucket".to_string());
11680 app.s3_state.object_tab = S3ObjectTab::Properties;
11681
11682 app.handle_action(Action::PrevDetailTab);
11683 assert_eq!(app.s3_state.object_tab, S3ObjectTab::Objects);
11684
11685 app.current_service = Service::EcrRepositories;
11687 app.ecr_state.current_repository = None;
11688 app.ecr_state.tab = EcrTab::Private;
11689
11690 app.handle_action(Action::PrevDetailTab);
11691 assert_eq!(app.ecr_state.tab, EcrTab::Public);
11692
11693 app.current_service = Service::CloudFormationStacks;
11695 app.cfn_state.current_stack = Some("test-stack".to_string());
11696 app.cfn_state.detail_tab = CfnDetailTab::Resources;
11697 }
11698
11699 #[test]
11700 fn test_cloudformation_status_filter_active() {
11701 let filter = CfnStatusFilter::Active;
11702 assert!(filter.matches("CREATE_IN_PROGRESS"));
11703 assert!(filter.matches("UPDATE_IN_PROGRESS"));
11704 assert!(!filter.matches("CREATE_COMPLETE"));
11705 assert!(!filter.matches("DELETE_COMPLETE"));
11706 assert!(!filter.matches("CREATE_FAILED"));
11707 }
11708
11709 #[test]
11710 fn test_cloudformation_status_filter_complete() {
11711 let filter = CfnStatusFilter::Complete;
11712 assert!(filter.matches("CREATE_COMPLETE"));
11713 assert!(filter.matches("UPDATE_COMPLETE"));
11714 assert!(!filter.matches("DELETE_COMPLETE"));
11715 assert!(!filter.matches("CREATE_IN_PROGRESS"));
11716 }
11717
11718 #[test]
11719 fn test_cloudformation_status_filter_failed() {
11720 let filter = CfnStatusFilter::Failed;
11721 assert!(filter.matches("CREATE_FAILED"));
11722 assert!(filter.matches("UPDATE_FAILED"));
11723 assert!(!filter.matches("CREATE_COMPLETE"));
11724 }
11725
11726 #[test]
11727 fn test_cloudformation_status_filter_deleted() {
11728 let filter = CfnStatusFilter::Deleted;
11729 assert!(filter.matches("DELETE_COMPLETE"));
11730 assert!(filter.matches("DELETE_IN_PROGRESS"));
11731 assert!(!filter.matches("CREATE_COMPLETE"));
11732 }
11733
11734 #[test]
11735 fn test_cloudformation_status_filter_in_progress() {
11736 let filter = CfnStatusFilter::InProgress;
11737 assert!(filter.matches("CREATE_IN_PROGRESS"));
11738 assert!(filter.matches("UPDATE_IN_PROGRESS"));
11739 assert!(filter.matches("DELETE_IN_PROGRESS"));
11740 assert!(!filter.matches("CREATE_COMPLETE"));
11741 }
11742
11743 #[test]
11744 fn test_cloudformation_status_filter_cycle() {
11745 let filter = CfnStatusFilter::All;
11746 assert_eq!(filter.next(), CfnStatusFilter::Active);
11747 assert_eq!(filter.next().next(), CfnStatusFilter::Complete);
11748 assert_eq!(filter.next().next().next(), CfnStatusFilter::Failed);
11749 assert_eq!(filter.next().next().next().next(), CfnStatusFilter::Deleted);
11750 assert_eq!(
11751 filter.next().next().next().next().next(),
11752 CfnStatusFilter::InProgress
11753 );
11754 assert_eq!(
11755 filter.next().next().next().next().next().next(),
11756 CfnStatusFilter::All
11757 );
11758 }
11759
11760 #[test]
11761 fn test_cloudformation_default_columns() {
11762 let app = test_app();
11763 assert_eq!(app.cfn_visible_column_ids.len(), 4);
11764 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Name.id()));
11765 assert!(app.cfn_visible_column_ids.contains(&CfnColumn::Status.id()));
11766 assert!(app
11767 .cfn_visible_column_ids
11768 .contains(&CfnColumn::CreatedTime.id()));
11769 assert!(app
11770 .cfn_visible_column_ids
11771 .contains(&CfnColumn::Description.id()));
11772 }
11773
11774 #[test]
11775 fn test_cloudformation_all_columns() {
11776 let app = test_app();
11777 assert_eq!(app.cfn_column_ids.len(), 10);
11778 }
11779
11780 #[test]
11781 fn test_cloudformation_filter_by_name() {
11782 let mut app = test_app();
11783 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11784 app.cfn_state.table.items = vec![
11785 CfnStack {
11786 name: "my-stack".to_string(),
11787 stack_id: "id1".to_string(),
11788 status: "CREATE_COMPLETE".to_string(),
11789 created_time: "2024-01-01".to_string(),
11790 updated_time: String::new(),
11791 deleted_time: String::new(),
11792 drift_status: String::new(),
11793 last_drift_check_time: String::new(),
11794 status_reason: String::new(),
11795 description: String::new(),
11796 detailed_status: String::new(),
11797 root_stack: String::new(),
11798 parent_stack: String::new(),
11799 termination_protection: false,
11800 iam_role: String::new(),
11801 tags: Vec::new(),
11802 stack_policy: String::new(),
11803 rollback_monitoring_time: String::new(),
11804 rollback_alarms: Vec::new(),
11805 notification_arns: Vec::new(),
11806 },
11807 CfnStack {
11808 name: "other-stack".to_string(),
11809 stack_id: "id2".to_string(),
11810 status: "CREATE_COMPLETE".to_string(),
11811 created_time: "2024-01-02".to_string(),
11812 updated_time: String::new(),
11813 deleted_time: String::new(),
11814 drift_status: String::new(),
11815 last_drift_check_time: String::new(),
11816 status_reason: String::new(),
11817 description: String::new(),
11818 detailed_status: String::new(),
11819 root_stack: String::new(),
11820 parent_stack: String::new(),
11821 termination_protection: false,
11822 iam_role: String::new(),
11823 tags: Vec::new(),
11824 stack_policy: String::new(),
11825 rollback_monitoring_time: String::new(),
11826 rollback_alarms: Vec::new(),
11827 notification_arns: Vec::new(),
11828 },
11829 ];
11830
11831 app.cfn_state.table.filter = "my".to_string();
11832 let filtered = filtered_cloudformation_stacks(&app);
11833 assert_eq!(filtered.len(), 1);
11834 assert_eq!(filtered[0].name, "my-stack");
11835 }
11836
11837 #[test]
11838 fn test_cloudformation_filter_by_description() {
11839 let mut app = test_app();
11840 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11841 app.cfn_state.table.items = vec![CfnStack {
11842 name: "stack1".to_string(),
11843 stack_id: "id1".to_string(),
11844 status: "CREATE_COMPLETE".to_string(),
11845 created_time: "2024-01-01".to_string(),
11846 updated_time: String::new(),
11847 deleted_time: String::new(),
11848 drift_status: String::new(),
11849 last_drift_check_time: String::new(),
11850 status_reason: String::new(),
11851 description: "production stack".to_string(),
11852 detailed_status: String::new(),
11853 root_stack: String::new(),
11854 parent_stack: String::new(),
11855 termination_protection: false,
11856 iam_role: String::new(),
11857 tags: Vec::new(),
11858 stack_policy: String::new(),
11859 rollback_monitoring_time: String::new(),
11860 rollback_alarms: Vec::new(),
11861 notification_arns: Vec::new(),
11862 }];
11863
11864 app.cfn_state.table.filter = "production".to_string();
11865 let filtered = filtered_cloudformation_stacks(&app);
11866 assert_eq!(filtered.len(), 1);
11867 }
11868
11869 #[test]
11870 fn test_cloudformation_status_filter_applied() {
11871 let mut app = test_app();
11872 app.cfn_state.table.items = vec![
11873 CfnStack {
11874 name: "complete-stack".to_string(),
11875 stack_id: "id1".to_string(),
11876 status: "CREATE_COMPLETE".to_string(),
11877 created_time: "2024-01-01".to_string(),
11878 updated_time: String::new(),
11879 deleted_time: String::new(),
11880 drift_status: String::new(),
11881 last_drift_check_time: String::new(),
11882 status_reason: String::new(),
11883 description: String::new(),
11884 detailed_status: String::new(),
11885 root_stack: String::new(),
11886 parent_stack: String::new(),
11887 termination_protection: false,
11888 iam_role: String::new(),
11889 tags: Vec::new(),
11890 stack_policy: String::new(),
11891 rollback_monitoring_time: String::new(),
11892 rollback_alarms: Vec::new(),
11893 notification_arns: Vec::new(),
11894 },
11895 CfnStack {
11896 name: "failed-stack".to_string(),
11897 stack_id: "id2".to_string(),
11898 status: "CREATE_FAILED".to_string(),
11899 created_time: "2024-01-02".to_string(),
11900 updated_time: String::new(),
11901 deleted_time: String::new(),
11902 drift_status: String::new(),
11903 last_drift_check_time: String::new(),
11904 status_reason: String::new(),
11905 description: String::new(),
11906 detailed_status: String::new(),
11907 root_stack: String::new(),
11908 parent_stack: String::new(),
11909 termination_protection: false,
11910 iam_role: String::new(),
11911 tags: Vec::new(),
11912 stack_policy: String::new(),
11913 rollback_monitoring_time: String::new(),
11914 rollback_alarms: Vec::new(),
11915 notification_arns: Vec::new(),
11916 },
11917 ];
11918
11919 app.cfn_state.status_filter = CfnStatusFilter::Complete;
11920 let filtered = filtered_cloudformation_stacks(&app);
11921 assert_eq!(filtered.len(), 1);
11922 assert_eq!(filtered[0].name, "complete-stack");
11923
11924 app.cfn_state.status_filter = CfnStatusFilter::Failed;
11925 let filtered = filtered_cloudformation_stacks(&app);
11926 assert_eq!(filtered.len(), 1);
11927 assert_eq!(filtered[0].name, "failed-stack");
11928 }
11929
11930 #[test]
11931 fn test_cloudformation_default_page_size() {
11932 let app = test_app();
11933 assert_eq!(app.cfn_state.table.page_size, PageSize::Fifty);
11934 }
11935
11936 #[test]
11937 fn test_cloudformation_default_status_filter() {
11938 let app = test_app();
11939 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
11940 }
11941
11942 #[test]
11943 fn test_cloudformation_view_nested_default_false() {
11944 let app = test_app();
11945 assert!(!app.cfn_state.view_nested);
11946 }
11947
11948 #[test]
11949 fn test_cloudformation_pagination_hotkeys() {
11950 let mut app = test_app();
11951 app.current_service = Service::CloudFormationStacks;
11952 app.service_selected = true;
11953 app.cfn_state.status_filter = CfnStatusFilter::All;
11954
11955 for i in 0..150 {
11957 app.cfn_state.table.items.push(CfnStack {
11958 name: format!("stack-{}", i),
11959 stack_id: format!("id-{}", i),
11960 status: "CREATE_COMPLETE".to_string(),
11961 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
11962 updated_time: String::new(),
11963 deleted_time: String::new(),
11964 drift_status: String::new(),
11965 last_drift_check_time: String::new(),
11966 status_reason: String::new(),
11967 description: String::new(),
11968 detailed_status: String::new(),
11969 root_stack: String::new(),
11970 parent_stack: String::new(),
11971 termination_protection: false,
11972 iam_role: String::new(),
11973 tags: vec![],
11974 stack_policy: String::new(),
11975 rollback_monitoring_time: String::new(),
11976 rollback_alarms: vec![],
11977 notification_arns: vec![],
11978 });
11979 }
11980
11981 app.go_to_page(2);
11983 assert_eq!(app.cfn_state.table.selected, 50);
11984
11985 app.go_to_page(3);
11987 assert_eq!(app.cfn_state.table.selected, 100);
11988
11989 app.go_to_page(1);
11991 assert_eq!(app.cfn_state.table.selected, 0);
11992 }
11993
11994 #[test]
11995 fn test_cloudformation_tab_cycling_in_filter_mode() {
11996 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
11997 let mut app = test_app();
11998 app.current_service = Service::CloudFormationStacks;
11999 app.service_selected = true;
12000 app.mode = Mode::FilterInput;
12001 app.cfn_state.input_focus = InputFocus::Filter;
12002
12003 app.handle_action(Action::NextFilterFocus);
12005 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12006
12007 app.handle_action(Action::NextFilterFocus);
12009 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12010
12011 app.handle_action(Action::NextFilterFocus);
12013 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12014
12015 app.handle_action(Action::NextFilterFocus);
12017 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12018 }
12019
12020 #[test]
12021 fn test_cloudformation_timestamp_format_includes_utc() {
12022 let stack = CfnStack {
12023 name: "test-stack".to_string(),
12024 stack_id: "id-123".to_string(),
12025 status: "CREATE_COMPLETE".to_string(),
12026 created_time: "2025-08-07 15:38:02 (UTC)".to_string(),
12027 updated_time: "2025-08-08 10:00:00 (UTC)".to_string(),
12028 deleted_time: String::new(),
12029 drift_status: String::new(),
12030 last_drift_check_time: "2025-08-09 12:00:00 (UTC)".to_string(),
12031 status_reason: String::new(),
12032 description: String::new(),
12033 detailed_status: String::new(),
12034 root_stack: String::new(),
12035 parent_stack: String::new(),
12036 termination_protection: false,
12037 iam_role: String::new(),
12038 tags: vec![],
12039 stack_policy: String::new(),
12040 rollback_monitoring_time: String::new(),
12041 rollback_alarms: vec![],
12042 notification_arns: vec![],
12043 };
12044
12045 assert!(stack.created_time.contains("(UTC)"));
12046 assert!(stack.updated_time.contains("(UTC)"));
12047 assert!(stack.last_drift_check_time.contains("(UTC)"));
12048 assert_eq!(stack.created_time.len(), 25);
12049 }
12050
12051 #[test]
12052 fn test_cloudformation_enter_drills_into_stack_view() {
12053 let mut app = test_app();
12054 app.current_service = Service::CloudFormationStacks;
12055 app.service_selected = true;
12056 app.mode = Mode::Normal;
12057 app.cfn_state.status_filter = CfnStatusFilter::All;
12058 app.tabs = vec![Tab {
12059 service: Service::CloudFormationStacks,
12060 title: "CloudFormation › Stacks".to_string(),
12061 breadcrumb: "CloudFormation › Stacks".to_string(),
12062 }];
12063 app.current_tab = 0;
12064
12065 app.cfn_state.table.items.push(CfnStack {
12066 name: "test-stack".to_string(),
12067 stack_id: "id-123".to_string(),
12068 status: "CREATE_COMPLETE".to_string(),
12069 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12070 updated_time: String::new(),
12071 deleted_time: String::new(),
12072 drift_status: String::new(),
12073 last_drift_check_time: String::new(),
12074 status_reason: String::new(),
12075 description: String::new(),
12076 detailed_status: String::new(),
12077 root_stack: String::new(),
12078 parent_stack: String::new(),
12079 termination_protection: false,
12080 iam_role: String::new(),
12081 tags: vec![],
12082 stack_policy: String::new(),
12083 rollback_monitoring_time: String::new(),
12084 rollback_alarms: vec![],
12085 notification_arns: vec![],
12086 });
12087
12088 app.cfn_state.table.reset();
12089 assert_eq!(app.cfn_state.current_stack, None);
12090
12091 app.handle_action(Action::Select);
12093 assert_eq!(app.cfn_state.current_stack, Some("test-stack".to_string()));
12094 }
12095
12096 #[test]
12097 fn test_cloudformation_arrow_keys_expand_collapse() {
12098 let mut app = test_app();
12099 app.current_service = Service::CloudFormationStacks;
12100 app.service_selected = true;
12101 app.mode = Mode::Normal;
12102 app.cfn_state.status_filter = CfnStatusFilter::All;
12103
12104 app.cfn_state.table.items.push(CfnStack {
12105 name: "test-stack".to_string(),
12106 stack_id: "id-123".to_string(),
12107 status: "CREATE_COMPLETE".to_string(),
12108 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12109 updated_time: String::new(),
12110 deleted_time: String::new(),
12111 drift_status: String::new(),
12112 last_drift_check_time: String::new(),
12113 status_reason: String::new(),
12114 description: String::new(),
12115 detailed_status: String::new(),
12116 root_stack: String::new(),
12117 parent_stack: String::new(),
12118 termination_protection: false,
12119 iam_role: String::new(),
12120 tags: vec![],
12121 stack_policy: String::new(),
12122 rollback_monitoring_time: String::new(),
12123 rollback_alarms: vec![],
12124 notification_arns: vec![],
12125 });
12126
12127 app.cfn_state.table.reset();
12128 assert_eq!(app.cfn_state.table.expanded_item, None);
12129
12130 app.handle_action(Action::NextPane);
12132 assert_eq!(app.cfn_state.table.expanded_item, Some(0));
12133
12134 app.handle_action(Action::PrevPane);
12136 assert_eq!(app.cfn_state.table.expanded_item, None);
12137
12138 assert_eq!(app.cfn_state.current_stack, None);
12140 }
12141
12142 #[test]
12143 fn test_cloudformation_tab_cycling() {
12144 use crate::ui::cfn::DetailTab;
12145 let mut app = test_app();
12146 app.current_service = Service::CloudFormationStacks;
12147 app.service_selected = true;
12148 app.mode = Mode::Normal;
12149 app.cfn_state.status_filter = CfnStatusFilter::All;
12150 app.cfn_state.current_stack = Some("test-stack".to_string());
12151
12152 assert_eq!(app.cfn_state.detail_tab, DetailTab::StackInfo);
12153 }
12154
12155 #[test]
12156 fn test_cloudformation_console_url() {
12157 use crate::ui::cfn::DetailTab;
12158 let mut app = test_app();
12159 app.current_service = Service::CloudFormationStacks;
12160 app.service_selected = true;
12161 app.cfn_state.status_filter = CfnStatusFilter::All;
12162
12163 app.cfn_state.table.items.push(CfnStack {
12164 name: "test-stack".to_string(),
12165 stack_id: "arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/abc123"
12166 .to_string(),
12167 status: "CREATE_COMPLETE".to_string(),
12168 created_time: "2025-01-01 00:00:00 (UTC)".to_string(),
12169 updated_time: String::new(),
12170 deleted_time: String::new(),
12171 drift_status: String::new(),
12172 last_drift_check_time: String::new(),
12173 status_reason: String::new(),
12174 description: String::new(),
12175 detailed_status: String::new(),
12176 root_stack: String::new(),
12177 parent_stack: String::new(),
12178 termination_protection: false,
12179 iam_role: String::new(),
12180 tags: vec![],
12181 stack_policy: String::new(),
12182 rollback_monitoring_time: String::new(),
12183 rollback_alarms: vec![],
12184 notification_arns: vec![],
12185 });
12186
12187 app.cfn_state.current_stack = Some("test-stack".to_string());
12188
12189 app.cfn_state.detail_tab = DetailTab::StackInfo;
12191 let url = app.get_console_url();
12192 assert!(url.contains("stackinfo"));
12193 assert!(url.contains("arn%3Aaws%3Acloudformation"));
12194
12195 app.cfn_state.detail_tab = DetailTab::Events;
12197 let url = app.get_console_url();
12198 assert!(url.contains("events"));
12199 assert!(url.contains("arn%3Aaws%3Acloudformation"));
12200 }
12201
12202 #[test]
12203 fn test_iam_role_select() {
12204 let mut app = test_app();
12205 app.current_service = Service::IamRoles;
12206 app.service_selected = true;
12207 app.mode = Mode::Normal;
12208
12209 app.iam_state.roles.items = vec![
12210 IamRole {
12211 role_name: "role1".to_string(),
12212 path: "/".to_string(),
12213 trusted_entities: "AWS Service: ec2".to_string(),
12214 last_activity: "-".to_string(),
12215 arn: "arn:aws:iam::123456789012:role/role1".to_string(),
12216 creation_time: "2025-01-01".to_string(),
12217 description: "Test role 1".to_string(),
12218 max_session_duration: Some(3600),
12219 },
12220 IamRole {
12221 role_name: "role2".to_string(),
12222 path: "/".to_string(),
12223 trusted_entities: "AWS Service: lambda".to_string(),
12224 last_activity: "-".to_string(),
12225 arn: "arn:aws:iam::123456789012:role/role2".to_string(),
12226 creation_time: "2025-01-02".to_string(),
12227 description: "Test role 2".to_string(),
12228 max_session_duration: Some(7200),
12229 },
12230 ];
12231
12232 app.iam_state.roles.selected = 0;
12234 app.handle_action(Action::Select);
12235
12236 assert_eq!(
12237 app.iam_state.current_role,
12238 Some("role1".to_string()),
12239 "Should open role detail view"
12240 );
12241 assert_eq!(
12242 app.iam_state.role_tab,
12243 RoleTab::Permissions,
12244 "Should default to Permissions tab"
12245 );
12246 }
12247
12248 #[test]
12249 fn test_iam_role_back_navigation() {
12250 let mut app = test_app();
12251 app.current_service = Service::IamRoles;
12252 app.service_selected = true;
12253 app.iam_state.current_role = Some("test-role".to_string());
12254
12255 app.handle_action(Action::GoBack);
12256
12257 assert_eq!(
12258 app.iam_state.current_role, None,
12259 "Should return to roles list"
12260 );
12261 }
12262
12263 #[test]
12264 fn test_iam_role_tab_navigation() {
12265 let mut app = test_app();
12266 app.current_service = Service::IamRoles;
12267 app.service_selected = true;
12268 app.iam_state.current_role = Some("test-role".to_string());
12269 app.iam_state.role_tab = RoleTab::Permissions;
12270
12271 app.handle_action(Action::NextDetailTab);
12272
12273 assert_eq!(
12274 app.iam_state.role_tab,
12275 RoleTab::TrustRelationships,
12276 "Should move to next tab"
12277 );
12278 }
12279
12280 #[test]
12281 fn test_iam_role_tab_cycle_order() {
12282 let mut app = test_app();
12283 app.current_service = Service::IamRoles;
12284 app.service_selected = true;
12285 app.iam_state.current_role = Some("test-role".to_string());
12286 app.iam_state.role_tab = RoleTab::Permissions;
12287
12288 app.handle_action(Action::NextDetailTab);
12289 assert_eq!(app.iam_state.role_tab, RoleTab::TrustRelationships);
12290
12291 app.handle_action(Action::NextDetailTab);
12292 assert_eq!(app.iam_state.role_tab, RoleTab::Tags);
12293
12294 app.handle_action(Action::NextDetailTab);
12295 assert_eq!(app.iam_state.role_tab, RoleTab::LastAccessed);
12296
12297 app.handle_action(Action::NextDetailTab);
12298 assert_eq!(app.iam_state.role_tab, RoleTab::RevokeSessions);
12299
12300 app.handle_action(Action::NextDetailTab);
12301 assert_eq!(
12302 app.iam_state.role_tab,
12303 RoleTab::Permissions,
12304 "Should cycle back to first tab"
12305 );
12306 }
12307
12308 #[test]
12309 fn test_iam_role_pagination() {
12310 let mut app = test_app();
12311 app.current_service = Service::IamRoles;
12312 app.service_selected = true;
12313 app.iam_state.roles.page_size = PageSize::Ten;
12314
12315 app.iam_state.roles.items = (0..25)
12316 .map(|i| IamRole {
12317 role_name: format!("role{}", i),
12318 path: "/".to_string(),
12319 trusted_entities: "AWS Service: ec2".to_string(),
12320 last_activity: "-".to_string(),
12321 arn: format!("arn:aws:iam::123456789012:role/role{}", i),
12322 creation_time: "2025-01-01".to_string(),
12323 description: format!("Test role {}", i),
12324 max_session_duration: Some(3600),
12325 })
12326 .collect();
12327
12328 app.go_to_page(2);
12330
12331 assert_eq!(
12332 app.iam_state.roles.selected, 10,
12333 "Should select first item of page 2"
12334 );
12335 assert_eq!(
12336 app.iam_state.roles.scroll_offset, 10,
12337 "Should update scroll offset"
12338 );
12339 }
12340
12341 #[test]
12342 fn test_tags_table_populated_on_role_detail() {
12343 let mut app = test_app();
12344 app.current_service = Service::IamRoles;
12345 app.service_selected = true;
12346 app.mode = Mode::Normal;
12347 app.iam_state.roles.items = vec![IamRole {
12348 role_name: "TestRole".to_string(),
12349 path: "/".to_string(),
12350 trusted_entities: String::new(),
12351 last_activity: String::new(),
12352 arn: "arn:aws:iam::123456789012:role/TestRole".to_string(),
12353 creation_time: "2025-01-01".to_string(),
12354 description: String::new(),
12355 max_session_duration: Some(3600),
12356 }];
12357
12358 app.iam_state.tags.items = vec![
12360 IamRoleTag {
12361 key: "Environment".to_string(),
12362 value: "Production".to_string(),
12363 },
12364 IamRoleTag {
12365 key: "Team".to_string(),
12366 value: "Platform".to_string(),
12367 },
12368 ];
12369
12370 assert_eq!(app.iam_state.tags.items.len(), 2);
12371 assert_eq!(app.iam_state.tags.items[0].key, "Environment");
12372 assert_eq!(app.iam_state.tags.items[0].value, "Production");
12373 assert_eq!(app.iam_state.tags.selected, 0);
12374 }
12375
12376 #[test]
12377 fn test_tags_table_navigation() {
12378 let mut app = test_app();
12379 app.current_service = Service::IamRoles;
12380 app.service_selected = true;
12381 app.mode = Mode::Normal;
12382 app.iam_state.current_role = Some("TestRole".to_string());
12383 app.iam_state.role_tab = RoleTab::Tags;
12384 app.iam_state.tags.items = vec![
12385 IamRoleTag {
12386 key: "Tag1".to_string(),
12387 value: "Value1".to_string(),
12388 },
12389 IamRoleTag {
12390 key: "Tag2".to_string(),
12391 value: "Value2".to_string(),
12392 },
12393 ];
12394
12395 app.handle_action(Action::NextItem);
12396 assert_eq!(app.iam_state.tags.selected, 1);
12397
12398 app.handle_action(Action::PrevItem);
12399 assert_eq!(app.iam_state.tags.selected, 0);
12400 }
12401
12402 #[test]
12403 fn test_last_accessed_table_navigation() {
12404 let mut app = test_app();
12405 app.current_service = Service::IamRoles;
12406 app.service_selected = true;
12407 app.mode = Mode::Normal;
12408 app.iam_state.current_role = Some("TestRole".to_string());
12409 app.iam_state.role_tab = RoleTab::LastAccessed;
12410 app.iam_state.last_accessed_services.items = vec![
12411 LastAccessedService {
12412 service: "S3".to_string(),
12413 policies_granting: "Policy1".to_string(),
12414 last_accessed: "2025-01-01".to_string(),
12415 },
12416 LastAccessedService {
12417 service: "EC2".to_string(),
12418 policies_granting: "Policy2".to_string(),
12419 last_accessed: "2025-01-02".to_string(),
12420 },
12421 ];
12422
12423 app.handle_action(Action::NextItem);
12424 assert_eq!(app.iam_state.last_accessed_services.selected, 1);
12425
12426 app.handle_action(Action::PrevItem);
12427 assert_eq!(app.iam_state.last_accessed_services.selected, 0);
12428 }
12429
12430 #[test]
12431 fn test_cfn_input_focus_next() {
12432 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12433 let mut app = test_app();
12434 app.current_service = Service::CloudFormationStacks;
12435 app.mode = Mode::FilterInput;
12436 app.cfn_state.input_focus = InputFocus::Filter;
12437
12438 app.handle_action(Action::NextFilterFocus);
12439 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12440
12441 app.handle_action(Action::NextFilterFocus);
12442 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12443
12444 app.handle_action(Action::NextFilterFocus);
12445 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12446
12447 app.handle_action(Action::NextFilterFocus);
12448 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12449 }
12450
12451 #[test]
12452 fn test_cfn_input_focus_prev() {
12453 use crate::ui::cfn::{STATUS_FILTER, VIEW_NESTED};
12454 let mut app = test_app();
12455 app.current_service = Service::CloudFormationStacks;
12456 app.mode = Mode::FilterInput;
12457 app.cfn_state.input_focus = InputFocus::Filter;
12458
12459 app.handle_action(Action::PrevFilterFocus);
12460 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12461
12462 app.handle_action(Action::PrevFilterFocus);
12463 assert_eq!(app.cfn_state.input_focus, VIEW_NESTED);
12464
12465 app.handle_action(Action::PrevFilterFocus);
12466 assert_eq!(app.cfn_state.input_focus, STATUS_FILTER);
12467
12468 app.handle_action(Action::PrevFilterFocus);
12469 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12470 }
12471
12472 #[test]
12473 fn test_cw_logs_input_focus_prev() {
12474 let mut app = test_app();
12475 app.current_service = Service::CloudWatchLogGroups;
12476 app.mode = Mode::FilterInput;
12477 app.view_mode = ViewMode::Detail;
12478 app.log_groups_state.detail_tab = CwLogsDetailTab::LogStreams;
12479 app.log_groups_state.input_focus = InputFocus::Filter;
12480
12481 app.handle_action(Action::PrevFilterFocus);
12482 assert_eq!(app.log_groups_state.input_focus, InputFocus::Pagination);
12483
12484 app.handle_action(Action::PrevFilterFocus);
12485 assert_eq!(
12486 app.log_groups_state.input_focus,
12487 InputFocus::Checkbox("ShowExpired")
12488 );
12489
12490 app.handle_action(Action::PrevFilterFocus);
12491 assert_eq!(
12492 app.log_groups_state.input_focus,
12493 InputFocus::Checkbox("ExactMatch")
12494 );
12495
12496 app.handle_action(Action::PrevFilterFocus);
12497 assert_eq!(app.log_groups_state.input_focus, InputFocus::Filter);
12498 }
12499
12500 #[test]
12501 fn test_cw_events_input_focus_prev() {
12502 use crate::ui::cw::logs::EventFilterFocus;
12503 let mut app = test_app();
12504 app.mode = Mode::EventFilterInput;
12505 app.log_groups_state.event_input_focus = EventFilterFocus::Filter;
12506
12507 app.handle_action(Action::PrevFilterFocus);
12508 assert_eq!(
12509 app.log_groups_state.event_input_focus,
12510 EventFilterFocus::DateRange
12511 );
12512
12513 app.handle_action(Action::PrevFilterFocus);
12514 assert_eq!(
12515 app.log_groups_state.event_input_focus,
12516 EventFilterFocus::Filter
12517 );
12518 }
12519
12520 #[test]
12521 fn test_cfn_input_focus_cycle_complete() {
12522 let mut app = test_app();
12523 app.current_service = Service::CloudFormationStacks;
12524 app.mode = Mode::FilterInput;
12525 app.cfn_state.input_focus = InputFocus::Filter;
12526
12527 for _ in 0..4 {
12529 app.handle_action(Action::NextFilterFocus);
12530 }
12531 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12532
12533 for _ in 0..4 {
12535 app.handle_action(Action::PrevFilterFocus);
12536 }
12537 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12538 }
12539
12540 #[test]
12541 fn test_cfn_resources_pagination_left_right_arrows() {
12542 let mut app = test_app();
12546 app.current_service = Service::CloudFormationStacks;
12547 app.service_selected = true;
12548 app.mode = Mode::FilterInput;
12549 app.cfn_state.current_stack = Some("test-stack".to_string());
12550 app.cfn_state.detail_tab = CfnDetailTab::Resources;
12551 app.cfn_state.resources_input_focus = InputFocus::Pagination;
12552
12553 app.cfn_state.resources.items = (0..60)
12555 .map(|i| rusticity_core::cfn::StackResource {
12556 logical_id: format!("Resource{}", i),
12557 physical_id: format!("phys{}", i),
12558 resource_type: "AWS::S3::Bucket".to_string(),
12559 status: String::new(),
12560 module_info: String::new(),
12561 })
12562 .collect();
12563 app.cfn_state.resources.page_size = crate::common::PageSize::Ten;
12564 app.cfn_state.resources.selected = 0;
12565
12566 app.handle_action(Action::PageDown);
12568 assert_eq!(
12569 app.cfn_state.resources.selected, 10,
12570 "Right arrow with pagination focus must move to page 2 of Resources"
12571 );
12572
12573 app.handle_action(Action::PageUp);
12575 assert_eq!(
12576 app.cfn_state.resources.selected, 0,
12577 "Left arrow with pagination focus must move back to page 1 of Resources"
12578 );
12579 }
12580
12581 #[test]
12582 fn test_cfn_filter_status_arrow_keys() {
12583 use crate::ui::cfn::STATUS_FILTER;
12584 let mut app = test_app();
12585 app.current_service = Service::CloudFormationStacks;
12586 app.mode = Mode::FilterInput;
12587 app.cfn_state.input_focus = STATUS_FILTER;
12588 app.cfn_state.status_filter = CfnStatusFilter::All;
12589
12590 app.handle_action(Action::NextItem);
12591 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
12592
12593 app.handle_action(Action::PrevItem);
12594 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::All);
12595 }
12596
12597 #[test]
12598 fn test_cfn_filter_shift_tab_cycles_backward() {
12599 use crate::ui::cfn::STATUS_FILTER;
12600 let mut app = test_app();
12601 app.current_service = Service::CloudFormationStacks;
12602 app.mode = Mode::FilterInput;
12603 app.cfn_state.input_focus = STATUS_FILTER;
12604
12605 app.handle_action(Action::PrevFilterFocus);
12607 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12608
12609 app.handle_action(Action::PrevFilterFocus);
12611 assert_eq!(app.cfn_state.input_focus, InputFocus::Pagination);
12612 }
12613
12614 #[test]
12615 fn test_cfn_pagination_arrow_keys() {
12616 let mut app = test_app();
12617 app.current_service = Service::CloudFormationStacks;
12618 app.mode = Mode::FilterInput;
12619 app.cfn_state.input_focus = InputFocus::Pagination;
12620 app.cfn_state.table.scroll_offset = 0;
12621 app.cfn_state.table.page_size = PageSize::Ten;
12622
12623 app.cfn_state.table.items = (0..30)
12625 .map(|i| CfnStack {
12626 name: format!("stack-{}", i),
12627 stack_id: format!("id-{}", i),
12628 status: "CREATE_COMPLETE".to_string(),
12629 created_time: "2024-01-01".to_string(),
12630 updated_time: String::new(),
12631 deleted_time: String::new(),
12632 drift_status: String::new(),
12633 last_drift_check_time: String::new(),
12634 status_reason: String::new(),
12635 description: String::new(),
12636 detailed_status: String::new(),
12637 root_stack: String::new(),
12638 parent_stack: String::new(),
12639 termination_protection: false,
12640 iam_role: String::new(),
12641 tags: Vec::new(),
12642 stack_policy: String::new(),
12643 rollback_monitoring_time: String::new(),
12644 rollback_alarms: Vec::new(),
12645 notification_arns: Vec::new(),
12646 })
12647 .collect();
12648
12649 app.handle_action(Action::PageDown);
12651 assert_eq!(app.cfn_state.table.scroll_offset, 10);
12652 let page_size = app.cfn_state.table.page_size.value();
12654 let current_page = app.cfn_state.table.scroll_offset / page_size;
12655 assert_eq!(current_page, 1);
12656
12657 app.handle_action(Action::PageUp);
12659 assert_eq!(app.cfn_state.table.scroll_offset, 0);
12660 let current_page = app.cfn_state.table.scroll_offset / page_size;
12661 assert_eq!(current_page, 0);
12662 }
12663
12664 #[test]
12665 fn test_cfn_page_navigation_updates_selection() {
12666 let mut app = test_app();
12667 app.current_service = Service::CloudFormationStacks;
12668 app.mode = Mode::Normal;
12669
12670 app.cfn_state.table.items = (0..30)
12672 .map(|i| CfnStack {
12673 name: format!("stack-{}", i),
12674 stack_id: format!("id-{}", i),
12675 status: "CREATE_COMPLETE".to_string(),
12676 created_time: "2024-01-01".to_string(),
12677 updated_time: String::new(),
12678 deleted_time: String::new(),
12679 drift_status: String::new(),
12680 last_drift_check_time: String::new(),
12681 status_reason: String::new(),
12682 description: String::new(),
12683 detailed_status: String::new(),
12684 root_stack: String::new(),
12685 parent_stack: String::new(),
12686 termination_protection: false,
12687 iam_role: String::new(),
12688 tags: Vec::new(),
12689 stack_policy: String::new(),
12690 rollback_monitoring_time: String::new(),
12691 rollback_alarms: Vec::new(),
12692 notification_arns: Vec::new(),
12693 })
12694 .collect();
12695
12696 app.cfn_state.table.reset();
12697 app.cfn_state.table.scroll_offset = 0;
12698
12699 app.handle_action(Action::PageDown);
12701 assert_eq!(app.cfn_state.table.selected, 10);
12702
12703 app.handle_action(Action::PageDown);
12705 assert_eq!(app.cfn_state.table.selected, 20);
12706
12707 app.handle_action(Action::PageUp);
12709 assert_eq!(app.cfn_state.table.selected, 10);
12710 }
12711
12712 #[test]
12713 fn test_cfn_filter_input_only_when_focused() {
12714 use crate::ui::cfn::STATUS_FILTER;
12715 let mut app = test_app();
12716 app.current_service = Service::CloudFormationStacks;
12717 app.mode = Mode::FilterInput;
12718 app.cfn_state.input_focus = STATUS_FILTER;
12719 app.cfn_state.table.filter = String::new();
12720
12721 app.handle_action(Action::FilterInput('t'));
12723 app.handle_action(Action::FilterInput('e'));
12724 app.handle_action(Action::FilterInput('s'));
12725 app.handle_action(Action::FilterInput('t'));
12726 assert_eq!(app.cfn_state.table.filter, "");
12727
12728 app.cfn_state.input_focus = InputFocus::Filter;
12730 app.handle_action(Action::FilterInput('t'));
12731 app.handle_action(Action::FilterInput('e'));
12732 app.handle_action(Action::FilterInput('s'));
12733 app.handle_action(Action::FilterInput('t'));
12734 assert_eq!(app.cfn_state.table.filter, "test");
12735 }
12736
12737 #[test]
12738 fn test_cfn_input_focus_resets_on_start() {
12739 let mut app = test_app();
12740 app.current_service = Service::CloudFormationStacks;
12741 app.service_selected = true;
12742 app.mode = Mode::Normal;
12743 app.cfn_state.input_focus = InputFocus::Pagination;
12744
12745 app.handle_action(Action::StartFilter);
12747 assert_eq!(app.mode, Mode::FilterInput);
12748 assert_eq!(app.cfn_state.input_focus, InputFocus::Filter);
12749 }
12750
12751 #[test]
12752 fn test_iam_roles_input_focus_cycles_forward() {
12753 let mut app = test_app();
12754 app.current_service = Service::IamRoles;
12755 app.mode = Mode::FilterInput;
12756 app.iam_state.role_input_focus = InputFocus::Filter;
12757
12758 app.handle_action(Action::NextFilterFocus);
12759 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12760
12761 app.handle_action(Action::NextFilterFocus);
12762 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12763 }
12764
12765 #[test]
12766 fn test_iam_roles_input_focus_cycles_backward() {
12767 let mut app = test_app();
12768 app.current_service = Service::IamRoles;
12769 app.mode = Mode::FilterInput;
12770 app.iam_state.role_input_focus = InputFocus::Filter;
12771
12772 app.handle_action(Action::PrevFilterFocus);
12773 assert_eq!(app.iam_state.role_input_focus, InputFocus::Pagination);
12774
12775 app.handle_action(Action::PrevFilterFocus);
12776 assert_eq!(app.iam_state.role_input_focus, InputFocus::Filter);
12777 }
12778
12779 #[test]
12780 fn test_iam_roles_filter_input_only_when_focused() {
12781 let mut app = test_app();
12782 app.current_service = Service::IamRoles;
12783 app.mode = Mode::FilterInput;
12784 app.iam_state.role_input_focus = InputFocus::Pagination;
12785 app.iam_state.roles.filter = String::new();
12786
12787 app.handle_action(Action::FilterInput('t'));
12789 app.handle_action(Action::FilterInput('e'));
12790 app.handle_action(Action::FilterInput('s'));
12791 app.handle_action(Action::FilterInput('t'));
12792 assert_eq!(app.iam_state.roles.filter, "");
12793
12794 app.iam_state.role_input_focus = InputFocus::Filter;
12796 app.handle_action(Action::FilterInput('t'));
12797 app.handle_action(Action::FilterInput('e'));
12798 app.handle_action(Action::FilterInput('s'));
12799 app.handle_action(Action::FilterInput('t'));
12800 assert_eq!(app.iam_state.roles.filter, "test");
12801 }
12802
12803 #[test]
12804 fn test_iam_roles_page_down_updates_scroll_offset() {
12805 let mut app = test_app();
12806 app.current_service = Service::IamRoles;
12807 app.mode = Mode::Normal;
12808 app.iam_state.roles.items = (0..50)
12809 .map(|i| IamRole {
12810 role_name: format!("role-{}", i),
12811 path: "/".to_string(),
12812 trusted_entities: "AWS Service".to_string(),
12813 last_activity: "N/A".to_string(),
12814 arn: format!("arn:aws:iam::123456789012:role/role-{}", i),
12815 creation_time: "2024-01-01".to_string(),
12816 description: String::new(),
12817 max_session_duration: Some(3600),
12818 })
12819 .collect();
12820
12821 app.iam_state.roles.selected = 0;
12822 app.iam_state.roles.scroll_offset = 0;
12823
12824 app.handle_action(Action::PageDown);
12826 assert_eq!(app.iam_state.roles.selected, 10);
12827 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
12829
12830 app.handle_action(Action::PageDown);
12832 assert_eq!(app.iam_state.roles.selected, 20);
12833 assert!(app.iam_state.roles.scroll_offset <= app.iam_state.roles.selected);
12834 }
12835
12836 #[test]
12837 fn test_application_selection_and_deployments_tab() {
12838 use crate::lambda::Application as LambdaApplication;
12839 use LambdaApplicationDetailTab;
12840
12841 let mut app = test_app();
12842 app.current_service = Service::LambdaApplications;
12843 app.service_selected = true;
12844 app.mode = Mode::Normal;
12845
12846 app.lambda_application_state.table.items = vec![LambdaApplication {
12847 name: "test-app".to_string(),
12848 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12849 description: "Test application".to_string(),
12850 status: "CREATE_COMPLETE".to_string(),
12851 last_modified: "2024-01-01".to_string(),
12852 }];
12853
12854 app.handle_action(Action::Select);
12856 assert_eq!(
12857 app.lambda_application_state.current_application,
12858 Some("test-app".to_string())
12859 );
12860 assert_eq!(
12861 app.lambda_application_state.detail_tab,
12862 LambdaApplicationDetailTab::Overview
12863 );
12864
12865 app.handle_action(Action::NextDetailTab);
12867 assert_eq!(
12868 app.lambda_application_state.detail_tab,
12869 LambdaApplicationDetailTab::Deployments
12870 );
12871
12872 app.handle_action(Action::GoBack);
12874 assert_eq!(app.lambda_application_state.current_application, None);
12875 }
12876
12877 #[test]
12878 fn test_application_resources_filter_and_pagination() {
12879 use crate::lambda::Application as LambdaApplication;
12880 use LambdaApplicationDetailTab;
12881
12882 let mut app = test_app();
12883 app.current_service = Service::LambdaApplications;
12884 app.service_selected = true;
12885 app.mode = Mode::Normal;
12886
12887 app.lambda_application_state.table.items = vec![LambdaApplication {
12888 name: "test-app".to_string(),
12889 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12890 description: "Test application".to_string(),
12891 status: "CREATE_COMPLETE".to_string(),
12892 last_modified: "2024-01-01".to_string(),
12893 }];
12894
12895 app.handle_action(Action::Select);
12897 assert_eq!(
12898 app.lambda_application_state.detail_tab,
12899 LambdaApplicationDetailTab::Overview
12900 );
12901
12902 assert!(!app.lambda_application_state.resources.items.is_empty());
12904
12905 app.mode = Mode::FilterInput;
12907 assert_eq!(
12908 app.lambda_application_state.resource_input_focus,
12909 InputFocus::Filter
12910 );
12911
12912 app.handle_action(Action::NextFilterFocus);
12913 assert_eq!(
12914 app.lambda_application_state.resource_input_focus,
12915 InputFocus::Pagination
12916 );
12917
12918 app.handle_action(Action::PrevFilterFocus);
12919 assert_eq!(
12920 app.lambda_application_state.resource_input_focus,
12921 InputFocus::Filter
12922 );
12923 }
12924
12925 #[test]
12926 fn test_application_deployments_filter_and_pagination() {
12927 use crate::lambda::Application as LambdaApplication;
12928 use LambdaApplicationDetailTab;
12929
12930 let mut app = test_app();
12931 app.current_service = Service::LambdaApplications;
12932 app.service_selected = true;
12933 app.mode = Mode::Normal;
12934
12935 app.lambda_application_state.table.items = vec![LambdaApplication {
12936 name: "test-app".to_string(),
12937 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12938 description: "Test application".to_string(),
12939 status: "CREATE_COMPLETE".to_string(),
12940 last_modified: "2024-01-01".to_string(),
12941 }];
12942
12943 app.handle_action(Action::Select);
12945 app.handle_action(Action::NextDetailTab);
12946 assert_eq!(
12947 app.lambda_application_state.detail_tab,
12948 LambdaApplicationDetailTab::Deployments
12949 );
12950
12951 assert!(!app.lambda_application_state.deployments.items.is_empty());
12953
12954 app.mode = Mode::FilterInput;
12956 assert_eq!(
12957 app.lambda_application_state.deployment_input_focus,
12958 InputFocus::Filter
12959 );
12960
12961 app.handle_action(Action::NextFilterFocus);
12962 assert_eq!(
12963 app.lambda_application_state.deployment_input_focus,
12964 InputFocus::Pagination
12965 );
12966
12967 app.handle_action(Action::PrevFilterFocus);
12968 assert_eq!(
12969 app.lambda_application_state.deployment_input_focus,
12970 InputFocus::Filter
12971 );
12972 }
12973
12974 #[test]
12975 fn test_application_resource_expansion() {
12976 use crate::lambda::Application as LambdaApplication;
12977 use LambdaApplicationDetailTab;
12978
12979 let mut app = test_app();
12980 app.current_service = Service::LambdaApplications;
12981 app.service_selected = true;
12982 app.mode = Mode::Normal;
12983
12984 app.lambda_application_state.table.items = vec![LambdaApplication {
12985 name: "test-app".to_string(),
12986 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
12987 description: "Test application".to_string(),
12988 status: "CREATE_COMPLETE".to_string(),
12989 last_modified: "2024-01-01".to_string(),
12990 }];
12991
12992 app.handle_action(Action::Select);
12994 assert_eq!(
12995 app.lambda_application_state.detail_tab,
12996 LambdaApplicationDetailTab::Overview
12997 );
12998
12999 app.handle_action(Action::NextPane);
13001 assert_eq!(
13002 app.lambda_application_state.resources.expanded_item,
13003 Some(0)
13004 );
13005
13006 app.handle_action(Action::PrevPane);
13008 assert_eq!(app.lambda_application_state.resources.expanded_item, None);
13009 }
13010
13011 #[test]
13012 fn test_application_deployment_expansion() {
13013 use crate::lambda::Application as LambdaApplication;
13014 use LambdaApplicationDetailTab;
13015
13016 let mut app = test_app();
13017 app.current_service = Service::LambdaApplications;
13018 app.service_selected = true;
13019 app.mode = Mode::Normal;
13020
13021 app.lambda_application_state.table.items = vec![LambdaApplication {
13022 name: "test-app".to_string(),
13023 arn: "arn:aws:serverlessrepo:::applications/test-app".to_string(),
13024 description: "Test application".to_string(),
13025 status: "CREATE_COMPLETE".to_string(),
13026 last_modified: "2024-01-01".to_string(),
13027 }];
13028
13029 app.handle_action(Action::Select);
13031 app.handle_action(Action::NextDetailTab);
13032 assert_eq!(
13033 app.lambda_application_state.detail_tab,
13034 LambdaApplicationDetailTab::Deployments
13035 );
13036
13037 app.handle_action(Action::NextPane);
13039 assert_eq!(
13040 app.lambda_application_state.deployments.expanded_item,
13041 Some(0)
13042 );
13043
13044 app.handle_action(Action::PrevPane);
13046 assert_eq!(app.lambda_application_state.deployments.expanded_item, None);
13047 }
13048
13049 #[test]
13050 fn test_s3_nested_prefix_expansion() {
13051 use crate::s3::Bucket;
13052 use S3Object;
13053
13054 let mut app = test_app();
13055 app.current_service = Service::S3Buckets;
13056 app.service_selected = true;
13057 app.mode = Mode::Normal;
13058
13059 app.s3_state.buckets.items = vec![Bucket {
13061 name: "test-bucket".to_string(),
13062 region: "us-east-1".to_string(),
13063 creation_date: "2024-01-01".to_string(),
13064 }];
13065
13066 app.s3_state.bucket_preview.insert(
13068 "test-bucket".to_string(),
13069 vec![S3Object {
13070 key: "level1/".to_string(),
13071 size: 0,
13072 last_modified: "".to_string(),
13073 is_prefix: true,
13074 storage_class: "".to_string(),
13075 }],
13076 );
13077
13078 app.s3_state.prefix_preview.insert(
13080 "level1/".to_string(),
13081 vec![S3Object {
13082 key: "level1/level2/".to_string(),
13083 size: 0,
13084 last_modified: "".to_string(),
13085 is_prefix: true,
13086 storage_class: "".to_string(),
13087 }],
13088 );
13089
13090 app.s3_state.selected_row = 0;
13092 app.handle_action(Action::NextPane);
13093 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
13094
13095 app.s3_state.selected_row = 1;
13097 app.handle_action(Action::NextPane);
13098 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
13099
13100 app.s3_state.selected_row = 2;
13102 app.handle_action(Action::NextPane);
13103 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
13104
13105 assert!(app.s3_state.expanded_prefixes.contains("test-bucket"));
13107 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
13108 }
13109
13110 #[test]
13111 fn test_s3_nested_prefix_collapse() {
13112 use crate::s3::Bucket;
13113 use S3Object;
13114
13115 let mut app = test_app();
13116 app.current_service = Service::S3Buckets;
13117 app.service_selected = true;
13118 app.mode = Mode::Normal;
13119
13120 app.s3_state.buckets.items = vec![Bucket {
13121 name: "test-bucket".to_string(),
13122 region: "us-east-1".to_string(),
13123 creation_date: "2024-01-01".to_string(),
13124 }];
13125
13126 app.s3_state.bucket_preview.insert(
13127 "test-bucket".to_string(),
13128 vec![S3Object {
13129 key: "level1/".to_string(),
13130 size: 0,
13131 last_modified: "".to_string(),
13132 is_prefix: true,
13133 storage_class: "".to_string(),
13134 }],
13135 );
13136
13137 app.s3_state.prefix_preview.insert(
13138 "level1/".to_string(),
13139 vec![S3Object {
13140 key: "level1/level2/".to_string(),
13141 size: 0,
13142 last_modified: "".to_string(),
13143 is_prefix: true,
13144 storage_class: "".to_string(),
13145 }],
13146 );
13147
13148 app.s3_state
13150 .expanded_prefixes
13151 .insert("test-bucket".to_string());
13152 app.s3_state.expanded_prefixes.insert("level1/".to_string());
13153 app.s3_state
13154 .expanded_prefixes
13155 .insert("level1/level2/".to_string());
13156
13157 app.s3_state.selected_row = 2;
13159 app.handle_action(Action::PrevPane);
13160 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
13161 assert!(app.s3_state.expanded_prefixes.contains("level1/")); app.s3_state.selected_row = 1;
13165 app.handle_action(Action::PrevPane);
13166 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
13167 assert!(app.s3_state.expanded_prefixes.contains("test-bucket")); app.s3_state.selected_row = 0;
13171 app.handle_action(Action::PrevPane);
13172 assert!(!app.s3_state.expanded_prefixes.contains("test-bucket"));
13173 }
13174}
13175
13176#[cfg(test)]
13177mod sqs_tests {
13178 use super::*;
13179 use test_helpers::*;
13180
13181 #[test]
13182 fn test_sqs_filter_input() {
13183 let mut app = test_app();
13184 app.current_service = Service::SqsQueues;
13185 app.service_selected = true;
13186 app.mode = Mode::FilterInput;
13187
13188 app.handle_action(Action::FilterInput('t'));
13189 app.handle_action(Action::FilterInput('e'));
13190 app.handle_action(Action::FilterInput('s'));
13191 app.handle_action(Action::FilterInput('t'));
13192 assert_eq!(app.sqs_state.queues.filter, "test");
13193
13194 app.handle_action(Action::FilterBackspace);
13195 assert_eq!(app.sqs_state.queues.filter, "tes");
13196 }
13197
13198 #[test]
13199 fn test_sqs_start_filter() {
13200 let mut app = test_app();
13201 app.current_service = Service::SqsQueues;
13202 app.service_selected = true;
13203 app.mode = Mode::Normal;
13204
13205 app.handle_action(Action::StartFilter);
13206 assert_eq!(app.mode, Mode::FilterInput);
13207 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
13208 }
13209
13210 #[test]
13211 fn test_sqs_filter_focus_cycling() {
13212 let mut app = test_app();
13213 app.current_service = Service::SqsQueues;
13214 app.service_selected = true;
13215 app.mode = Mode::FilterInput;
13216 app.sqs_state.input_focus = InputFocus::Filter;
13217
13218 app.handle_action(Action::NextFilterFocus);
13219 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
13220
13221 app.handle_action(Action::NextFilterFocus);
13222 assert_eq!(app.sqs_state.input_focus, InputFocus::Filter);
13223
13224 app.handle_action(Action::PrevFilterFocus);
13225 assert_eq!(app.sqs_state.input_focus, InputFocus::Pagination);
13226 }
13227
13228 #[test]
13229 fn test_sqs_navigation() {
13230 let mut app = test_app();
13231 app.current_service = Service::SqsQueues;
13232 app.service_selected = true;
13233 app.mode = Mode::Normal;
13234 app.sqs_state.queues.items = (0..10)
13235 .map(|i| SqsQueue {
13236 name: format!("queue{}", i),
13237 url: String::new(),
13238 queue_type: "Standard".to_string(),
13239 created_timestamp: String::new(),
13240 messages_available: "0".to_string(),
13241 messages_in_flight: "0".to_string(),
13242 encryption: "Disabled".to_string(),
13243 content_based_deduplication: "Disabled".to_string(),
13244 last_modified_timestamp: String::new(),
13245 visibility_timeout: String::new(),
13246 message_retention_period: String::new(),
13247 maximum_message_size: String::new(),
13248 delivery_delay: String::new(),
13249 receive_message_wait_time: String::new(),
13250 high_throughput_fifo: "N/A".to_string(),
13251 deduplication_scope: "N/A".to_string(),
13252 fifo_throughput_limit: "N/A".to_string(),
13253 dead_letter_queue: "-".to_string(),
13254 messages_delayed: "0".to_string(),
13255 redrive_allow_policy: "-".to_string(),
13256 redrive_policy: "".to_string(),
13257 redrive_task_id: "-".to_string(),
13258 redrive_task_start_time: "-".to_string(),
13259 redrive_task_status: "-".to_string(),
13260 redrive_task_percent: "-".to_string(),
13261 redrive_task_destination: "-".to_string(),
13262 })
13263 .collect();
13264
13265 app.handle_action(Action::NextItem);
13266 assert_eq!(app.sqs_state.queues.selected, 1);
13267
13268 app.handle_action(Action::PrevItem);
13269 assert_eq!(app.sqs_state.queues.selected, 0);
13270 }
13271
13272 #[test]
13273 fn test_sqs_page_navigation() {
13274 let mut app = test_app();
13275 app.current_service = Service::SqsQueues;
13276 app.service_selected = true;
13277 app.mode = Mode::Normal;
13278 app.sqs_state.queues.items = (0..100)
13279 .map(|i| SqsQueue {
13280 name: format!("queue{}", i),
13281 url: String::new(),
13282 queue_type: "Standard".to_string(),
13283 created_timestamp: String::new(),
13284 messages_available: "0".to_string(),
13285 messages_in_flight: "0".to_string(),
13286 encryption: "Disabled".to_string(),
13287 content_based_deduplication: "Disabled".to_string(),
13288 last_modified_timestamp: String::new(),
13289 visibility_timeout: String::new(),
13290 message_retention_period: String::new(),
13291 maximum_message_size: String::new(),
13292 delivery_delay: String::new(),
13293 receive_message_wait_time: String::new(),
13294 high_throughput_fifo: "N/A".to_string(),
13295 deduplication_scope: "N/A".to_string(),
13296 fifo_throughput_limit: "N/A".to_string(),
13297 dead_letter_queue: "-".to_string(),
13298 messages_delayed: "0".to_string(),
13299 redrive_allow_policy: "-".to_string(),
13300 redrive_policy: "".to_string(),
13301 redrive_task_id: "-".to_string(),
13302 redrive_task_start_time: "-".to_string(),
13303 redrive_task_status: "-".to_string(),
13304 redrive_task_percent: "-".to_string(),
13305 redrive_task_destination: "-".to_string(),
13306 })
13307 .collect();
13308
13309 app.handle_action(Action::PageDown);
13310 assert_eq!(app.sqs_state.queues.selected, 10);
13311
13312 app.handle_action(Action::PageUp);
13313 assert_eq!(app.sqs_state.queues.selected, 0);
13314 }
13315
13316 #[test]
13317 fn test_sqs_queue_expansion() {
13318 let mut app = test_app();
13319 app.current_service = Service::SqsQueues;
13320 app.service_selected = true;
13321 app.sqs_state.queues.items = vec![SqsQueue {
13322 name: "my-queue".to_string(),
13323 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
13324 queue_type: "Standard".to_string(),
13325 created_timestamp: "2023-01-01".to_string(),
13326 messages_available: "5".to_string(),
13327 messages_in_flight: "2".to_string(),
13328 encryption: "Enabled".to_string(),
13329 content_based_deduplication: "Disabled".to_string(),
13330 last_modified_timestamp: "2023-01-02".to_string(),
13331 visibility_timeout: "30".to_string(),
13332 message_retention_period: "345600".to_string(),
13333 maximum_message_size: "262144".to_string(),
13334 delivery_delay: "0".to_string(),
13335 receive_message_wait_time: "0".to_string(),
13336 high_throughput_fifo: "N/A".to_string(),
13337 deduplication_scope: "N/A".to_string(),
13338 fifo_throughput_limit: "N/A".to_string(),
13339 dead_letter_queue: "-".to_string(),
13340 messages_delayed: "0".to_string(),
13341 redrive_allow_policy: "-".to_string(),
13342 redrive_policy: "".to_string(),
13343 redrive_task_id: "-".to_string(),
13344 redrive_task_start_time: "-".to_string(),
13345 redrive_task_status: "-".to_string(),
13346 redrive_task_percent: "-".to_string(),
13347 redrive_task_destination: "-".to_string(),
13348 }];
13349 app.sqs_state.queues.selected = 0;
13350
13351 assert_eq!(app.sqs_state.queues.expanded_item, None);
13352
13353 app.handle_action(Action::NextPane);
13355 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
13356
13357 app.handle_action(Action::NextPane);
13359 assert_eq!(app.sqs_state.queues.expanded_item, Some(0));
13360
13361 app.handle_action(Action::PrevPane);
13363 assert_eq!(app.sqs_state.queues.expanded_item, None);
13364
13365 app.handle_action(Action::PrevPane);
13367 assert_eq!(app.sqs_state.queues.expanded_item, None);
13368 }
13369
13370 #[test]
13371 fn test_sqs_column_toggle() {
13372 use crate::sqs::queue::Column as SqsColumn;
13373 let mut app = test_app();
13374 app.current_service = Service::SqsQueues;
13375 app.service_selected = true;
13376 app.mode = Mode::ColumnSelector;
13377
13378 app.sqs_visible_column_ids = SqsColumn::ids();
13380 let initial_count = app.sqs_visible_column_ids.len();
13381
13382 app.column_selector_index = 1;
13384 app.handle_action(Action::ToggleColumn);
13385
13386 assert_eq!(app.sqs_visible_column_ids.len(), initial_count - 1);
13388 assert!(!app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
13389
13390 app.handle_action(Action::ToggleColumn);
13392 assert_eq!(app.sqs_visible_column_ids.len(), initial_count);
13393 assert!(app.sqs_visible_column_ids.contains(&SqsColumn::Name.id()));
13394 }
13395
13396 #[test]
13397 fn test_sqs_column_selector_navigation() {
13398 let mut app = test_app();
13399 app.current_service = Service::SqsQueues;
13400 app.service_selected = true;
13401 app.mode = Mode::ColumnSelector;
13402 app.column_selector_index = 0;
13403
13404 let max_index = app.sqs_column_ids.len() - 1;
13406
13407 for _ in 0..max_index {
13409 app.handle_action(Action::NextItem);
13410 }
13411 assert_eq!(app.column_selector_index, max_index);
13412
13413 for _ in 0..max_index {
13415 app.handle_action(Action::PrevItem);
13416 }
13417 assert_eq!(app.column_selector_index, 0);
13418 }
13419
13420 #[test]
13421 fn test_sqs_queue_selection() {
13422 let mut app = test_app();
13423 app.current_service = Service::SqsQueues;
13424 app.service_selected = true;
13425 app.mode = Mode::Normal;
13426 app.sqs_state.queues.items = vec![SqsQueue {
13427 name: "my-queue".to_string(),
13428 url: "https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string(),
13429 queue_type: "Standard".to_string(),
13430 created_timestamp: "2023-01-01".to_string(),
13431 messages_available: "5".to_string(),
13432 messages_in_flight: "2".to_string(),
13433 encryption: "Enabled".to_string(),
13434 content_based_deduplication: "Disabled".to_string(),
13435 last_modified_timestamp: "2023-01-02".to_string(),
13436 visibility_timeout: "30".to_string(),
13437 message_retention_period: "345600".to_string(),
13438 maximum_message_size: "262144".to_string(),
13439 delivery_delay: "0".to_string(),
13440 receive_message_wait_time: "0".to_string(),
13441 high_throughput_fifo: "N/A".to_string(),
13442 deduplication_scope: "N/A".to_string(),
13443 fifo_throughput_limit: "N/A".to_string(),
13444 dead_letter_queue: "-".to_string(),
13445 messages_delayed: "0".to_string(),
13446 redrive_allow_policy: "-".to_string(),
13447 redrive_policy: "".to_string(),
13448 redrive_task_id: "-".to_string(),
13449 redrive_task_start_time: "-".to_string(),
13450 redrive_task_status: "-".to_string(),
13451 redrive_task_percent: "-".to_string(),
13452 redrive_task_destination: "-".to_string(),
13453 }];
13454 app.sqs_state.queues.selected = 0;
13455
13456 assert_eq!(app.sqs_state.current_queue, None);
13457
13458 app.handle_action(Action::Select);
13460 assert_eq!(
13461 app.sqs_state.current_queue,
13462 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string())
13463 );
13464
13465 app.handle_action(Action::GoBack);
13467 assert_eq!(app.sqs_state.current_queue, None);
13468 }
13469
13470 #[test]
13471 fn test_sqs_lambda_triggers_expand_collapse() {
13472 let mut app = test_app();
13473 app.current_service = Service::SqsQueues;
13474 app.service_selected = true;
13475 app.sqs_state.current_queue =
13476 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13477 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13478 app.sqs_state.triggers.items = vec![LambdaTrigger {
13479 uuid: "test-uuid".to_string(),
13480 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
13481 status: "Enabled".to_string(),
13482 last_modified: "2024-01-01T00:00:00Z".to_string(),
13483 }];
13484 app.sqs_state.triggers.selected = 0;
13485
13486 assert_eq!(app.sqs_state.triggers.expanded_item, None);
13487
13488 app.handle_action(Action::NextPane);
13490 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13491
13492 app.handle_action(Action::PrevPane);
13494 assert_eq!(app.sqs_state.triggers.expanded_item, None);
13495 }
13496
13497 #[test]
13498 fn test_sqs_lambda_triggers_expand_toggle() {
13499 let mut app = test_app();
13500 app.current_service = Service::SqsQueues;
13501 app.service_selected = true;
13502 app.sqs_state.current_queue =
13503 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13504 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13505 app.sqs_state.triggers.items = vec![LambdaTrigger {
13506 uuid: "test-uuid".to_string(),
13507 arn: "arn:aws:lambda:us-east-1:123456789012:function:test".to_string(),
13508 status: "Enabled".to_string(),
13509 last_modified: "2024-01-01T00:00:00Z".to_string(),
13510 }];
13511 app.sqs_state.triggers.selected = 0;
13512
13513 app.handle_action(Action::NextPane);
13515 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13516
13517 app.handle_action(Action::NextPane);
13519 assert_eq!(app.sqs_state.triggers.expanded_item, None);
13520
13521 app.handle_action(Action::NextPane);
13523 assert_eq!(app.sqs_state.triggers.expanded_item, Some(0));
13524 }
13525
13526 #[test]
13527 fn test_sqs_lambda_triggers_sorted_by_last_modified_asc() {
13528 use crate::ui::sqs::filtered_lambda_triggers;
13529
13530 let mut app = test_app();
13531 app.current_service = Service::SqsQueues;
13532 app.service_selected = true;
13533 app.sqs_state.current_queue =
13534 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13535 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13536 app.sqs_state.triggers.items = vec![
13537 LambdaTrigger {
13538 uuid: "uuid-3".to_string(),
13539 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-3".to_string(),
13540 status: "Enabled".to_string(),
13541 last_modified: "2024-03-01T00:00:00Z".to_string(),
13542 },
13543 LambdaTrigger {
13544 uuid: "uuid-1".to_string(),
13545 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-1".to_string(),
13546 status: "Enabled".to_string(),
13547 last_modified: "2024-01-01T00:00:00Z".to_string(),
13548 },
13549 LambdaTrigger {
13550 uuid: "uuid-2".to_string(),
13551 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-2".to_string(),
13552 status: "Enabled".to_string(),
13553 last_modified: "2024-02-01T00:00:00Z".to_string(),
13554 },
13555 ];
13556
13557 let sorted = filtered_lambda_triggers(&app);
13558
13559 assert_eq!(sorted.len(), 3);
13561 assert_eq!(sorted[0].uuid, "uuid-1");
13562 assert_eq!(sorted[0].last_modified, "2024-01-01T00:00:00Z");
13563 assert_eq!(sorted[1].uuid, "uuid-2");
13564 assert_eq!(sorted[1].last_modified, "2024-02-01T00:00:00Z");
13565 assert_eq!(sorted[2].uuid, "uuid-3");
13566 assert_eq!(sorted[2].last_modified, "2024-03-01T00:00:00Z");
13567 }
13568
13569 #[test]
13570 fn test_sqs_lambda_triggers_filter_input() {
13571 let mut app = test_app();
13572 app.current_service = Service::SqsQueues;
13573 app.service_selected = true;
13574 app.mode = Mode::FilterInput;
13575 app.sqs_state.current_queue =
13576 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13577 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13578 app.sqs_state.input_focus = InputFocus::Filter;
13579
13580 assert_eq!(app.sqs_state.triggers.filter, "");
13581
13582 app.handle_action(Action::FilterInput('t'));
13584 assert_eq!(app.sqs_state.triggers.filter, "t");
13585
13586 app.handle_action(Action::FilterInput('e'));
13587 assert_eq!(app.sqs_state.triggers.filter, "te");
13588
13589 app.handle_action(Action::FilterInput('s'));
13590 assert_eq!(app.sqs_state.triggers.filter, "tes");
13591
13592 app.handle_action(Action::FilterInput('t'));
13593 assert_eq!(app.sqs_state.triggers.filter, "test");
13594
13595 app.handle_action(Action::FilterBackspace);
13597 assert_eq!(app.sqs_state.triggers.filter, "tes");
13598 }
13599
13600 #[test]
13601 fn test_sqs_lambda_triggers_filter_applied() {
13602 use crate::ui::sqs::filtered_lambda_triggers;
13603
13604 let mut app = test_app();
13605 app.current_service = Service::SqsQueues;
13606 app.service_selected = true;
13607 app.sqs_state.current_queue =
13608 Some("https://sqs.us-east-1.amazonaws.com/123456789012/my-queue".to_string());
13609 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13610 app.sqs_state.triggers.items = vec![
13611 LambdaTrigger {
13612 uuid: "uuid-1".to_string(),
13613 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-alpha".to_string(),
13614 status: "Enabled".to_string(),
13615 last_modified: "2024-01-01T00:00:00Z".to_string(),
13616 },
13617 LambdaTrigger {
13618 uuid: "uuid-2".to_string(),
13619 arn: "arn:aws:lambda:us-east-1:123456789012:function:test-beta".to_string(),
13620 status: "Enabled".to_string(),
13621 last_modified: "2024-02-01T00:00:00Z".to_string(),
13622 },
13623 LambdaTrigger {
13624 uuid: "uuid-3".to_string(),
13625 arn: "arn:aws:lambda:us-east-1:123456789012:function:prod-gamma".to_string(),
13626 status: "Enabled".to_string(),
13627 last_modified: "2024-03-01T00:00:00Z".to_string(),
13628 },
13629 ];
13630
13631 let filtered = filtered_lambda_triggers(&app);
13633 assert_eq!(filtered.len(), 3);
13634
13635 app.sqs_state.triggers.filter = "alpha".to_string();
13637 let filtered = filtered_lambda_triggers(&app);
13638 assert_eq!(filtered.len(), 1);
13639 assert_eq!(
13640 filtered[0].arn,
13641 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
13642 );
13643
13644 app.sqs_state.triggers.filter = "test".to_string();
13646 let filtered = filtered_lambda_triggers(&app);
13647 assert_eq!(filtered.len(), 2);
13648 assert_eq!(
13649 filtered[0].arn,
13650 "arn:aws:lambda:us-east-1:123456789012:function:test-alpha"
13651 );
13652 assert_eq!(
13653 filtered[1].arn,
13654 "arn:aws:lambda:us-east-1:123456789012:function:test-beta"
13655 );
13656
13657 app.sqs_state.triggers.filter = "uuid-3".to_string();
13659 let filtered = filtered_lambda_triggers(&app);
13660 assert_eq!(filtered.len(), 1);
13661 assert_eq!(filtered[0].uuid, "uuid-3");
13662 }
13663
13664 #[test]
13665 fn test_sqs_triggers_navigation() {
13666 let mut app = test_app();
13667 app.service_selected = true;
13668 app.mode = Mode::Normal;
13669 app.current_service = Service::SqsQueues;
13670 app.sqs_state.current_queue = Some("test-queue".to_string());
13671 app.sqs_state.detail_tab = SqsQueueDetailTab::LambdaTriggers;
13672 app.sqs_state.triggers.items = vec![
13673 LambdaTrigger {
13674 uuid: "1".to_string(),
13675 arn: "arn1".to_string(),
13676 status: "Enabled".to_string(),
13677 last_modified: "2024-01-01".to_string(),
13678 },
13679 LambdaTrigger {
13680 uuid: "2".to_string(),
13681 arn: "arn2".to_string(),
13682 status: "Enabled".to_string(),
13683 last_modified: "2024-01-02".to_string(),
13684 },
13685 ];
13686
13687 assert_eq!(app.sqs_state.triggers.selected, 0);
13688 app.next_item();
13689 assert_eq!(app.sqs_state.triggers.selected, 1);
13690 app.prev_item();
13691 assert_eq!(app.sqs_state.triggers.selected, 0);
13692 }
13693
13694 #[test]
13695 fn test_sqs_pipes_navigation() {
13696 let mut app = test_app();
13697 app.service_selected = true;
13698 app.mode = Mode::Normal;
13699 app.current_service = Service::SqsQueues;
13700 app.sqs_state.current_queue = Some("test-queue".to_string());
13701 app.sqs_state.detail_tab = SqsQueueDetailTab::EventBridgePipes;
13702 app.sqs_state.pipes.items = vec![
13703 EventBridgePipe {
13704 name: "pipe1".to_string(),
13705 status: "RUNNING".to_string(),
13706 target: "target1".to_string(),
13707 last_modified: "2024-01-01".to_string(),
13708 },
13709 EventBridgePipe {
13710 name: "pipe2".to_string(),
13711 status: "RUNNING".to_string(),
13712 target: "target2".to_string(),
13713 last_modified: "2024-01-02".to_string(),
13714 },
13715 ];
13716
13717 assert_eq!(app.sqs_state.pipes.selected, 0);
13718 app.next_item();
13719 assert_eq!(app.sqs_state.pipes.selected, 1);
13720 app.prev_item();
13721 assert_eq!(app.sqs_state.pipes.selected, 0);
13722 }
13723
13724 #[test]
13725 fn test_sqs_tags_navigation() {
13726 let mut app = test_app();
13727 app.service_selected = true;
13728 app.mode = Mode::Normal;
13729 app.current_service = Service::SqsQueues;
13730 app.sqs_state.current_queue = Some("test-queue".to_string());
13731 app.sqs_state.detail_tab = SqsQueueDetailTab::Tagging;
13732 app.sqs_state.tags.items = vec![
13733 SqsQueueTag {
13734 key: "Env".to_string(),
13735 value: "prod".to_string(),
13736 },
13737 SqsQueueTag {
13738 key: "Team".to_string(),
13739 value: "backend".to_string(),
13740 },
13741 ];
13742
13743 assert_eq!(app.sqs_state.tags.selected, 0);
13744 app.next_item();
13745 assert_eq!(app.sqs_state.tags.selected, 1);
13746 app.prev_item();
13747 assert_eq!(app.sqs_state.tags.selected, 0);
13748 }
13749
13750 #[test]
13751 fn test_sqs_queues_navigation() {
13752 let mut app = test_app();
13753 app.service_selected = true;
13754 app.mode = Mode::Normal;
13755 app.current_service = Service::SqsQueues;
13756 app.sqs_state.queues.items = vec![
13757 SqsQueue {
13758 name: "queue1".to_string(),
13759 url: "url1".to_string(),
13760 queue_type: "Standard".to_string(),
13761 created_timestamp: "".to_string(),
13762 messages_available: "0".to_string(),
13763 messages_in_flight: "0".to_string(),
13764 encryption: "Disabled".to_string(),
13765 content_based_deduplication: "Disabled".to_string(),
13766 last_modified_timestamp: "".to_string(),
13767 visibility_timeout: "".to_string(),
13768 message_retention_period: "".to_string(),
13769 maximum_message_size: "".to_string(),
13770 delivery_delay: "".to_string(),
13771 receive_message_wait_time: "".to_string(),
13772 high_throughput_fifo: "-".to_string(),
13773 deduplication_scope: "-".to_string(),
13774 fifo_throughput_limit: "-".to_string(),
13775 dead_letter_queue: "-".to_string(),
13776 messages_delayed: "0".to_string(),
13777 redrive_allow_policy: "-".to_string(),
13778 redrive_policy: "".to_string(),
13779 redrive_task_id: "-".to_string(),
13780 redrive_task_start_time: "-".to_string(),
13781 redrive_task_status: "-".to_string(),
13782 redrive_task_percent: "-".to_string(),
13783 redrive_task_destination: "-".to_string(),
13784 },
13785 SqsQueue {
13786 name: "queue2".to_string(),
13787 url: "url2".to_string(),
13788 queue_type: "Standard".to_string(),
13789 created_timestamp: "".to_string(),
13790 messages_available: "0".to_string(),
13791 messages_in_flight: "0".to_string(),
13792 encryption: "Disabled".to_string(),
13793 content_based_deduplication: "Disabled".to_string(),
13794 last_modified_timestamp: "".to_string(),
13795 visibility_timeout: "".to_string(),
13796 message_retention_period: "".to_string(),
13797 maximum_message_size: "".to_string(),
13798 delivery_delay: "".to_string(),
13799 receive_message_wait_time: "".to_string(),
13800 high_throughput_fifo: "-".to_string(),
13801 deduplication_scope: "-".to_string(),
13802 fifo_throughput_limit: "-".to_string(),
13803 dead_letter_queue: "-".to_string(),
13804 messages_delayed: "0".to_string(),
13805 redrive_allow_policy: "-".to_string(),
13806 redrive_policy: "".to_string(),
13807 redrive_task_id: "-".to_string(),
13808 redrive_task_start_time: "-".to_string(),
13809 redrive_task_status: "-".to_string(),
13810 redrive_task_percent: "-".to_string(),
13811 redrive_task_destination: "-".to_string(),
13812 },
13813 ];
13814
13815 assert_eq!(app.sqs_state.queues.selected, 0);
13816 app.next_item();
13817 assert_eq!(app.sqs_state.queues.selected, 1);
13818 app.prev_item();
13819 assert_eq!(app.sqs_state.queues.selected, 0);
13820 }
13821
13822 #[test]
13823 fn test_sqs_subscriptions_navigation() {
13824 let mut app = test_app();
13825 app.service_selected = true;
13826 app.mode = Mode::Normal;
13827 app.current_service = Service::SqsQueues;
13828 app.sqs_state.current_queue = Some("test-queue".to_string());
13829 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
13830 app.sqs_state.subscriptions.items = vec![
13831 SnsSubscription {
13832 subscription_arn: "arn:aws:sns:us-east-1:123:sub1".to_string(),
13833 topic_arn: "arn:aws:sns:us-east-1:123:topic1".to_string(),
13834 },
13835 SnsSubscription {
13836 subscription_arn: "arn:aws:sns:us-east-1:123:sub2".to_string(),
13837 topic_arn: "arn:aws:sns:us-east-1:123:topic2".to_string(),
13838 },
13839 ];
13840
13841 assert_eq!(app.sqs_state.subscriptions.selected, 0);
13842 app.next_item();
13843 assert_eq!(app.sqs_state.subscriptions.selected, 1);
13844 app.prev_item();
13845 assert_eq!(app.sqs_state.subscriptions.selected, 0);
13846 }
13847
13848 #[test]
13849 fn test_sqs_subscription_region_dropdown_navigation() {
13850 let mut app = test_app();
13851 app.service_selected = true;
13852 app.mode = Mode::FilterInput;
13853 app.current_service = Service::SqsQueues;
13854 app.sqs_state.current_queue = Some("test-queue".to_string());
13855 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
13856 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
13857
13858 assert_eq!(app.sqs_state.subscription_region_selected, 0);
13859 app.next_item();
13860 assert_eq!(app.sqs_state.subscription_region_selected, 1);
13861 app.next_item();
13862 assert_eq!(app.sqs_state.subscription_region_selected, 2);
13863 app.prev_item();
13864 assert_eq!(app.sqs_state.subscription_region_selected, 1);
13865 app.prev_item();
13866 assert_eq!(app.sqs_state.subscription_region_selected, 0);
13867 }
13868
13869 #[test]
13870 fn test_sqs_subscription_region_selection() {
13871 let mut app = test_app();
13872 app.service_selected = true;
13873 app.mode = Mode::FilterInput;
13874 app.current_service = Service::SqsQueues;
13875 app.sqs_state.current_queue = Some("test-queue".to_string());
13876 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
13877 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
13878 app.sqs_state.subscription_region_selected = 2; assert_eq!(app.sqs_state.subscription_region_filter, "");
13881 app.handle_action(Action::ApplyFilter);
13882 assert_eq!(app.sqs_state.subscription_region_filter, "us-west-1");
13883 assert_eq!(app.mode, Mode::Normal);
13884 }
13885
13886 #[test]
13887 fn test_sqs_subscription_region_change_resets_selection() {
13888 let mut app = test_app();
13889 app.service_selected = true;
13890 app.mode = Mode::FilterInput;
13891 app.current_service = Service::SqsQueues;
13892 app.sqs_state.current_queue = Some("test-queue".to_string());
13893 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
13894 app.sqs_state.input_focus = InputFocus::Dropdown("SubscriptionRegion");
13895 app.sqs_state.subscription_region_selected = 0;
13896 app.sqs_state.subscriptions.selected = 5;
13897
13898 app.handle_action(Action::NextItem);
13899
13900 assert_eq!(app.sqs_state.subscription_region_selected, 1);
13901 assert_eq!(app.sqs_state.subscriptions.selected, 0);
13902 }
13903
13904 #[test]
13905 fn test_s3_object_filter_resets_selection() {
13906 let mut app = test_app();
13907 app.service_selected = true;
13908 app.current_service = Service::S3Buckets;
13909 app.s3_state.current_bucket = Some("test-bucket".to_string());
13910 app.s3_state.selected_row = 5;
13911 app.mode = Mode::FilterInput;
13912
13913 app.handle_action(Action::CloseMenu);
13914
13915 assert_eq!(app.s3_state.selected_row, 0);
13916 assert_eq!(app.mode, Mode::Normal);
13917 }
13918
13919 #[test]
13920 fn test_s3_bucket_filter_resets_selection() {
13921 let mut app = test_app();
13922 app.service_selected = true;
13923 app.current_service = Service::S3Buckets;
13924 app.s3_state.selected_row = 10;
13925 app.mode = Mode::FilterInput;
13926
13927 app.handle_action(Action::CloseMenu);
13928
13929 assert_eq!(app.s3_state.selected_row, 0);
13930 assert_eq!(app.mode, Mode::Normal);
13931 }
13932
13933 #[test]
13934 fn test_s3_selection_stays_in_bounds() {
13935 let mut app = test_app();
13936 app.service_selected = true;
13937 app.current_service = Service::S3Buckets;
13938 app.s3_state.selected_row = 0;
13939 app.s3_state.selected_object = 0;
13940
13941 app.prev_item();
13943
13944 assert_eq!(app.s3_state.selected_row, 0);
13946 assert_eq!(app.s3_state.selected_object, 0);
13947 }
13948
13949 #[test]
13950 fn test_cfn_filter_resets_selection() {
13951 let mut app = test_app();
13952 app.service_selected = true;
13953 app.current_service = Service::CloudFormationStacks;
13954 app.cfn_state.table.selected = 10;
13955 app.mode = Mode::FilterInput;
13956
13957 app.handle_action(Action::CloseMenu);
13958
13959 assert_eq!(app.cfn_state.table.selected, 0);
13960 assert_eq!(app.mode, Mode::Normal);
13961 }
13962
13963 #[test]
13964 fn test_lambda_filter_resets_selection() {
13965 let mut app = test_app();
13966 app.service_selected = true;
13967 app.current_service = Service::LambdaFunctions;
13968 app.lambda_state.table.selected = 8;
13969 app.mode = Mode::FilterInput;
13970
13971 app.handle_action(Action::CloseMenu);
13972
13973 assert_eq!(app.lambda_state.table.selected, 0);
13974 assert_eq!(app.mode, Mode::Normal);
13975 }
13976
13977 #[test]
13978 fn test_sqs_filter_resets_selection() {
13979 let mut app = test_app();
13980 app.service_selected = true;
13981 app.current_service = Service::SqsQueues;
13982 app.sqs_state.queues.selected = 7;
13983 app.mode = Mode::FilterInput;
13984
13985 app.handle_action(Action::CloseMenu);
13986
13987 assert_eq!(app.sqs_state.queues.selected, 0);
13988 assert_eq!(app.mode, Mode::Normal);
13989 }
13990
13991 #[test]
13992 fn test_sqs_queues_list_shows_preferences() {
13993 let mut app = test_app();
13994 app.service_selected = true;
13995 app.current_service = Service::SqsQueues;
13996 app.mode = Mode::Normal;
13997
13998 app.handle_action(Action::OpenColumnSelector);
13999
14000 assert_eq!(app.mode, Mode::ColumnSelector);
14001 }
14002
14003 #[test]
14004 fn test_sqs_queue_policies_tab_no_preferences() {
14005 let mut app = test_app();
14006 app.service_selected = true;
14007 app.current_service = Service::SqsQueues;
14008 app.sqs_state.current_queue = Some("test-queue".to_string());
14009 app.sqs_state.detail_tab = SqsQueueDetailTab::QueuePolicies;
14010 app.mode = Mode::Normal;
14011
14012 app.handle_action(Action::OpenColumnSelector);
14013
14014 assert_eq!(app.mode, Mode::Normal);
14015 }
14016
14017 #[test]
14018 fn test_sqs_sns_subscriptions_tab_shows_preferences() {
14019 let mut app = test_app();
14020 app.service_selected = true;
14021 app.current_service = Service::SqsQueues;
14022 app.sqs_state.current_queue = Some("test-queue".to_string());
14023 app.sqs_state.detail_tab = SqsQueueDetailTab::SnsSubscriptions;
14024 app.mode = Mode::Normal;
14025
14026 app.handle_action(Action::OpenColumnSelector);
14027
14028 assert_eq!(app.mode, Mode::ColumnSelector);
14029 }
14030
14031 #[test]
14032 fn test_sqs_monitoring_tab_no_preferences() {
14033 let mut app = test_app();
14034 app.service_selected = true;
14035 app.current_service = Service::SqsQueues;
14036 app.sqs_state.current_queue = Some("test-queue".to_string());
14037 app.sqs_state.detail_tab = SqsQueueDetailTab::Monitoring;
14038 app.mode = Mode::Normal;
14039
14040 app.handle_action(Action::OpenColumnSelector);
14041
14042 assert_eq!(app.mode, Mode::Normal);
14043 }
14044
14045 #[test]
14046 fn test_cfn_status_filter_change_resets_selection() {
14047 use crate::ui::cfn::STATUS_FILTER;
14048 let mut app = test_app();
14049 app.service_selected = true;
14050 app.current_service = Service::CloudFormationStacks;
14051 app.mode = Mode::FilterInput;
14052 app.cfn_state.input_focus = STATUS_FILTER;
14053 app.cfn_state.status_filter = CfnStatusFilter::All;
14054 app.cfn_state.table.items = vec![
14055 CfnStack {
14056 name: "stack1".to_string(),
14057 stack_id: "id1".to_string(),
14058 status: "CREATE_COMPLETE".to_string(),
14059 created_time: "2024-01-01".to_string(),
14060 updated_time: String::new(),
14061 deleted_time: String::new(),
14062 drift_status: String::new(),
14063 last_drift_check_time: String::new(),
14064 status_reason: String::new(),
14065 description: String::new(),
14066 detailed_status: String::new(),
14067 root_stack: String::new(),
14068 parent_stack: String::new(),
14069 termination_protection: false,
14070 iam_role: String::new(),
14071 tags: Vec::new(),
14072 stack_policy: String::new(),
14073 rollback_monitoring_time: String::new(),
14074 rollback_alarms: Vec::new(),
14075 notification_arns: Vec::new(),
14076 },
14077 CfnStack {
14078 name: "stack2".to_string(),
14079 stack_id: "id2".to_string(),
14080 status: "UPDATE_IN_PROGRESS".to_string(),
14081 created_time: "2024-01-02".to_string(),
14082 updated_time: String::new(),
14083 deleted_time: String::new(),
14084 drift_status: String::new(),
14085 last_drift_check_time: String::new(),
14086 status_reason: String::new(),
14087 description: String::new(),
14088 detailed_status: String::new(),
14089 root_stack: String::new(),
14090 parent_stack: String::new(),
14091 termination_protection: false,
14092 iam_role: String::new(),
14093 tags: Vec::new(),
14094 stack_policy: String::new(),
14095 rollback_monitoring_time: String::new(),
14096 rollback_alarms: Vec::new(),
14097 notification_arns: Vec::new(),
14098 },
14099 ];
14100 app.cfn_state.table.selected = 1;
14101
14102 app.handle_action(Action::NextItem);
14103
14104 assert_eq!(app.cfn_state.status_filter, CfnStatusFilter::Active);
14105 assert_eq!(app.cfn_state.table.selected, 0);
14106 }
14107
14108 #[test]
14109 fn test_cfn_view_nested_toggle_resets_selection() {
14110 use crate::ui::cfn::VIEW_NESTED;
14111 let mut app = test_app();
14112 app.service_selected = true;
14113 app.current_service = Service::CloudFormationStacks;
14114 app.mode = Mode::FilterInput;
14115 app.cfn_state.input_focus = VIEW_NESTED;
14116 app.cfn_state.view_nested = false;
14117 app.cfn_state.table.items = vec![CfnStack {
14118 name: "stack1".to_string(),
14119 stack_id: "id1".to_string(),
14120 status: "CREATE_COMPLETE".to_string(),
14121 created_time: "2024-01-01".to_string(),
14122 updated_time: String::new(),
14123 deleted_time: String::new(),
14124 drift_status: String::new(),
14125 last_drift_check_time: String::new(),
14126 status_reason: String::new(),
14127 description: String::new(),
14128 detailed_status: String::new(),
14129 root_stack: String::new(),
14130 parent_stack: String::new(),
14131 termination_protection: false,
14132 iam_role: String::new(),
14133 tags: Vec::new(),
14134 stack_policy: String::new(),
14135 rollback_monitoring_time: String::new(),
14136 rollback_alarms: Vec::new(),
14137 notification_arns: Vec::new(),
14138 }];
14139 app.cfn_state.table.selected = 5;
14140
14141 app.handle_action(Action::ToggleFilterCheckbox);
14142
14143 assert!(app.cfn_state.view_nested, "view_nested must be toggled on");
14144 assert_eq!(app.cfn_state.table.selected, 0, "selection must reset");
14145 assert!(
14149 app.cfn_state.table.loading,
14150 "loading must be set to trigger reload in main.rs"
14151 );
14152 assert!(
14153 app.cfn_state.table.loading,
14154 "loading must be set to trigger reload in main.rs"
14155 );
14156 }
14157
14158 #[test]
14159 fn test_cfn_view_nested_toggle_off_also_triggers_reload() {
14160 use crate::ui::cfn::VIEW_NESTED;
14162 let mut app = test_app();
14163 app.service_selected = true;
14164 app.current_service = Service::CloudFormationStacks;
14165 app.mode = Mode::FilterInput;
14166 app.cfn_state.input_focus = VIEW_NESTED;
14167 app.cfn_state.view_nested = true; app.cfn_state.table.items = vec![CfnStack {
14169 name: "nested-stack".to_string(),
14170 stack_id: "id2".to_string(),
14171 status: "CREATE_COMPLETE".to_string(),
14172 created_time: "2024-01-01".to_string(),
14173 updated_time: String::new(),
14174 deleted_time: String::new(),
14175 drift_status: String::new(),
14176 last_drift_check_time: String::new(),
14177 status_reason: String::new(),
14178 description: String::new(),
14179 detailed_status: String::new(),
14180 root_stack: String::new(),
14181 parent_stack: String::new(),
14182 termination_protection: false,
14183 iam_role: String::new(),
14184 tags: Vec::new(),
14185 stack_policy: String::new(),
14186 rollback_monitoring_time: String::new(),
14187 rollback_alarms: Vec::new(),
14188 notification_arns: Vec::new(),
14189 }];
14190
14191 app.handle_action(Action::ToggleFilterCheckbox);
14192
14193 assert!(
14194 !app.cfn_state.view_nested,
14195 "view_nested must be toggled off"
14196 );
14197 assert!(
14199 !app.cfn_state.table.items.is_empty(),
14200 "items must stay visible during reload (no flash)"
14201 );
14202 assert!(
14203 app.cfn_state.table.loading,
14204 "loading must be set to trigger reload"
14205 );
14206 }
14207
14208 #[test]
14209 fn test_cfn_view_nested_hierarchical_shows_children_when_expanded() {
14210 use crate::ui::cfn::filtered_cloudformation_stacks;
14213
14214 let mut app = test_app();
14215 app.current_service = Service::CloudFormationStacks;
14216 app.service_selected = true;
14217 app.cfn_state.view_nested = true;
14218
14219 let parent_id = "arn:aws:cloudformation:us-east-1:123:stack/parent/abc123".to_string();
14220 let child_id =
14221 "arn:aws:cloudformation:us-east-1:123:stack/parent-nested/def456".to_string();
14222
14223 app.cfn_state.table.items = vec![
14224 CfnStack {
14225 name: "parent-stack".to_string(),
14226 stack_id: parent_id.clone(),
14227 status: "CREATE_COMPLETE".to_string(),
14228 parent_stack: String::new(), root_stack: parent_id.clone(),
14230 created_time: "2024-01-02".to_string(),
14231 ..Default::default()
14232 },
14233 CfnStack {
14234 name: "child-stack".to_string(),
14235 stack_id: child_id.clone(),
14236 status: "CREATE_COMPLETE".to_string(),
14237 parent_stack: parent_id.clone(), root_stack: parent_id.clone(),
14239 created_time: "2024-01-01".to_string(),
14240 ..Default::default()
14241 },
14242 ];
14243
14244 let filtered = filtered_cloudformation_stacks(&app);
14246 assert_eq!(
14247 filtered.len(),
14248 2,
14249 "Root + child shown when view_nested=true"
14250 );
14251 assert_eq!(filtered[0].name, "parent-stack", "Root first");
14252 assert_eq!(filtered[1].name, "child-stack", "Child after root");
14253 }
14254
14255 #[test]
14256 fn test_cfn_template_scroll_up() {
14257 let mut app = test_app();
14258 app.service_selected = true;
14259 app.current_service = Service::CloudFormationStacks;
14260 app.cfn_state.current_stack = Some("test-stack".to_string());
14261 app.cfn_state.detail_tab = CfnDetailTab::Template;
14262 app.cfn_state.template_scroll = 20;
14263
14264 app.page_up();
14265
14266 assert_eq!(app.cfn_state.template_scroll, 10);
14267 }
14268
14269 #[test]
14270 fn test_cfn_template_scroll_down() {
14271 let mut app = test_app();
14272 app.service_selected = true;
14273 app.current_service = Service::CloudFormationStacks;
14274 app.cfn_state.current_stack = Some("test-stack".to_string());
14275 app.cfn_state.detail_tab = CfnDetailTab::Template;
14276 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14\nline15".to_string();
14277 app.cfn_state.template_scroll = 0;
14278
14279 app.page_down();
14280
14281 assert_eq!(app.cfn_state.template_scroll, 10);
14282 }
14283
14284 #[test]
14285 fn test_cfn_template_scroll_down_respects_max() {
14286 let mut app = test_app();
14287 app.service_selected = true;
14288 app.current_service = Service::CloudFormationStacks;
14289 app.cfn_state.current_stack = Some("test-stack".to_string());
14290 app.cfn_state.detail_tab = CfnDetailTab::Template;
14291 app.cfn_state.template_body = "line1\nline2\nline3".to_string();
14292 app.cfn_state.template_scroll = 0;
14293
14294 app.page_down();
14295
14296 assert_eq!(app.cfn_state.template_scroll, 2);
14298 }
14299
14300 #[test]
14301 fn test_cfn_template_arrow_up() {
14302 let mut app = test_app();
14303 app.service_selected = true;
14304 app.current_service = Service::CloudFormationStacks;
14305 app.mode = Mode::Normal;
14306 app.cfn_state.current_stack = Some("test-stack".to_string());
14307 app.cfn_state.detail_tab = CfnDetailTab::Template;
14308 app.cfn_state.template_scroll = 5;
14309
14310 app.prev_item();
14311
14312 assert_eq!(app.cfn_state.template_scroll, 4);
14313 }
14314
14315 #[test]
14316 fn test_cfn_template_arrow_down() {
14317 let mut app = test_app();
14318 app.service_selected = true;
14319 app.current_service = Service::CloudFormationStacks;
14320 app.mode = Mode::Normal;
14321 app.cfn_state.current_stack = Some("test-stack".to_string());
14322 app.cfn_state.detail_tab = CfnDetailTab::Template;
14323 app.cfn_state.template_body = "line1\nline2\nline3\nline4\nline5".to_string();
14324 app.cfn_state.template_scroll = 2;
14325
14326 app.next_item();
14327
14328 assert_eq!(app.cfn_state.template_scroll, 3);
14329 }
14330
14331 #[test]
14332 fn test_cfn_template_arrow_down_respects_max() {
14333 let mut app = test_app();
14334 app.service_selected = true;
14335 app.current_service = Service::CloudFormationStacks;
14336 app.mode = Mode::Normal;
14337 app.cfn_state.current_stack = Some("test-stack".to_string());
14338 app.cfn_state.detail_tab = CfnDetailTab::Template;
14339 app.cfn_state.template_body = "line1\nline2".to_string();
14340 app.cfn_state.template_scroll = 1;
14341
14342 app.next_item();
14343
14344 assert_eq!(app.cfn_state.template_scroll, 1);
14346 }
14347
14348 #[test]
14349 fn test_cfn_yank_on_template_tab_copies_template_body() {
14350 let mut app = test_app();
14353 app.current_service = Service::CloudFormationStacks;
14354 app.cfn_state.current_stack = Some("my-stack".to_string());
14355 app.cfn_state.detail_tab = CfnDetailTab::Template;
14356 app.cfn_state.template_body = "AWSTemplateFormatVersion: '2010-09-09'".to_string();
14357 assert!(!app.cfn_state.template_body.is_empty());
14361 crate::cfn::actions::yank(&app); }
14363
14364 #[test]
14365 fn test_cfn_change_sets_console_url_includes_events_view_graph() {
14366 use crate::cfn::console_url_stack_detail_with_tab;
14367 let url = console_url_stack_detail_with_tab(
14368 "us-east-1",
14369 "arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack/abc123",
14370 &CfnDetailTab::ChangeSets,
14371 );
14372 assert!(
14373 url.contains("eventsView=graph"),
14374 "ChangeSets URL must include eventsView=graph, got: {url}"
14375 );
14376 assert!(url.contains("changesets"), "URL must contain 'changesets'");
14377 }
14378
14379 #[test]
14380 fn test_cfn_other_tabs_console_url_no_events_view_graph() {
14381 use crate::cfn::console_url_stack_detail_with_tab;
14382 for tab in [
14383 CfnDetailTab::StackInfo,
14384 CfnDetailTab::Events,
14385 CfnDetailTab::Resources,
14386 ] {
14387 let url = console_url_stack_detail_with_tab(
14388 "us-east-1",
14389 "arn:aws:cloudformation:us-east-1:123456789012:stack/my-stack/abc123",
14390 &tab,
14391 );
14392 assert!(
14393 !url.contains("eventsView=graph"),
14394 "{tab:?} URL must NOT include eventsView=graph, got: {url}"
14395 );
14396 }
14397 }
14398
14399 #[test]
14400 fn test_cfn_change_sets_expand_collapse() {
14401 let mut app = test_app();
14402 app.current_service = Service::CloudFormationStacks;
14403 app.service_selected = true;
14404 app.cfn_state.current_stack = Some("my-stack".to_string());
14405 app.cfn_state.detail_tab = CfnDetailTab::ChangeSets;
14406 app.cfn_state.change_sets.items = vec![rusticity_core::cfn::StackChangeSet {
14407 name: "cs-001".to_string(),
14408 change_set_id: "id-001".to_string(),
14409 created_time: String::new(),
14410 status: "CREATE_COMPLETE".to_string(),
14411 description: String::new(),
14412 root_change_set_id: String::new(),
14413 parent_change_set_id: String::new(),
14414 }];
14415 app.cfn_state.change_sets.selected = 0;
14416
14417 assert_eq!(app.cfn_state.change_sets.expanded_item, None);
14418 app.handle_action(Action::ExpandRow);
14419 assert_eq!(
14420 app.cfn_state.change_sets.expanded_item,
14421 Some(0),
14422 "Enter must expand change set row"
14423 );
14424 app.handle_action(Action::CollapseRow);
14425 assert_eq!(
14426 app.cfn_state.change_sets.expanded_item, None,
14427 "Left arrow must collapse change set row"
14428 );
14429 }
14430
14431 #[test]
14432 fn test_cfn_events_toggle_column_hides_visible_column() {
14433 let mut app = test_app();
14436 app.current_service = Service::CloudFormationStacks;
14437 app.mode = Mode::ColumnSelector;
14438 app.cfn_state.current_stack = Some("my-stack".to_string());
14439 app.cfn_state.detail_tab = CfnDetailTab::Events;
14440
14441 let first_col = app.cfn_event_column_ids[0];
14443 assert!(
14444 app.cfn_event_visible_column_ids.contains(&first_col),
14445 "First column must be visible before toggle"
14446 );
14447
14448 app.column_selector_index = 1; app.handle_action(Action::ToggleColumn);
14450
14451 assert!(
14452 !app.cfn_event_visible_column_ids.contains(&first_col),
14453 "ToggleColumn must hide the first Events column"
14454 );
14455
14456 app.handle_action(Action::ToggleColumn);
14458 assert!(
14459 app.cfn_event_visible_column_ids.contains(&first_col),
14460 "ToggleColumn again must re-show the column"
14461 );
14462 }
14463
14464 #[test]
14465 fn test_cfn_events_toggle_column_keeps_at_least_one_visible() {
14466 let mut app = test_app();
14468 app.current_service = Service::CloudFormationStacks;
14469 app.mode = Mode::ColumnSelector;
14470 app.cfn_state.current_stack = Some("my-stack".to_string());
14471 app.cfn_state.detail_tab = CfnDetailTab::Events;
14472
14473 app.cfn_event_visible_column_ids = vec![app.cfn_event_column_ids[0]];
14475 app.column_selector_index = 1;
14476
14477 app.handle_action(Action::ToggleColumn);
14478
14479 assert!(
14481 !app.cfn_event_visible_column_ids.is_empty(),
14482 "At least one column must remain visible"
14483 );
14484 }
14485
14486 #[test]
14487 fn test_space_menu_o_opens_service_picker() {
14488 let mut app = test_app();
14490 app.handle_action(Action::OpenSpaceMenu);
14491 assert_eq!(app.mode, Mode::SpaceMenu);
14492
14493 app.handle_action(Action::OpenServicePicker);
14494 assert_eq!(
14495 app.mode,
14496 Mode::ServicePicker,
14497 "'o' from SpaceMenu must open ServicePicker"
14498 );
14499 }
14500
14501 #[test]
14502 fn test_space_menu_c_closes_service() {
14503 let mut app = test_app();
14504 app.current_service = Service::CloudFormationStacks;
14505 app.service_selected = true;
14506 app.tabs.push(Tab {
14507 service: Service::CloudFormationStacks,
14508 title: "CFN".to_string(),
14509 breadcrumb: "CloudFormation".to_string(),
14510 });
14511 app.current_tab = 0;
14512
14513 app.handle_action(Action::OpenSpaceMenu);
14514 app.handle_action(Action::CloseService);
14515 assert_ne!(
14517 app.mode,
14518 Mode::SpaceMenu,
14519 "CloseService must exit SpaceMenu"
14520 );
14521 }
14522}
14523
14524#[cfg(test)]
14525mod lambda_version_tab_tests {
14526 use super::*;
14527 use crate::ui::iam::POLICY_TYPE_DROPDOWN;
14528 use test_helpers::*;
14529
14530 #[test]
14531 fn test_lambda_version_tab_cycling_next() {
14532 let mut app = test_app();
14533 app.current_service = Service::LambdaFunctions;
14534 app.lambda_state.current_function = Some("test-function".to_string());
14535 app.lambda_state.current_version = Some("1".to_string());
14536 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14537
14538 app.handle_action(Action::NextDetailTab);
14540 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14541 assert!(app.lambda_state.metrics_loading);
14542
14543 app.lambda_state.metrics_loading = false;
14545 app.handle_action(Action::NextDetailTab);
14546 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14547
14548 app.handle_action(Action::NextDetailTab);
14550 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14551 }
14552
14553 #[test]
14554 fn test_lambda_version_tab_cycling_prev() {
14555 let mut app = test_app();
14556 app.current_service = Service::LambdaFunctions;
14557 app.lambda_state.current_function = Some("test-function".to_string());
14558 app.lambda_state.current_version = Some("1".to_string());
14559 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14560
14561 app.handle_action(Action::PrevDetailTab);
14563 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Configuration);
14564
14565 app.handle_action(Action::PrevDetailTab);
14567 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14568 assert!(app.lambda_state.metrics_loading);
14569
14570 app.lambda_state.metrics_loading = false;
14572 app.handle_action(Action::PrevDetailTab);
14573 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Code);
14574 }
14575
14576 #[test]
14577 fn test_lambda_version_monitor_clears_metrics() {
14578 let mut app = test_app();
14579 app.current_service = Service::LambdaFunctions;
14580 app.lambda_state.current_function = Some("test-function".to_string());
14581 app.lambda_state.current_version = Some("1".to_string());
14582 app.lambda_state.detail_tab = LambdaDetailTab::Code;
14583
14584 app.lambda_state.metric_data_invocations = vec![(1, 10.0), (2, 20.0)];
14586 app.lambda_state.monitoring_scroll = 5;
14587
14588 app.handle_action(Action::NextDetailTab);
14590
14591 assert_eq!(app.lambda_state.detail_tab, LambdaDetailTab::Monitor);
14592 assert!(app.lambda_state.metrics_loading);
14593 assert_eq!(app.lambda_state.monitoring_scroll, 0);
14594 assert!(app.lambda_state.metric_data_invocations.is_empty());
14595 }
14596
14597 #[test]
14598 fn test_cfn_parameters_expand_collapse() {
14599 let mut app = test_app();
14600 app.current_service = Service::CloudFormationStacks;
14601 app.service_selected = true;
14602 app.cfn_state.current_stack = Some("test-stack".to_string());
14603 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
14604 app.cfn_state.parameters.items = vec![rusticity_core::cfn::StackParameter {
14605 key: "Param1".to_string(),
14606 value: "Value1".to_string(),
14607 resolved_value: "Resolved1".to_string(),
14608 }];
14609 app.cfn_state.parameters.reset();
14610
14611 assert_eq!(app.cfn_state.parameters.expanded_item, None);
14612
14613 app.handle_action(Action::NextPane);
14615 assert_eq!(app.cfn_state.parameters.expanded_item, Some(0));
14616
14617 app.handle_action(Action::PrevPane);
14619 assert_eq!(app.cfn_state.parameters.expanded_item, None);
14620 }
14621
14622 #[test]
14623 fn test_cfn_parameters_filter_resets_selection() {
14624 let mut app = test_app();
14625 app.current_service = Service::CloudFormationStacks;
14626 app.service_selected = true;
14627 app.cfn_state.current_stack = Some("test-stack".to_string());
14628 app.cfn_state.detail_tab = CfnDetailTab::Parameters;
14629 app.cfn_state.parameters.items = vec![
14630 rusticity_core::cfn::StackParameter {
14631 key: "DatabaseName".to_string(),
14632 value: "mydb".to_string(),
14633 resolved_value: "mydb".to_string(),
14634 },
14635 rusticity_core::cfn::StackParameter {
14636 key: "InstanceType".to_string(),
14637 value: "t2.micro".to_string(),
14638 resolved_value: "t2.micro".to_string(),
14639 },
14640 rusticity_core::cfn::StackParameter {
14641 key: "Environment".to_string(),
14642 value: "production".to_string(),
14643 resolved_value: "production".to_string(),
14644 },
14645 ];
14646 app.cfn_state.parameters.selected = 2; app.mode = Mode::FilterInput;
14648 app.cfn_state.parameters_input_focus = InputFocus::Filter;
14649
14650 app.handle_action(Action::FilterInput('D'));
14652 assert_eq!(app.cfn_state.parameters.selected, 0);
14653 assert_eq!(app.cfn_state.parameters.filter, "D");
14654
14655 app.cfn_state.parameters.selected = 1;
14657
14658 app.handle_action(Action::FilterInput('a'));
14660 assert_eq!(app.cfn_state.parameters.selected, 0);
14661 assert_eq!(app.cfn_state.parameters.filter, "Da");
14662
14663 app.cfn_state.parameters.selected = 1;
14665
14666 app.handle_action(Action::FilterBackspace);
14668 assert_eq!(app.cfn_state.parameters.selected, 0);
14669 assert_eq!(app.cfn_state.parameters.filter, "D");
14670 }
14671
14672 #[test]
14673 fn test_cfn_template_tab_no_preferences() {
14674 let mut app = test_app();
14675 app.current_service = Service::CloudFormationStacks;
14676 app.service_selected = true;
14677 app.cfn_state.current_stack = Some("test-stack".to_string());
14678 app.cfn_state.detail_tab = CfnDetailTab::Template;
14679 app.mode = Mode::Normal;
14680
14681 app.handle_action(Action::OpenColumnSelector);
14683 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::GitSync;
14687 app.handle_action(Action::OpenColumnSelector);
14688 assert_eq!(app.mode, Mode::Normal); app.cfn_state.detail_tab = CfnDetailTab::Parameters;
14692 app.handle_action(Action::OpenColumnSelector);
14693 assert_eq!(app.mode, Mode::ColumnSelector); app.mode = Mode::Normal;
14697 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
14698 app.handle_action(Action::OpenColumnSelector);
14699 assert_eq!(app.mode, Mode::ColumnSelector); }
14701
14702 #[test]
14703 fn test_iam_user_groups_tab_shows_preferences() {
14704 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14705 app.current_service = Service::IamUsers;
14706 app.service_selected = true;
14707 app.mode = Mode::Normal;
14708 app.iam_state.current_user = Some("test-user".to_string());
14709 app.iam_state.user_tab = UserTab::Groups;
14710
14711 app.handle_action(Action::OpenColumnSelector);
14713 assert_eq!(app.mode, Mode::ColumnSelector);
14714 }
14715
14716 #[test]
14717 fn test_iam_user_tags_tab_shows_preferences() {
14718 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14719 app.current_service = Service::IamUsers;
14720 app.service_selected = true;
14721 app.mode = Mode::Normal;
14722 app.iam_state.current_user = Some("test-user".to_string());
14723 app.iam_state.user_tab = UserTab::Tags;
14724
14725 app.handle_action(Action::OpenColumnSelector);
14727 assert_eq!(app.mode, Mode::ColumnSelector);
14728 }
14729
14730 #[test]
14731 fn test_iam_user_last_accessed_tab_shows_preferences() {
14732 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14733 app.current_service = Service::IamUsers;
14734 app.service_selected = true;
14735 app.mode = Mode::Normal;
14736 app.iam_state.current_user = Some("test-user".to_string());
14737 app.iam_state.user_tab = UserTab::LastAccessed;
14738
14739 app.handle_action(Action::OpenColumnSelector);
14741 assert_eq!(app.mode, Mode::ColumnSelector);
14742 }
14743
14744 #[test]
14745 fn test_iam_user_security_credentials_tab_no_preferences() {
14746 let mut app = App::new_without_client("test".to_string(), Some("us-east-1".to_string()));
14747 app.current_service = Service::IamUsers;
14748 app.service_selected = true;
14749 app.mode = Mode::Normal;
14750 app.iam_state.current_user = Some("test-user".to_string());
14751 app.iam_state.user_tab = UserTab::SecurityCredentials;
14752
14753 app.handle_action(Action::OpenColumnSelector);
14755 assert_eq!(app.mode, Mode::Normal);
14756 }
14757
14758 #[test]
14759 fn test_iam_user_tabs_without_column_preferences() {
14760 let mut app = test_app();
14761 app.current_service = Service::IamUsers;
14762 app.service_selected = true;
14763 app.iam_state.current_user = Some("test-user".to_string());
14764 app.mode = Mode::Normal;
14765
14766 app.iam_state.user_tab = UserTab::Groups;
14768 app.handle_action(Action::OpenColumnSelector);
14769 assert_eq!(app.mode, Mode::ColumnSelector);
14770 app.mode = Mode::Normal;
14771
14772 app.iam_state.user_tab = UserTab::Tags;
14774 app.handle_action(Action::OpenColumnSelector);
14775 assert_eq!(app.mode, Mode::ColumnSelector);
14776 app.mode = Mode::Normal;
14777
14778 app.iam_state.user_tab = UserTab::SecurityCredentials;
14780 app.handle_action(Action::OpenColumnSelector);
14781 assert_eq!(app.mode, Mode::Normal);
14782
14783 app.iam_state.user_tab = UserTab::LastAccessed;
14785 app.handle_action(Action::OpenColumnSelector);
14786 assert_eq!(app.mode, Mode::ColumnSelector);
14787 app.mode = Mode::Normal;
14788
14789 app.iam_state.user_tab = UserTab::Permissions;
14791 app.handle_action(Action::OpenColumnSelector);
14792 assert_eq!(app.mode, Mode::ColumnSelector);
14793
14794 app.mode = Mode::Normal;
14796 app.iam_state.current_user = None;
14797 app.handle_action(Action::OpenColumnSelector);
14798 assert_eq!(app.mode, Mode::ColumnSelector);
14799 }
14800
14801 #[test]
14802 fn test_iam_role_policies_dropdown_cycling() {
14803 let mut app = test_app();
14804 app.current_service = Service::IamRoles;
14805 app.service_selected = true;
14806 app.iam_state.current_role = Some("test-role".to_string());
14807 app.iam_state.role_tab = RoleTab::Permissions;
14808 app.mode = Mode::FilterInput;
14809 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
14810 app.iam_state.policy_type_filter = "All types".to_string();
14811
14812 app.next_item();
14814 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
14815 app.next_item();
14816 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
14817 app.next_item();
14818 assert_eq!(app.iam_state.policy_type_filter, "All types");
14819
14820 app.prev_item();
14822 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
14823 app.prev_item();
14824 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
14825 app.prev_item();
14826 assert_eq!(app.iam_state.policy_type_filter, "All types");
14827 }
14828
14829 #[test]
14830 fn test_iam_user_policies_dropdown_cycling() {
14831 let mut app = test_app();
14832 app.current_service = Service::IamUsers;
14833 app.service_selected = true;
14834 app.iam_state.current_user = Some("test-user".to_string());
14835 app.iam_state.user_tab = UserTab::Permissions;
14836 app.mode = Mode::FilterInput;
14837 app.iam_state.policy_input_focus = POLICY_TYPE_DROPDOWN;
14838 app.iam_state.policy_type_filter = "All types".to_string();
14839
14840 app.next_item();
14842 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
14843 app.next_item();
14844 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
14845 app.next_item();
14846 assert_eq!(app.iam_state.policy_type_filter, "All types");
14847
14848 app.prev_item();
14850 assert_eq!(app.iam_state.policy_type_filter, "Customer managed");
14851 app.prev_item();
14852 assert_eq!(app.iam_state.policy_type_filter, "AWS managed");
14853 app.prev_item();
14854 assert_eq!(app.iam_state.policy_type_filter, "All types");
14855 }
14856
14857 #[test]
14858 fn test_iam_role_tabs_without_column_preferences() {
14859 let mut app = test_app();
14860 app.current_service = Service::IamRoles;
14861 app.service_selected = true;
14862 app.iam_state.current_role = Some("test-role".to_string());
14863 app.mode = Mode::Normal;
14864
14865 app.iam_state.role_tab = RoleTab::TrustRelationships;
14867 app.handle_action(Action::OpenColumnSelector);
14868 assert_eq!(app.mode, Mode::Normal);
14869
14870 app.iam_state.role_tab = RoleTab::RevokeSessions;
14872 app.handle_action(Action::OpenColumnSelector);
14873 assert_eq!(app.mode, Mode::Normal);
14874
14875 app.iam_state.role_tab = RoleTab::LastAccessed;
14877 app.handle_action(Action::OpenColumnSelector);
14878 assert_eq!(app.mode, Mode::ColumnSelector);
14879
14880 app.mode = Mode::Normal;
14882 app.iam_state.role_tab = RoleTab::Permissions;
14883 app.handle_action(Action::OpenColumnSelector);
14884 assert_eq!(app.mode, Mode::ColumnSelector);
14885
14886 app.mode = Mode::Normal;
14888 app.iam_state.role_tab = RoleTab::Tags;
14889 app.handle_action(Action::OpenColumnSelector);
14890 assert_eq!(app.mode, Mode::ColumnSelector);
14891
14892 app.mode = Mode::Normal;
14894 app.iam_state.current_role = None;
14895 app.handle_action(Action::OpenColumnSelector);
14896 assert_eq!(app.mode, Mode::ColumnSelector);
14897 }
14898
14899 #[test]
14900 fn test_iam_role_tags_tab_cycling() {
14901 let mut app = test_app();
14902 app.current_service = Service::IamRoles;
14903 app.service_selected = true;
14904 app.iam_state.current_role = Some("test-role".to_string());
14905 app.iam_state.role_tab = RoleTab::Tags;
14906 app.mode = Mode::ColumnSelector;
14907 app.column_selector_index = 0;
14908
14909 app.handle_action(Action::NextPreferences);
14911 assert_eq!(app.column_selector_index, 4);
14912
14913 app.handle_action(Action::NextPreferences);
14915 assert_eq!(app.column_selector_index, 0);
14916
14917 app.handle_action(Action::PrevPreferences);
14919 assert_eq!(app.column_selector_index, 4);
14920
14921 app.handle_action(Action::PrevPreferences);
14923 assert_eq!(app.column_selector_index, 0);
14924 }
14925
14926 #[test]
14927 fn test_cfn_outputs_expand_collapse() {
14928 let mut app = test_app();
14929 app.current_service = Service::CloudFormationStacks;
14930 app.service_selected = true;
14931 app.cfn_state.current_stack = Some("test-stack".to_string());
14932 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
14933 app.cfn_state.outputs.items = vec![rusticity_core::cfn::StackOutput {
14934 key: "Output1".to_string(),
14935 value: "Value1".to_string(),
14936 description: "Description1".to_string(),
14937 export_name: "Export1".to_string(),
14938 }];
14939 app.cfn_state.outputs.reset();
14940
14941 assert_eq!(app.cfn_state.outputs.expanded_item, None);
14942
14943 app.handle_action(Action::NextPane);
14945 assert_eq!(app.cfn_state.outputs.expanded_item, Some(0));
14946
14947 app.handle_action(Action::PrevPane);
14949 assert_eq!(app.cfn_state.outputs.expanded_item, None);
14950 }
14951
14952 #[test]
14953 fn test_cfn_outputs_filter_resets_selection() {
14954 let mut app = test_app();
14955 app.current_service = Service::CloudFormationStacks;
14956 app.service_selected = true;
14957 app.cfn_state.current_stack = Some("test-stack".to_string());
14958 app.cfn_state.detail_tab = CfnDetailTab::Outputs;
14959 app.cfn_state.outputs.items = vec![
14960 rusticity_core::cfn::StackOutput {
14961 key: "ApiUrl".to_string(),
14962 value: "https://api.example.com".to_string(),
14963 description: "API endpoint".to_string(),
14964 export_name: "MyApiUrl".to_string(),
14965 },
14966 rusticity_core::cfn::StackOutput {
14967 key: "BucketName".to_string(),
14968 value: "my-bucket".to_string(),
14969 description: "S3 bucket".to_string(),
14970 export_name: "MyBucket".to_string(),
14971 },
14972 ];
14973 app.cfn_state.outputs.reset();
14974 app.cfn_state.outputs.selected = 1;
14975
14976 app.handle_action(Action::StartFilter);
14978 assert_eq!(app.mode, Mode::FilterInput);
14979
14980 app.handle_action(Action::FilterInput('A'));
14982 assert_eq!(app.cfn_state.outputs.selected, 0);
14983 assert_eq!(app.cfn_state.outputs.filter, "A");
14984
14985 app.cfn_state.outputs.selected = 1;
14987 app.handle_action(Action::FilterInput('p'));
14988 assert_eq!(app.cfn_state.outputs.selected, 0);
14989
14990 app.cfn_state.outputs.selected = 1;
14992 app.handle_action(Action::FilterBackspace);
14993 assert_eq!(app.cfn_state.outputs.selected, 0);
14994 }
14995
14996 #[test]
14997 fn test_ec2_service_in_picker() {
14998 let app = test_app();
14999 assert!(app.service_picker.services.contains(&"EC2 › Instances"));
15000 }
15001
15002 #[test]
15003 fn test_ec2_state_filter_cycles() {
15004 let mut app = test_app();
15005 app.current_service = Service::Ec2Instances;
15006 app.service_selected = true;
15007 app.mode = Mode::FilterInput;
15008 app.ec2_state.input_focus = EC2_STATE_FILTER;
15009
15010 let initial = app.ec2_state.state_filter;
15011 assert_eq!(initial, Ec2StateFilter::AllStates);
15012
15013 app.handle_action(Action::ToggleFilterCheckbox);
15015 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15016
15017 app.handle_action(Action::ToggleFilterCheckbox);
15018 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
15019
15020 app.handle_action(Action::ToggleFilterCheckbox);
15021 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Terminated);
15022
15023 app.handle_action(Action::ToggleFilterCheckbox);
15024 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Pending);
15025
15026 app.handle_action(Action::ToggleFilterCheckbox);
15027 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::ShuttingDown);
15028
15029 app.handle_action(Action::ToggleFilterCheckbox);
15030 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
15031
15032 app.handle_action(Action::ToggleFilterCheckbox);
15033 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15034 }
15035
15036 #[test]
15037 fn test_ec2_filter_resets_table() {
15038 let mut app = test_app();
15039 app.current_service = Service::Ec2Instances;
15040 app.service_selected = true;
15041 app.mode = Mode::FilterInput;
15042 app.ec2_state.input_focus = EC2_STATE_FILTER;
15043 app.ec2_state.table.selected = 5;
15044
15045 app.handle_action(Action::ToggleFilterCheckbox);
15046 assert_eq!(app.ec2_state.table.selected, 0);
15047 }
15048
15049 #[test]
15050 fn test_ec2_columns_visible() {
15051 let app = test_app();
15052 assert_eq!(app.ec2_visible_column_ids.len(), 16); assert_eq!(app.ec2_column_ids.len(), 52); }
15055
15056 #[test]
15057 fn test_ec2_breadcrumbs() {
15058 let mut app = test_app();
15059 app.current_service = Service::Ec2Instances;
15060 app.service_selected = true;
15061 let breadcrumb = app.breadcrumbs();
15062 assert_eq!(breadcrumb, "EC2 > Instances");
15063 }
15064
15065 #[test]
15066 fn test_ec2_console_url() {
15067 let mut app = test_app();
15068 app.current_service = Service::Ec2Instances;
15069 app.service_selected = true;
15070 let url = app.get_console_url();
15071 assert!(url.contains("ec2"));
15072 assert!(url.contains("Instances"));
15073 }
15074
15075 #[test]
15076 fn test_ec2_filter_handling() {
15077 let mut app = test_app();
15078 app.current_service = Service::Ec2Instances;
15079 app.service_selected = true;
15080 app.mode = Mode::FilterInput;
15081
15082 app.handle_action(Action::FilterInput('t'));
15083 app.handle_action(Action::FilterInput('e'));
15084 app.handle_action(Action::FilterInput('s'));
15085 app.handle_action(Action::FilterInput('t'));
15086
15087 assert_eq!(app.ec2_state.table.filter, "test");
15088
15089 app.handle_action(Action::FilterBackspace);
15090 assert_eq!(app.ec2_state.table.filter, "tes");
15091 }
15092
15093 #[test]
15094 fn test_column_selector_page_down_ec2() {
15095 let mut app = test_app();
15096 app.current_service = Service::Ec2Instances;
15097 app.service_selected = true;
15098 app.mode = Mode::ColumnSelector;
15099 app.column_selector_index = 0;
15100
15101 app.handle_action(Action::PageDown);
15102 assert_eq!(app.column_selector_index, 10);
15103
15104 app.handle_action(Action::PageDown);
15105 assert_eq!(app.column_selector_index, 20);
15106 }
15107
15108 #[test]
15109 fn test_column_selector_page_up_ec2() {
15110 let mut app = test_app();
15111 app.current_service = Service::Ec2Instances;
15112 app.service_selected = true;
15113 app.mode = Mode::ColumnSelector;
15114 app.column_selector_index = 30;
15115
15116 app.handle_action(Action::PageUp);
15117 assert_eq!(app.column_selector_index, 20);
15118
15119 app.handle_action(Action::PageUp);
15120 assert_eq!(app.column_selector_index, 10);
15121 }
15122
15123 #[test]
15124 fn test_ec2_state_filter_dropdown_focus() {
15125 let mut app = test_app();
15126 app.current_service = Service::Ec2Instances;
15127 app.service_selected = true;
15128 app.mode = Mode::FilterInput;
15129
15130 app.handle_action(Action::NextFilterFocus);
15132 assert_eq!(app.ec2_state.input_focus, EC2_STATE_FILTER);
15133
15134 app.handle_action(Action::ToggleFilterCheckbox);
15137 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15138 }
15139
15140 #[test]
15141 fn test_column_selector_ctrl_d_scrolling() {
15142 let mut app = test_app();
15143 app.current_service = Service::LambdaFunctions;
15144 app.mode = Mode::ColumnSelector;
15145 app.column_selector_index = 0;
15146
15147 app.handle_action(Action::PageDown);
15149 assert_eq!(app.column_selector_index, 11);
15150
15151 let max = app.get_column_selector_max();
15153 app.handle_action(Action::PageDown);
15154 assert_eq!(app.column_selector_index, max);
15155 }
15156
15157 #[test]
15158 fn test_column_selector_ctrl_u_scrolling() {
15159 let mut app = test_app();
15160 app.current_service = Service::CloudFormationStacks;
15161 app.mode = Mode::ColumnSelector;
15162 app.column_selector_index = 25;
15163
15164 app.handle_action(Action::PageUp);
15165 assert_eq!(app.column_selector_index, 15);
15166
15167 app.handle_action(Action::PageUp);
15168 assert_eq!(app.column_selector_index, 5);
15169 }
15170
15171 #[test]
15172 fn test_prev_preferences_lambda() {
15173 let mut app = test_app();
15174 app.current_service = Service::LambdaFunctions;
15175 app.mode = Mode::ColumnSelector;
15176 let page_size_idx = app.lambda_state.function_column_ids.len() + 2;
15177 app.column_selector_index = page_size_idx;
15178
15179 app.handle_action(Action::PrevPreferences);
15180 assert_eq!(app.column_selector_index, 0);
15181
15182 app.handle_action(Action::PrevPreferences);
15183 assert_eq!(app.column_selector_index, page_size_idx);
15184 }
15185
15186 #[test]
15187 fn test_prev_preferences_cloudformation() {
15188 let mut app = test_app();
15189 app.current_service = Service::CloudFormationStacks;
15190 app.mode = Mode::ColumnSelector;
15191 let page_size_idx = app.cfn_column_ids.len() + 2;
15192 app.column_selector_index = page_size_idx;
15193
15194 app.handle_action(Action::PrevPreferences);
15195 assert_eq!(app.column_selector_index, 0);
15196
15197 app.handle_action(Action::PrevPreferences);
15198 assert_eq!(app.column_selector_index, page_size_idx);
15199 }
15200
15201 #[test]
15202 fn test_prev_preferences_alarms() {
15203 let mut app = test_app();
15204 app.current_service = Service::CloudWatchAlarms;
15205 app.mode = Mode::ColumnSelector;
15206 app.column_selector_index = 28; app.handle_action(Action::PrevPreferences);
15209 assert_eq!(app.column_selector_index, 22); app.handle_action(Action::PrevPreferences);
15212 assert_eq!(app.column_selector_index, 18); app.handle_action(Action::PrevPreferences);
15215 assert_eq!(app.column_selector_index, 0); app.handle_action(Action::PrevPreferences);
15218 assert_eq!(app.column_selector_index, 28); }
15220
15221 #[test]
15222 fn test_ec2_page_size_in_preferences() {
15223 let mut app = test_app();
15224 app.current_service = Service::Ec2Instances;
15225 app.mode = Mode::ColumnSelector;
15226 app.ec2_state.table.page_size = PageSize::Fifty;
15227
15228 let page_size_idx = app.ec2_column_ids.len() + 3; app.column_selector_index = page_size_idx;
15231 app.handle_action(Action::ToggleColumn);
15232
15233 assert_eq!(app.ec2_state.table.page_size, PageSize::Ten);
15234 }
15235
15236 #[test]
15237 fn test_ec2_next_preferences_with_page_size() {
15238 let mut app = test_app();
15239 app.current_service = Service::Ec2Instances;
15240 app.mode = Mode::ColumnSelector;
15241 app.column_selector_index = 0;
15242
15243 let page_size_idx = app.ec2_column_ids.len() + 2;
15244 app.handle_action(Action::NextPreferences);
15245 assert_eq!(app.column_selector_index, page_size_idx);
15246
15247 app.handle_action(Action::NextPreferences);
15248 assert_eq!(app.column_selector_index, 0);
15249 }
15250
15251 #[test]
15252 fn test_ec2_dropdown_next_item() {
15253 let mut app = test_app();
15254 app.current_service = Service::Ec2Instances;
15255 app.mode = Mode::FilterInput;
15256 app.ec2_state.input_focus = EC2_STATE_FILTER;
15257 app.ec2_state.state_filter = Ec2StateFilter::AllStates;
15258
15259 app.handle_action(Action::NextItem);
15260 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15261
15262 app.handle_action(Action::NextItem);
15263 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopped);
15264 }
15265
15266 #[test]
15267 fn test_ec2_dropdown_prev_item() {
15268 let mut app = test_app();
15269 app.current_service = Service::Ec2Instances;
15270 app.mode = Mode::FilterInput;
15271 app.ec2_state.input_focus = EC2_STATE_FILTER;
15272 app.ec2_state.state_filter = Ec2StateFilter::Stopped;
15273
15274 app.handle_action(Action::PrevItem);
15275 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Running);
15276
15277 app.handle_action(Action::PrevItem);
15278 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15279 }
15280
15281 #[test]
15282 fn test_ec2_dropdown_cycles_with_arrows() {
15283 let mut app = test_app();
15284 app.current_service = Service::Ec2Instances;
15285 app.mode = Mode::FilterInput;
15286 app.ec2_state.input_focus = EC2_STATE_FILTER;
15287 app.ec2_state.state_filter = Ec2StateFilter::Stopping;
15288
15289 app.handle_action(Action::NextItem);
15291 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::AllStates);
15292
15293 app.handle_action(Action::PrevItem);
15295 assert_eq!(app.ec2_state.state_filter, Ec2StateFilter::Stopping);
15296 }
15297
15298 #[test]
15299 fn test_collapse_row_ec2_instances() {
15300 let mut app = test_app();
15301 app.current_service = Service::Ec2Instances;
15302 app.ec2_state.table.expanded_item = Some(0);
15303
15304 app.handle_action(Action::CollapseRow);
15305 assert_eq!(app.ec2_state.table.expanded_item, None);
15306 }
15307
15308 #[test]
15309 fn test_collapse_row_ec2_tags() {
15310 let mut app = test_app();
15311 app.current_service = Service::Ec2Instances;
15312 app.ec2_state.current_instance = Some("i-123".to_string());
15313 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
15314 app.ec2_state.tags.expanded_item = Some(1);
15315
15316 app.handle_action(Action::CollapseRow);
15317 assert_eq!(app.ec2_state.tags.expanded_item, None);
15318 }
15319
15320 #[test]
15321 fn test_collapse_row_cloudwatch_log_groups() {
15322 let mut app = test_app();
15323 app.current_service = Service::CloudWatchLogGroups;
15324 app.log_groups_state.log_groups.expanded_item = Some(2);
15325
15326 app.handle_action(Action::CollapseRow);
15327 assert_eq!(app.log_groups_state.log_groups.expanded_item, None);
15328 }
15329
15330 #[test]
15331 fn test_collapse_row_cloudwatch_alarms() {
15332 let mut app = test_app();
15333 app.current_service = Service::CloudWatchAlarms;
15334 app.alarms_state.table.expanded_item = Some(0);
15335
15336 app.handle_action(Action::CollapseRow);
15337 assert_eq!(app.alarms_state.table.expanded_item, None);
15338 }
15339
15340 #[test]
15341 fn test_collapse_row_lambda_functions() {
15342 let mut app = test_app();
15343 app.current_service = Service::LambdaFunctions;
15344 app.lambda_state.table.expanded_item = Some(1);
15345
15346 app.handle_action(Action::CollapseRow);
15347 assert_eq!(app.lambda_state.table.expanded_item, None);
15348 }
15349
15350 #[test]
15351 fn test_collapse_row_cfn_stacks() {
15352 let mut app = test_app();
15353 app.current_service = Service::CloudFormationStacks;
15354 app.cfn_state.table.expanded_item = Some(0);
15355
15356 app.handle_action(Action::CollapseRow);
15357 assert_eq!(app.cfn_state.table.expanded_item, None);
15358 }
15359
15360 #[test]
15361 fn test_collapse_row_cfn_resources() {
15362 let mut app = test_app();
15363 app.current_service = Service::CloudFormationStacks;
15364 app.cfn_state.current_stack = Some("test-stack".to_string());
15365 app.cfn_state.detail_tab = crate::ui::cfn::DetailTab::Resources;
15366 app.cfn_state.resources.expanded_item = Some(2);
15367
15368 app.handle_action(Action::CollapseRow);
15369 assert_eq!(app.cfn_state.resources.expanded_item, None);
15370 }
15371
15372 #[test]
15373 fn test_collapse_row_iam_users() {
15374 let mut app = test_app();
15375 app.current_service = Service::IamUsers;
15376 app.iam_state.users.expanded_item = Some(1);
15377
15378 app.handle_action(Action::CollapseRow);
15379 assert_eq!(app.iam_state.users.expanded_item, None);
15380 }
15381
15382 #[test]
15383 fn test_collapse_row_does_nothing_when_not_expanded() {
15384 let mut app = test_app();
15385 app.current_service = Service::Ec2Instances;
15386 app.ec2_state.table.expanded_item = None;
15387
15388 app.handle_action(Action::CollapseRow);
15389 assert_eq!(app.ec2_state.table.expanded_item, None);
15390 }
15391
15392 #[test]
15393 fn test_s3_collapse_expanded_folder_moves_to_parent() {
15394 let mut app = test_app();
15395 app.current_service = Service::S3Buckets;
15396 app.service_selected = true;
15397 app.mode = Mode::Normal;
15398
15399 app.s3_state.buckets.items = vec![S3Bucket {
15401 name: "bucket1".to_string(),
15402 region: "us-east-1".to_string(),
15403 creation_date: "2024-01-01T00:00:00Z".to_string(),
15404 }];
15405
15406 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
15408 app.s3_state.bucket_preview.insert(
15409 "bucket1".to_string(),
15410 vec![S3Object {
15411 key: "folder1/".to_string(),
15412 size: 0,
15413 last_modified: "2024-01-01T00:00:00Z".to_string(),
15414 is_prefix: true,
15415 storage_class: String::new(),
15416 }],
15417 );
15418
15419 app.s3_state
15421 .expanded_prefixes
15422 .insert("folder1/".to_string());
15423 app.s3_state.prefix_preview.insert(
15424 "folder1/".to_string(),
15425 vec![S3Object {
15426 key: "folder1/file.txt".to_string(),
15427 size: 0,
15428 last_modified: "2024-01-01T00:00:00Z".to_string(),
15429 is_prefix: false,
15430 storage_class: String::new(),
15431 }],
15432 );
15433
15434 app.s3_state.selected_row = 1;
15436
15437 app.handle_action(Action::PrevPane);
15439
15440 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
15442 assert_eq!(app.s3_state.selected_row, 0);
15444 }
15445
15446 #[test]
15447 fn test_s3_collapse_hierarchy_level_by_level() {
15448 let mut app = test_app();
15449 app.current_service = Service::S3Buckets;
15450 app.service_selected = true;
15451 app.mode = Mode::Normal;
15452
15453 app.s3_state.buckets.items = vec![S3Bucket {
15455 name: "bucket1".to_string(),
15456 region: "us-east-1".to_string(),
15457 creation_date: "2024-01-01T00:00:00Z".to_string(),
15458 }];
15459
15460 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
15462 app.s3_state.bucket_preview.insert(
15463 "bucket1".to_string(),
15464 vec![S3Object {
15465 key: "level1/".to_string(),
15466 size: 0,
15467 last_modified: "2024-01-01T00:00:00Z".to_string(),
15468 is_prefix: true,
15469 storage_class: String::new(),
15470 }],
15471 );
15472
15473 app.s3_state.expanded_prefixes.insert("level1/".to_string());
15475 app.s3_state.prefix_preview.insert(
15476 "level1/".to_string(),
15477 vec![S3Object {
15478 key: "level1/level2/".to_string(),
15479 size: 0,
15480 last_modified: "2024-01-01T00:00:00Z".to_string(),
15481 is_prefix: true,
15482 storage_class: String::new(),
15483 }],
15484 );
15485
15486 app.s3_state
15488 .expanded_prefixes
15489 .insert("level1/level2/".to_string());
15490 app.s3_state.prefix_preview.insert(
15491 "level1/level2/".to_string(),
15492 vec![S3Object {
15493 key: "level1/level2/file.txt".to_string(),
15494 size: 100,
15495 last_modified: "2024-01-01T00:00:00Z".to_string(),
15496 is_prefix: false,
15497 storage_class: String::new(),
15498 }],
15499 );
15500
15501 app.s3_state.selected_row = 3;
15503
15504 app.handle_action(Action::PrevPane);
15506 assert_eq!(app.s3_state.selected_row, 2);
15507
15508 app.handle_action(Action::PrevPane);
15510 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
15511 assert_eq!(app.s3_state.selected_row, 1);
15512
15513 app.handle_action(Action::PrevPane);
15515 assert!(!app.s3_state.expanded_prefixes.contains("level1/"));
15516 assert_eq!(app.s3_state.selected_row, 0);
15517
15518 app.handle_action(Action::PrevPane);
15520 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
15521 assert_eq!(app.s3_state.selected_row, 0);
15522 }
15523
15524 #[test]
15525 fn test_ec2_instance_detail_tabs_no_preferences() {
15526 let mut app = test_app();
15527 app.current_service = Service::Ec2Instances;
15528 app.service_selected = true;
15529 app.ec2_state.table.expanded_item = Some(0);
15530 app.mode = Mode::Normal;
15531
15532 app.ec2_state.detail_tab = Ec2DetailTab::Details;
15534 app.handle_action(Action::OpenColumnSelector);
15535 assert_eq!(app.mode, Mode::Normal);
15536
15537 app.ec2_state.detail_tab = Ec2DetailTab::StatusAndAlarms;
15539 app.handle_action(Action::OpenColumnSelector);
15540 assert_eq!(app.mode, Mode::Normal);
15541
15542 app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
15544 app.handle_action(Action::OpenColumnSelector);
15545 assert_eq!(app.mode, Mode::Normal);
15546
15547 app.ec2_state.detail_tab = Ec2DetailTab::Security;
15549 app.handle_action(Action::OpenColumnSelector);
15550 assert_eq!(app.mode, Mode::Normal);
15551
15552 app.ec2_state.detail_tab = Ec2DetailTab::Networking;
15554 app.handle_action(Action::OpenColumnSelector);
15555 assert_eq!(app.mode, Mode::Normal);
15556
15557 app.ec2_state.detail_tab = Ec2DetailTab::Storage;
15559 app.handle_action(Action::OpenColumnSelector);
15560 assert_eq!(app.mode, Mode::Normal);
15561
15562 app.ec2_state.detail_tab = Ec2DetailTab::Tags;
15564 app.handle_action(Action::OpenColumnSelector);
15565 assert_eq!(app.mode, Mode::ColumnSelector);
15566 }
15567
15568 #[test]
15569 fn test_ec2_monitoring_ctrl_u_scrolls_up() {
15570 let mut app = test_app();
15574 app.current_service = Service::Ec2Instances;
15575 app.service_selected = true;
15576 app.mode = Mode::Normal;
15577 app.ec2_state.current_instance = Some("i-abc".to_string());
15578 app.ec2_state.detail_tab = Ec2DetailTab::Monitoring;
15579
15580 app.ec2_state.set_monitoring_scroll(3);
15582 assert_eq!(app.ec2_state.monitoring_scroll(), 3);
15583
15584 app.handle_action(Action::PageUp);
15586 assert_eq!(
15587 app.ec2_state.monitoring_scroll(),
15588 2,
15589 "Ctrl+U must scroll monitoring up by 1"
15590 );
15591
15592 app.handle_action(Action::PageDown);
15594 assert_eq!(
15595 app.ec2_state.monitoring_scroll(),
15596 3,
15597 "Ctrl+D must scroll monitoring down by 1"
15598 );
15599 }
15600
15601 #[test]
15602 fn test_ec2_instance_always_shows_when_state_filter_is_all_states() {
15603 use crate::app::Ec2Instance;
15606 use crate::ui::ec2::{filtered_ec2_instances, StateFilter};
15607 let mut app = test_app();
15608 app.current_service = Service::Ec2Instances;
15609 app.service_selected = true;
15610 app.ec2_state.table.filter.clear();
15611 app.ec2_state.state_filter = StateFilter::AllStates;
15612
15613 app.ec2_state.table.items = vec![Ec2Instance {
15615 instance_id: "i-05f181d4aaabadf5a".to_string(),
15616 state: "running".to_string(),
15617 ..Default::default()
15618 }];
15619
15620 let filtered = filtered_ec2_instances(&app);
15621 assert_eq!(
15622 filtered.len(),
15623 1,
15624 "Instance must appear with AllStates filter and no text filter"
15625 );
15626 assert_eq!(filtered[0].instance_id, "i-05f181d4aaabadf5a");
15627 }
15628
15629 #[test]
15630 fn test_ec2_instance_private_ip_populated_from_load() {
15631 use crate::app::Ec2Instance;
15635 let instance = Ec2Instance {
15636 instance_id: "i-abc".to_string(),
15637 name: "my-server".to_string(),
15638 state: "running".to_string(),
15639 private_dns_name: "ip-10-0-1-5.ec2.internal".to_string(),
15640 private_ip_address: "10.0.1.5".to_string(),
15641 security_group_ids: "sg-abc123".to_string(),
15642 ..Default::default()
15643 };
15644 assert!(
15646 !instance.private_ip_address.is_empty(),
15647 "private_ip_address must be populated"
15648 );
15649 assert!(
15650 !instance.private_dns_name.is_empty(),
15651 "private_dns_name must be populated"
15652 );
15653 }
15654
15655 #[test]
15656 fn test_log_streams_filter_only_updates_when_focused() {
15657 let mut app = test_app();
15658 app.current_service = Service::CloudWatchLogGroups;
15659 app.service_selected = true;
15660 app.view_mode = ViewMode::Detail;
15661 app.mode = Mode::FilterInput;
15662 app.log_groups_state.stream_filter = "test".to_string();
15663
15664 app.log_groups_state.input_focus = InputFocus::Filter;
15666 app.handle_action(Action::FilterInput('x'));
15667 assert_eq!(app.log_groups_state.stream_filter, "testx");
15668
15669 app.log_groups_state.input_focus = InputFocus::Pagination;
15671 app.handle_action(Action::FilterInput('y'));
15672 assert_eq!(app.log_groups_state.stream_filter, "testx"); }
15674
15675 #[test]
15676 fn test_log_streams_backspace_only_updates_when_focused() {
15677 let mut app = test_app();
15678 app.current_service = Service::CloudWatchLogGroups;
15679 app.service_selected = true;
15680 app.view_mode = ViewMode::Detail;
15681 app.mode = Mode::FilterInput;
15682 app.log_groups_state.stream_filter = "test".to_string();
15683
15684 app.log_groups_state.input_focus = InputFocus::Filter;
15686 app.handle_action(Action::FilterBackspace);
15687 assert_eq!(app.log_groups_state.stream_filter, "tes");
15688
15689 app.log_groups_state.input_focus = InputFocus::Pagination;
15691 app.handle_action(Action::FilterBackspace);
15692 assert_eq!(app.log_groups_state.stream_filter, "tes"); }
15694
15695 #[test]
15696 fn test_log_groups_filter_only_updates_when_focused() {
15697 let mut app = test_app();
15698 app.current_service = Service::CloudWatchLogGroups;
15699 app.service_selected = true;
15700 app.view_mode = ViewMode::List;
15701 app.mode = Mode::FilterInput;
15702 app.log_groups_state.log_groups.filter = "test".to_string();
15703
15704 app.log_groups_state.input_focus = InputFocus::Filter;
15706 app.handle_action(Action::FilterInput('x'));
15707 assert_eq!(app.log_groups_state.log_groups.filter, "testx");
15708
15709 app.log_groups_state.input_focus = InputFocus::Pagination;
15711 app.handle_action(Action::FilterInput('y'));
15712 assert_eq!(app.log_groups_state.log_groups.filter, "testx"); }
15714
15715 #[test]
15716 fn test_s3_bucket_collapse_nested_prefix_jumps_to_parent() {
15717 use S3Bucket;
15718 use S3Object;
15719
15720 let mut app = test_app();
15721 app.current_service = Service::S3Buckets;
15722 app.service_selected = true;
15723
15724 app.s3_state.buckets.items = vec![S3Bucket {
15726 name: "test-bucket".to_string(),
15727 region: "us-east-1".to_string(),
15728 creation_date: String::new(),
15729 }];
15730
15731 app.s3_state
15733 .expanded_prefixes
15734 .insert("test-bucket".to_string());
15735 app.s3_state.bucket_preview.insert(
15736 "test-bucket".to_string(),
15737 vec![S3Object {
15738 key: "folder1/".to_string(),
15739 is_prefix: true,
15740 size: 0,
15741 last_modified: String::new(),
15742 storage_class: String::new(),
15743 }],
15744 );
15745
15746 app.s3_state
15748 .expanded_prefixes
15749 .insert("folder1/".to_string());
15750 app.s3_state.prefix_preview.insert(
15751 "folder1/".to_string(),
15752 vec![S3Object {
15753 key: "folder1/folder2/".to_string(),
15754 is_prefix: true,
15755 size: 0,
15756 last_modified: String::new(),
15757 storage_class: String::new(),
15758 }],
15759 );
15760
15761 app.s3_state.selected_row = 2;
15763
15764 app.handle_action(Action::CollapseRow);
15766
15767 assert!(!app.s3_state.expanded_prefixes.contains("folder1/folder2/"));
15769 assert_eq!(app.s3_state.selected_row, 1);
15771 }
15772
15773 #[test]
15774 fn test_s3_bucket_collapse_expanded_folder_moves_to_parent() {
15775 use S3Bucket;
15776 use S3Object;
15777
15778 let mut app = test_app();
15779 app.current_service = Service::S3Buckets;
15780 app.service_selected = true;
15781
15782 app.s3_state.buckets.items = vec![S3Bucket {
15784 name: "test-bucket".to_string(),
15785 region: "us-east-1".to_string(),
15786 creation_date: String::new(),
15787 }];
15788
15789 app.s3_state
15791 .expanded_prefixes
15792 .insert("test-bucket".to_string());
15793 app.s3_state.bucket_preview.insert(
15794 "test-bucket".to_string(),
15795 vec![S3Object {
15796 key: "folder1/".to_string(),
15797 is_prefix: true,
15798 size: 0,
15799 last_modified: String::new(),
15800 storage_class: String::new(),
15801 }],
15802 );
15803
15804 app.s3_state
15806 .expanded_prefixes
15807 .insert("folder1/".to_string());
15808 app.s3_state.prefix_preview.insert(
15809 "folder1/".to_string(),
15810 vec![S3Object {
15811 key: "folder1/file.txt".to_string(),
15812 is_prefix: false,
15813 size: 100,
15814 last_modified: String::new(),
15815 storage_class: String::new(),
15816 }],
15817 );
15818
15819 app.s3_state.selected_row = 1;
15821
15822 app.handle_action(Action::CollapseRow);
15824
15825 assert!(!app.s3_state.expanded_prefixes.contains("folder1/"));
15827 assert_eq!(app.s3_state.selected_row, 0);
15829 }
15830
15831 #[test]
15832 fn test_log_streams_pagination_limits_table_content() {
15833 let mut app = test_app();
15834 app.current_service = Service::CloudWatchLogGroups;
15835 app.service_selected = true;
15836 app.view_mode = ViewMode::Detail;
15837
15838 app.log_groups_state.log_streams = (0..50)
15840 .map(|i| rusticity_core::LogStream {
15841 name: format!("stream-{}", i),
15842 creation_time: None,
15843 last_event_time: None,
15844 })
15845 .collect();
15846
15847 app.log_groups_state.stream_page_size = 10;
15849 app.log_groups_state.stream_current_page = 0;
15850
15851 assert_eq!(app.log_groups_state.stream_page_size, 10);
15854 assert_eq!(app.log_groups_state.stream_current_page, 0);
15855
15856 app.log_groups_state.stream_current_page = 1;
15858 assert_eq!(app.log_groups_state.stream_current_page, 1);
15859 }
15860
15861 #[test]
15862 fn test_log_streams_page_size_change_resets_page() {
15863 let mut app = test_app();
15864 app.current_service = Service::CloudWatchLogGroups;
15865 app.service_selected = true;
15866 app.view_mode = ViewMode::Detail;
15867 app.mode = Mode::ColumnSelector;
15868
15869 app.log_groups_state.stream_page_size = 10;
15870 app.log_groups_state.stream_current_page = 3;
15871
15872 app.column_selector_index = app.cw_log_stream_column_ids.len() + 4; app.handle_action(Action::ToggleColumn);
15875
15876 assert_eq!(app.log_groups_state.stream_page_size, 25);
15877 assert_eq!(app.log_groups_state.stream_current_page, 0);
15878 }
15879
15880 #[test]
15881 fn test_s3_objects_expanded_rows_stay_visible() {
15882 use S3Object;
15883
15884 let mut app = test_app();
15885 app.current_service = Service::S3Buckets;
15886 app.service_selected = true;
15887 app.mode = Mode::Normal;
15888 app.s3_state.current_bucket = Some("test-bucket".to_string());
15889
15890 app.s3_state.objects = vec![S3Object {
15892 key: "folder1/".to_string(),
15893 is_prefix: true,
15894 size: 0,
15895 last_modified: String::new(),
15896 storage_class: String::new(),
15897 }];
15898
15899 app.s3_state
15901 .expanded_prefixes
15902 .insert("folder1/".to_string());
15903 app.s3_state.prefix_preview.insert(
15904 "folder1/".to_string(),
15905 (0..20)
15906 .map(|i| S3Object {
15907 key: format!("folder1/file{}.txt", i),
15908 is_prefix: false,
15909 size: 100,
15910 last_modified: String::new(),
15911 storage_class: String::new(),
15912 })
15913 .collect(),
15914 );
15915
15916 app.s3_state.object_visible_rows.set(10);
15918 app.s3_state.object_scroll_offset = 0;
15919 app.s3_state.selected_object = 0; for i in 1..=20 {
15923 app.handle_action(Action::NextItem);
15924 assert_eq!(app.s3_state.selected_object, i);
15925
15926 let visible_start = app.s3_state.object_scroll_offset;
15928 let visible_end = visible_start + app.s3_state.object_visible_rows.get();
15929 assert!(
15930 app.s3_state.selected_object >= visible_start
15931 && app.s3_state.selected_object < visible_end,
15932 "Selection {} should be visible in range [{}, {})",
15933 app.s3_state.selected_object,
15934 visible_start,
15935 visible_end
15936 );
15937 }
15938 }
15939
15940 #[test]
15941 fn test_s3_bucket_error_rows_counted_in_total() {
15942 use S3Bucket;
15943
15944 let mut app = test_app();
15945 app.current_service = Service::S3Buckets;
15946 app.service_selected = true;
15947
15948 app.s3_state.buckets.items = vec![
15950 S3Bucket {
15951 name: "bucket1".to_string(),
15952 region: "us-east-1".to_string(),
15953 creation_date: String::new(),
15954 },
15955 S3Bucket {
15956 name: "bucket2".to_string(),
15957 region: "us-east-1".to_string(),
15958 creation_date: String::new(),
15959 },
15960 ];
15961
15962 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
15964 let long_error = "service error: unhandled error (PermanentRedirect): Error { code: PermanentRedirect, message: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint., request_id: 6D5VJ9TXYEMXSMXG, s3_extended_request_id: CGSwddO9ummjFYFHKyqNEU= }".to_string();
15965 app.s3_state
15966 .bucket_errors
15967 .insert("bucket1".to_string(), long_error.clone());
15968
15969 let total = app.calculate_total_bucket_rows();
15971
15972 let error_rows = long_error.len().div_ceil(120);
15974 assert_eq!(total, 2 + error_rows);
15975 }
15976
15977 #[test]
15978 fn test_s3_bucket_with_error_can_be_collapsed() {
15979 use S3Bucket;
15980
15981 let mut app = test_app();
15982 app.current_service = Service::S3Buckets;
15983 app.service_selected = true;
15984 app.mode = Mode::Normal;
15985
15986 app.s3_state.buckets.items = vec![S3Bucket {
15988 name: "bucket1".to_string(),
15989 region: "us-east-1".to_string(),
15990 creation_date: String::new(),
15991 }];
15992
15993 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
15995 let error = "service error: PermanentRedirect".to_string();
15996 app.s3_state
15997 .bucket_errors
15998 .insert("bucket1".to_string(), error);
15999
16000 app.s3_state.selected_row = 0;
16002
16003 app.handle_action(Action::CollapseRow);
16005
16006 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
16008 assert_eq!(app.s3_state.selected_row, 0);
16010 }
16011
16012 #[test]
16013 fn test_s3_bucket_collapse_on_bucket_row() {
16014 use S3Bucket;
16015
16016 let mut app = test_app();
16017 app.current_service = Service::S3Buckets;
16018 app.service_selected = true;
16019 app.mode = Mode::Normal;
16020
16021 app.s3_state.buckets.items = vec![S3Bucket {
16023 name: "bucket1".to_string(),
16024 region: "us-east-1".to_string(),
16025 creation_date: String::new(),
16026 }];
16027
16028 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16030 let error = "service error: PermanentRedirect".to_string();
16031 app.s3_state
16032 .bucket_errors
16033 .insert("bucket1".to_string(), error);
16034
16035 app.s3_state.selected_row = 0;
16037
16038 app.handle_action(Action::CollapseRow);
16040
16041 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
16043 assert_eq!(app.s3_state.selected_row, 0);
16045 }
16046
16047 #[test]
16048 fn test_s3_bucket_collapse_adjusts_scroll_offset() {
16049 use S3Bucket;
16050
16051 let mut app = test_app();
16052 app.current_service = Service::S3Buckets;
16053 app.service_selected = true;
16054 app.mode = Mode::Normal;
16055
16056 app.s3_state.buckets.items = (0..20)
16058 .map(|i| S3Bucket {
16059 name: format!("bucket{}", i),
16060 region: "us-east-1".to_string(),
16061 creation_date: String::new(),
16062 })
16063 .collect();
16064
16065 app.s3_state
16067 .expanded_prefixes
16068 .insert("bucket10".to_string());
16069 let long_error = "service error: unhandled error (PermanentRedirect): Error { code: PermanentRedirect, message: The bucket you are attempting to access must be addressed using the specified endpoint. Please send all future requests to this endpoint., request_id: 6D5VJ9TXYEMXSMXG }".to_string();
16070 app.s3_state
16071 .bucket_errors
16072 .insert("bucket10".to_string(), long_error.clone());
16073
16074 app.s3_state.bucket_visible_rows.set(10);
16076 app.s3_state.bucket_scroll_offset = 10; app.s3_state.selected_row = 10;
16080
16081 app.handle_action(Action::CollapseRow);
16083
16084 assert!(!app.s3_state.expanded_prefixes.contains("bucket10"));
16086 assert_eq!(app.s3_state.selected_row, 10);
16088 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
16090 assert!(
16091 app.s3_state.selected_row
16092 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
16093 );
16094 }
16095
16096 #[test]
16097 fn test_s3_collapse_second_to_last_bucket_with_last_having_error() {
16098 use S3Bucket;
16099
16100 let mut app = test_app();
16101 app.current_service = Service::S3Buckets;
16102 app.service_selected = true;
16103 app.mode = Mode::Normal;
16104
16105 app.s3_state.buckets.items = vec![
16107 S3Bucket {
16108 name: "bucket1".to_string(),
16109 region: "us-east-1".to_string(),
16110 creation_date: String::new(),
16111 },
16112 S3Bucket {
16113 name: "bucket2".to_string(),
16114 region: "us-east-1".to_string(),
16115 creation_date: String::new(),
16116 },
16117 S3Bucket {
16118 name: "bucket3".to_string(),
16119 region: "us-east-1".to_string(),
16120 creation_date: String::new(),
16121 },
16122 ];
16123
16124 app.s3_state.expanded_prefixes.insert("bucket2".to_string());
16126 app.s3_state.bucket_preview.insert(
16127 "bucket2".to_string(),
16128 vec![
16129 S3Object {
16130 key: "folder1/".to_string(),
16131 is_prefix: true,
16132 size: 0,
16133 last_modified: String::new(),
16134 storage_class: String::new(),
16135 },
16136 S3Object {
16137 key: "file1.txt".to_string(),
16138 is_prefix: false,
16139 size: 100,
16140 last_modified: String::new(),
16141 storage_class: String::new(),
16142 },
16143 ],
16144 );
16145
16146 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
16148 let error = "service error: PermanentRedirect".to_string();
16149 app.s3_state
16150 .bucket_errors
16151 .insert("bucket3".to_string(), error);
16152
16153 app.s3_state.bucket_visible_rows.set(10);
16155 app.s3_state.bucket_scroll_offset = 0;
16156
16157 app.s3_state.selected_row = 3;
16159
16160 app.handle_action(Action::CollapseRow);
16162
16163 assert!(app.s3_state.expanded_prefixes.contains("bucket2"));
16165 assert_eq!(app.s3_state.selected_row, 1);
16167 assert!(app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset);
16169 assert!(
16170 app.s3_state.selected_row
16171 < app.s3_state.bucket_scroll_offset + app.s3_state.bucket_visible_rows.get()
16172 );
16173 }
16174
16175 #[test]
16176 fn test_s3_collapse_bucket_with_error() {
16177 use S3Bucket;
16178
16179 let mut app = test_app();
16180 app.current_service = Service::S3Buckets;
16181 app.service_selected = true;
16182 app.mode = Mode::Normal;
16183
16184 app.s3_state.buckets.items = vec![
16185 S3Bucket {
16186 name: "bucket1".to_string(),
16187 region: "us-east-1".to_string(),
16188 creation_date: String::new(),
16189 },
16190 S3Bucket {
16191 name: "bucket2".to_string(),
16192 region: "us-east-1".to_string(),
16193 creation_date: String::new(),
16194 },
16195 ];
16196
16197 let error = "service error: unhandled error (PermanentRedirect)".to_string();
16198 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16199 app.s3_state
16200 .bucket_errors
16201 .insert("bucket1".to_string(), error);
16202
16203 app.s3_state.bucket_visible_rows.set(10);
16204 app.s3_state.bucket_scroll_offset = 0;
16205
16206 app.s3_state.selected_row = 0;
16208
16209 app.handle_action(Action::CollapseRow);
16211
16212 assert!(!app.s3_state.expanded_prefixes.contains("bucket1"));
16214 assert_eq!(app.s3_state.selected_row, 0);
16215 }
16216
16217 #[test]
16218 fn test_s3_collapse_row_with_multiple_error_buckets() {
16219 use S3Bucket;
16220
16221 let mut app = test_app();
16222 app.current_service = Service::S3Buckets;
16223 app.service_selected = true;
16224 app.mode = Mode::Normal;
16225
16226 app.s3_state.buckets.items = vec![
16227 S3Bucket {
16228 name: "bucket1".to_string(),
16229 region: "us-east-1".to_string(),
16230 creation_date: String::new(),
16231 },
16232 S3Bucket {
16233 name: "bucket2".to_string(),
16234 region: "us-east-1".to_string(),
16235 creation_date: String::new(),
16236 },
16237 S3Bucket {
16238 name: "bucket3".to_string(),
16239 region: "us-east-1".to_string(),
16240 creation_date: String::new(),
16241 },
16242 ];
16243
16244 let error = "service error: unhandled error (PermanentRedirect)".to_string();
16245
16246 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16248 app.s3_state
16249 .bucket_errors
16250 .insert("bucket1".to_string(), error.clone());
16251
16252 app.s3_state.expanded_prefixes.insert("bucket3".to_string());
16254 app.s3_state
16255 .bucket_errors
16256 .insert("bucket3".to_string(), error.clone());
16257
16258 app.s3_state.bucket_visible_rows.set(30);
16259 app.s3_state.bucket_scroll_offset = 0;
16260
16261 app.s3_state.selected_row = 2;
16266
16267 app.handle_action(Action::CollapseRow);
16268
16269 assert!(
16271 !app.s3_state.expanded_prefixes.contains("bucket3"),
16272 "bucket3 should be collapsed"
16273 );
16274 assert!(
16275 app.s3_state.expanded_prefixes.contains("bucket1"),
16276 "bucket1 should still be expanded"
16277 );
16278 assert_eq!(app.s3_state.selected_row, 2);
16279 }
16280
16281 #[test]
16282 fn test_s3_collapse_row_nested_only_collapses_one_level() {
16283 use S3Bucket;
16284
16285 let mut app = test_app();
16286 app.current_service = Service::S3Buckets;
16287 app.service_selected = true;
16288 app.mode = Mode::Normal;
16289
16290 app.s3_state.buckets.items = vec![S3Bucket {
16291 name: "bucket1".to_string(),
16292 region: "us-east-1".to_string(),
16293 creation_date: String::new(),
16294 }];
16295
16296 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16298 app.s3_state.bucket_preview.insert(
16299 "bucket1".to_string(),
16300 vec![S3Object {
16301 key: "level1/".to_string(),
16302 size: 0,
16303 last_modified: "2024-01-01T00:00:00Z".to_string(),
16304 is_prefix: true,
16305 storage_class: String::new(),
16306 }],
16307 );
16308
16309 app.s3_state.expanded_prefixes.insert("level1/".to_string());
16311 app.s3_state.prefix_preview.insert(
16312 "level1/".to_string(),
16313 vec![S3Object {
16314 key: "level1/level2/".to_string(),
16315 size: 0,
16316 last_modified: "2024-01-01T00:00:00Z".to_string(),
16317 is_prefix: true,
16318 storage_class: String::new(),
16319 }],
16320 );
16321
16322 app.s3_state
16324 .expanded_prefixes
16325 .insert("level1/level2/".to_string());
16326 app.s3_state.prefix_preview.insert(
16327 "level1/level2/".to_string(),
16328 vec![S3Object {
16329 key: "level1/level2/file.txt".to_string(),
16330 size: 100,
16331 last_modified: "2024-01-01T00:00:00Z".to_string(),
16332 is_prefix: false,
16333 storage_class: String::new(),
16334 }],
16335 );
16336
16337 app.s3_state.bucket_visible_rows.set(10);
16338
16339 app.s3_state.selected_row = 2;
16341
16342 app.handle_action(Action::CollapseRow);
16344
16345 assert!(!app.s3_state.expanded_prefixes.contains("level1/level2/"));
16347 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
16349 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
16351 assert_eq!(app.s3_state.selected_row, 1);
16353 }
16354
16355 #[test]
16356 fn test_s3_collapse_row_deeply_nested_file() {
16357 use S3Bucket;
16358
16359 let mut app = test_app();
16360 app.current_service = Service::S3Buckets;
16361 app.service_selected = true;
16362 app.mode = Mode::Normal;
16363
16364 app.s3_state.buckets.items = vec![S3Bucket {
16365 name: "bucket1".to_string(),
16366 region: "us-east-1".to_string(),
16367 creation_date: String::new(),
16368 }];
16369
16370 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
16372 app.s3_state.bucket_preview.insert(
16373 "bucket1".to_string(),
16374 vec![S3Object {
16375 key: "level1/".to_string(),
16376 size: 0,
16377 last_modified: "2024-01-01T00:00:00Z".to_string(),
16378 is_prefix: true,
16379 storage_class: String::new(),
16380 }],
16381 );
16382
16383 app.s3_state.expanded_prefixes.insert("level1/".to_string());
16385 app.s3_state.prefix_preview.insert(
16386 "level1/".to_string(),
16387 vec![S3Object {
16388 key: "level1/level2/".to_string(),
16389 size: 0,
16390 last_modified: "2024-01-01T00:00:00Z".to_string(),
16391 is_prefix: true,
16392 storage_class: String::new(),
16393 }],
16394 );
16395
16396 app.s3_state
16398 .expanded_prefixes
16399 .insert("level1/level2/".to_string());
16400 app.s3_state.prefix_preview.insert(
16401 "level1/level2/".to_string(),
16402 vec![S3Object {
16403 key: "level1/level2/file.txt".to_string(),
16404 size: 100,
16405 last_modified: "2024-01-01T00:00:00Z".to_string(),
16406 is_prefix: false,
16407 storage_class: String::new(),
16408 }],
16409 );
16410
16411 app.s3_state.bucket_visible_rows.set(10);
16412
16413 app.s3_state.selected_row = 3;
16415
16416 app.handle_action(Action::CollapseRow);
16418
16419 assert!(app.s3_state.expanded_prefixes.contains("level1/level2/"));
16421 assert!(app.s3_state.expanded_prefixes.contains("level1/"));
16422 assert!(app.s3_state.expanded_prefixes.contains("bucket1"));
16423 assert_eq!(app.s3_state.selected_row, 2);
16425 }
16426
16427 #[test]
16428 fn test_s3_bucket_pagination_adjusts_scroll() {
16429 use S3Bucket;
16430
16431 let mut app = test_app();
16432 app.current_service = Service::S3Buckets;
16433 app.service_selected = true;
16434 app.mode = Mode::Normal;
16435
16436 app.s3_state.buckets.items = (0..150)
16438 .map(|i| S3Bucket {
16439 name: format!("bucket{:03}", i),
16440 region: "us-east-1".to_string(),
16441 creation_date: String::new(),
16442 })
16443 .collect();
16444
16445 app.s3_state.bucket_visible_rows.set(20);
16446 app.s3_state.selected_row = 0;
16447 app.s3_state.bucket_scroll_offset = 0;
16448
16449 app.go_to_page(2);
16451
16452 assert_eq!(app.s3_state.selected_row, 50);
16453 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
16455
16456 app.go_to_page(3);
16458
16459 assert_eq!(app.s3_state.selected_row, 100);
16460 assert_eq!(app.s3_state.bucket_scroll_offset, 100);
16461
16462 app.go_to_page(1);
16464
16465 assert_eq!(app.s3_state.selected_row, 0);
16466 assert_eq!(app.s3_state.bucket_scroll_offset, 0);
16467 }
16468
16469 #[test]
16470 fn test_s3_bucket_pagination_uses_page_size() {
16471 use S3Bucket;
16472
16473 let mut app = test_app();
16474 app.current_service = Service::S3Buckets;
16475 app.service_selected = true;
16476 app.mode = Mode::Normal;
16477
16478 app.s3_state.buckets.items = (0..100)
16480 .map(|i| S3Bucket {
16481 name: format!("bucket{:03}", i),
16482 region: "us-east-1".to_string(),
16483 creation_date: String::new(),
16484 })
16485 .collect();
16486
16487 app.s3_state.bucket_visible_rows.set(20);
16488 app.s3_state.selected_row = 0;
16489
16490 assert_eq!(app.s3_state.buckets.page_size.value(), 50);
16492
16493 app.go_to_page(2);
16495 assert_eq!(app.s3_state.selected_row, 50);
16496 assert_eq!(app.s3_state.bucket_scroll_offset, 50);
16497
16498 app.s3_state.buckets.page_size = crate::common::PageSize::TwentyFive;
16500 assert_eq!(app.s3_state.buckets.page_size.value(), 25);
16501
16502 app.go_to_page(2);
16504 assert_eq!(app.s3_state.selected_row, 25);
16505 assert_eq!(app.s3_state.bucket_scroll_offset, 25);
16506 }
16507
16508 #[test]
16509 fn test_s3_bucket_page_size_limits_visible_rows() {
16510 use S3Bucket;
16511
16512 let mut app = test_app();
16513 app.current_service = Service::S3Buckets;
16514 app.service_selected = true;
16515 app.mode = Mode::Normal;
16516
16517 app.s3_state.buckets.items = (0..100)
16519 .map(|i| S3Bucket {
16520 name: format!("bucket{:03}", i),
16521 region: "us-east-1".to_string(),
16522 creation_date: String::new(),
16523 })
16524 .collect();
16525
16526 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16528 assert_eq!(app.s3_state.buckets.page_size.value(), 10);
16529
16530 let total_rows = app.calculate_total_bucket_rows();
16532 assert!(total_rows >= 10, "Should have at least 10 rows");
16536 }
16537
16538 #[test]
16539 fn test_s3_bucket_tab_cycling_in_filter() {
16540 use crate::common::InputFocus;
16541
16542 let mut app = test_app();
16543 app.current_service = Service::S3Buckets;
16544 app.mode = Mode::FilterInput;
16545
16546 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
16548
16549 app.handle_action(Action::NextFilterFocus);
16551 assert_eq!(app.s3_state.input_focus, InputFocus::Pagination);
16552
16553 app.handle_action(Action::NextFilterFocus);
16555 assert_eq!(app.s3_state.input_focus, InputFocus::Filter);
16556 }
16557
16558 #[test]
16559 fn test_s3_bucket_pagination_navigation_with_arrows() {
16560 use S3Bucket;
16561
16562 let mut app = test_app();
16563 app.current_service = Service::S3Buckets;
16564 app.mode = Mode::FilterInput;
16565
16566 app.s3_state.buckets.items = (0..100)
16568 .map(|i| S3Bucket {
16569 name: format!("bucket{:03}", i),
16570 region: "us-east-1".to_string(),
16571 creation_date: String::new(),
16572 })
16573 .collect();
16574
16575 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16576 app.s3_state.selected_row = 0;
16577
16578 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
16580
16581 app.handle_action(Action::NextItem);
16583 assert_eq!(app.s3_state.selected_row, 10);
16584
16585 app.handle_action(Action::NextItem);
16587 assert_eq!(app.s3_state.selected_row, 20);
16588
16589 app.handle_action(Action::PrevItem);
16591 assert_eq!(app.s3_state.selected_row, 10);
16592 }
16593
16594 #[test]
16595 fn test_s3_bucket_go_to_page_shows_correct_buckets() {
16596 use S3Bucket;
16597
16598 let mut app = test_app();
16599 app.current_service = Service::S3Buckets;
16600 app.service_selected = true;
16601 app.mode = Mode::Normal;
16602
16603 app.s3_state.buckets.items = (0..100)
16605 .map(|i| S3Bucket {
16606 name: format!("bucket{:03}", i),
16607 region: "us-east-1".to_string(),
16608 creation_date: String::new(),
16609 })
16610 .collect();
16611
16612 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16613
16614 app.go_to_page(2);
16616 assert_eq!(app.s3_state.selected_row, 10);
16617
16618 app.go_to_page(5);
16620 assert_eq!(app.s3_state.selected_row, 40);
16621 }
16622
16623 #[test]
16624 fn test_s3_bucket_left_right_arrows_change_pages() {
16625 use S3Bucket;
16626
16627 let mut app = test_app();
16628 app.current_service = Service::S3Buckets;
16629 app.mode = Mode::FilterInput;
16630
16631 app.s3_state.buckets.items = (0..100)
16633 .map(|i| S3Bucket {
16634 name: format!("bucket{:03}", i),
16635 region: "us-east-1".to_string(),
16636 creation_date: String::new(),
16637 })
16638 .collect();
16639
16640 app.s3_state.buckets.page_size = crate::common::PageSize::Ten;
16641 app.s3_state.selected_row = 0;
16642 app.s3_state.input_focus = crate::common::InputFocus::Pagination;
16643
16644 app.handle_action(Action::PageDown);
16646 assert_eq!(app.s3_state.selected_row, 10);
16647
16648 app.handle_action(Action::PageDown);
16650 assert_eq!(app.s3_state.selected_row, 20);
16651
16652 app.handle_action(Action::PageUp);
16654 assert_eq!(app.s3_state.selected_row, 10);
16655 }
16656
16657 #[test]
16658 fn test_s3_bucket_preview_uses_bucket_region_not_config_region() {
16659 use S3Bucket;
16667 let mut app = test_app();
16668 app.config.region = "ap-southeast-3".to_string(); app.s3_state.buckets.items = vec![S3Bucket {
16672 name: "my-bucket".to_string(),
16673 region: "us-east-1".to_string(), creation_date: String::new(),
16675 }];
16676
16677 let bucket = app
16679 .s3_state
16680 .buckets
16681 .items
16682 .iter()
16683 .find(|b| b.name == "my-bucket")
16684 .unwrap();
16685 let effective_region = if bucket.region.is_empty() {
16686 app.config.region.as_str()
16687 } else {
16688 bucket.region.as_str()
16689 };
16690 assert_eq!(
16691 effective_region, "us-east-1",
16692 "load_bucket_preview must use bucket's region, not app config region"
16693 );
16694 assert_ne!(
16695 effective_region, "ap-southeast-3",
16696 "Must NOT fall back to configured region for cross-region buckets"
16697 );
16698 }
16699
16700 #[test]
16701 fn test_s3_scroll_follows_selection_after_bucket_expand() {
16702 use S3Bucket;
16703 use S3Object;
16704 let mut app = test_app();
16707 app.current_service = Service::S3Buckets;
16708 app.service_selected = true;
16709 app.mode = Mode::Normal;
16710
16711 app.s3_state.buckets.items = (0..5)
16713 .map(|i| S3Bucket {
16714 name: format!("bucket{}", i),
16715 region: "us-east-1".to_string(),
16716 creation_date: String::new(),
16717 })
16718 .collect();
16719
16720 app.s3_state.bucket_visible_rows = std::cell::Cell::new(3);
16721
16722 app.s3_state.selected_row = 2;
16724 app.s3_state.bucket_scroll_offset = 0;
16725
16726 app.s3_state.bucket_preview.insert(
16728 "bucket2".to_string(),
16729 vec![
16730 S3Object {
16731 key: "file1.txt".to_string(),
16732 is_prefix: false,
16733 size: 100,
16734 last_modified: String::new(),
16735 storage_class: String::new(),
16736 },
16737 S3Object {
16738 key: "file2.txt".to_string(),
16739 is_prefix: false,
16740 size: 200,
16741 last_modified: String::new(),
16742 storage_class: String::new(),
16743 },
16744 ],
16745 );
16746
16747 app.handle_action(Action::CollapseRow); app.handle_action(Action::NextPane); let visible = app.s3_state.bucket_visible_rows.get();
16755 let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
16756 && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
16757 assert!(
16758 in_view,
16759 "selected_row {} must be visible in [{}, {})",
16760 app.s3_state.selected_row,
16761 app.s3_state.bucket_scroll_offset,
16762 app.s3_state.bucket_scroll_offset + visible
16763 );
16764 }
16765
16766 #[test]
16767 fn test_s3_expand_without_preview_does_not_move_selection() {
16768 use S3Bucket;
16772 let mut app = test_app();
16773 app.current_service = Service::S3Buckets;
16774 app.service_selected = true;
16775 app.mode = Mode::Normal;
16776
16777 app.s3_state.buckets.items = vec![
16778 S3Bucket {
16779 name: "bucket0".to_string(),
16780 region: "us-east-1".to_string(),
16781 creation_date: String::new(),
16782 },
16783 S3Bucket {
16784 name: "bucket1".to_string(),
16785 region: "us-east-1".to_string(),
16786 creation_date: String::new(),
16787 },
16788 ];
16789 app.s3_state.selected_row = 0; app.handle_action(Action::NextPane); assert_eq!(
16796 app.s3_state.selected_row, 0,
16797 "Selection must not move before preview is loaded"
16798 );
16799 assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
16801 assert!(app.s3_state.buckets.loading);
16802 }
16803
16804 #[test]
16805 fn test_s3_expand_with_preview_moves_to_first_child() {
16806 use crate::s3::Object as S3Object;
16808 use S3Bucket;
16809 let mut app = test_app();
16810 app.current_service = Service::S3Buckets;
16811 app.service_selected = true;
16812 app.mode = Mode::Normal;
16813
16814 app.s3_state.buckets.items = vec![
16815 S3Bucket {
16816 name: "bucket0".to_string(),
16817 region: "us-east-1".to_string(),
16818 creation_date: String::new(),
16819 },
16820 S3Bucket {
16821 name: "bucket1".to_string(),
16822 region: "us-east-1".to_string(),
16823 creation_date: String::new(),
16824 },
16825 ];
16826 app.s3_state.selected_row = 0;
16827 app.s3_state.bucket_preview.insert(
16828 "bucket0".to_string(),
16829 vec![S3Object {
16830 key: "file.txt".to_string(),
16831 is_prefix: false,
16832 size: 0,
16833 last_modified: String::new(),
16834 storage_class: String::new(),
16835 }],
16836 );
16837 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
16838
16839 app.handle_action(Action::NextPane);
16841 assert_eq!(app.s3_state.selected_row, 1, "Should move to first child");
16842 }
16843
16844 #[test]
16845 fn test_s3_expand_then_load_then_right_goes_to_first_child_not_sibling() {
16846 use crate::s3::Object as S3Object;
16850 use S3Bucket;
16851 let mut app = test_app();
16852 app.current_service = Service::S3Buckets;
16853 app.service_selected = true;
16854 app.mode = Mode::Normal;
16855 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
16856
16857 app.s3_state.buckets.items = vec![
16858 S3Bucket {
16859 name: "bucket0".to_string(),
16860 region: "us-east-1".to_string(),
16861 creation_date: String::new(),
16862 },
16863 S3Bucket {
16864 name: "bucket1".to_string(),
16865 region: "us-east-1".to_string(),
16866 creation_date: String::new(),
16867 },
16868 ];
16869 app.s3_state.selected_row = 0;
16870
16871 app.handle_action(Action::NextPane);
16873 assert_eq!(
16874 app.s3_state.selected_row, 0,
16875 "Must stay on bucket0 while loading"
16876 );
16877 assert!(app.s3_state.buckets.loading);
16878
16879 app.s3_state.bucket_preview.insert(
16881 "bucket0".to_string(),
16882 vec![S3Object {
16883 key: "file.txt".to_string(),
16884 is_prefix: false,
16885 size: 0,
16886 last_modified: String::new(),
16887 storage_class: String::new(),
16888 }],
16889 );
16890 app.s3_state.buckets.loading = false;
16891
16892 app.handle_action(Action::NextPane);
16894 assert_eq!(
16895 app.s3_state.selected_row, 1,
16896 "After preview loads, right arrow must select first child (row 1), not sibling (row 2)"
16897 );
16898 }
16899
16900 #[test]
16901 fn test_s3_first_expand_never_moves_selection_before_children_visible() {
16902 use S3Bucket;
16906 let mut app = test_app();
16907 app.current_service = Service::S3Buckets;
16908 app.service_selected = true;
16909 app.mode = Mode::Normal;
16910 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
16911
16912 app.s3_state.buckets.items = vec![
16913 S3Bucket {
16914 name: "bucket0".to_string(),
16915 region: "us-east-1".to_string(),
16916 creation_date: String::new(),
16917 },
16918 S3Bucket {
16919 name: "bucket1".to_string(),
16920 region: "us-east-1".to_string(),
16921 creation_date: String::new(),
16922 },
16923 ];
16924 app.s3_state.selected_row = 0;
16926
16927 app.handle_action(Action::ExpandRow);
16929 assert_eq!(
16930 app.s3_state.selected_row, 0,
16931 "Selection must stay on bucket0 until children are visible"
16932 );
16933 assert!(app.s3_state.expanded_prefixes.contains("bucket0"));
16934 assert!(app.s3_state.buckets.loading);
16935 }
16936
16937 #[test]
16938 fn test_s3_after_preview_loads_selection_moves_to_first_child() {
16939 use crate::s3::Object as S3Object;
16942 use S3Bucket;
16943 let mut app = test_app();
16944 app.current_service = Service::S3Buckets;
16945 app.service_selected = true;
16946 app.mode = Mode::Normal;
16947 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
16948 app.s3_state.buckets.items = vec![
16949 S3Bucket {
16950 name: "b0".to_string(),
16951 region: "us-east-1".to_string(),
16952 creation_date: String::new(),
16953 },
16954 S3Bucket {
16955 name: "b1".to_string(),
16956 region: "us-east-1".to_string(),
16957 creation_date: String::new(),
16958 },
16959 ];
16960 app.s3_state.selected_row = 0;
16961
16962 app.handle_action(Action::ExpandRow);
16964 assert_eq!(
16965 app.s3_state.selected_row, 0,
16966 "Must stay on bucket while loading"
16967 );
16968
16969 app.s3_state.bucket_preview.insert(
16971 "b0".to_string(),
16972 vec![S3Object {
16973 key: "f.txt".to_string(),
16974 is_prefix: false,
16975 size: 0,
16976 last_modified: String::new(),
16977 storage_class: String::new(),
16978 }],
16979 );
16980 app.after_bucket_preview_loaded("b0");
16982 assert_eq!(
16983 app.s3_state.selected_row, 1,
16984 "After preview loads, selection must advance to first child (row 1)"
16985 );
16986 }
16987
16988 #[test]
16989 fn test_s3_right_arrow_on_expanded_loaded_bucket_enters_first_child() {
16990 use crate::s3::Object as S3Object;
16993 use S3Bucket;
16994 let mut app = test_app();
16995 app.current_service = Service::S3Buckets;
16996 app.service_selected = true;
16997 app.mode = Mode::Normal;
16998 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
16999 app.s3_state.buckets.items = vec![
17000 S3Bucket {
17001 name: "b0".to_string(),
17002 region: "us-east-1".to_string(),
17003 creation_date: String::new(),
17004 },
17005 S3Bucket {
17006 name: "b1".to_string(),
17007 region: "us-east-1".to_string(),
17008 creation_date: String::new(),
17009 },
17010 ];
17011 app.s3_state.expanded_prefixes.insert("b0".to_string());
17012 app.s3_state.bucket_preview.insert(
17013 "b0".to_string(),
17014 vec![S3Object {
17015 key: "f.txt".to_string(),
17016 is_prefix: false,
17017 size: 0,
17018 last_modified: String::new(),
17019 storage_class: String::new(),
17020 }],
17021 );
17022 app.s3_state.selected_row = 0;
17023
17024 app.handle_action(Action::ExpandRow);
17025
17026 assert!(
17028 app.s3_state.expanded_prefixes.contains("b0"),
17029 "Must stay expanded"
17030 );
17031 assert_eq!(app.s3_state.selected_row, 1, "Must move to first child");
17032 }
17033
17034 #[test]
17035 fn test_s3_second_expand_press_collapses_when_already_expanded_with_children() {
17036 use crate::s3::Object as S3Object;
17039 use S3Bucket;
17040 let mut app = test_app();
17041 app.current_service = Service::S3Buckets;
17042 app.service_selected = true;
17043 app.mode = Mode::Normal;
17044 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17045 app.s3_state.buckets.items = vec![S3Bucket {
17046 name: "bucket0".to_string(),
17047 region: "us-east-1".to_string(),
17048 creation_date: String::new(),
17049 }];
17050 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17051 app.s3_state.bucket_preview.insert(
17052 "bucket0".to_string(),
17053 vec![S3Object {
17054 key: "file.txt".to_string(),
17055 is_prefix: false,
17056 size: 0,
17057 last_modified: String::new(),
17058 storage_class: String::new(),
17059 }],
17060 );
17061 app.s3_state.selected_row = 0;
17062
17063 app.handle_action(Action::CollapseRow);
17065 assert!(
17066 !app.s3_state.expanded_prefixes.contains("bucket0"),
17067 "Left arrow must collapse the bucket"
17068 );
17069 assert_eq!(app.s3_state.selected_row, 0);
17070 }
17071
17072 #[test]
17073 fn test_s3_expand_row_index_correct_when_prior_bucket_has_error() {
17074 use S3Bucket;
17080 let mut app = test_app();
17081 app.current_service = Service::S3Buckets;
17082 app.service_selected = true;
17083 app.mode = Mode::Normal;
17084 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17085
17086 app.s3_state.buckets.items = vec![
17087 S3Bucket {
17088 name: "bucket0".to_string(),
17089 region: "us-east-1".to_string(),
17090 creation_date: String::new(),
17091 },
17092 S3Bucket {
17093 name: "bucket1".to_string(),
17094 region: "us-east-1".to_string(),
17095 creation_date: String::new(),
17096 },
17097 S3Bucket {
17098 name: "bucket2".to_string(),
17099 region: "us-east-1".to_string(),
17100 creation_date: String::new(),
17101 },
17102 ];
17103
17104 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17106 app.s3_state
17107 .bucket_errors
17108 .insert("bucket0".to_string(), "AccessDenied".to_string());
17109
17110 let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
17113 assert_eq!(
17114 total, 4,
17115 "Should be 4 rows: bucket0 + error + bucket1 + bucket2"
17116 );
17117
17118 app.s3_state.selected_row = 3;
17120
17121 use crate::s3::Object as S3Object;
17123 app.s3_state.bucket_preview.insert(
17124 "bucket2".to_string(),
17125 vec![S3Object {
17126 key: "file.txt".to_string(),
17127 is_prefix: false,
17128 size: 0,
17129 last_modified: String::new(),
17130 storage_class: String::new(),
17131 }],
17132 );
17133
17134 app.handle_action(Action::NextPane);
17135
17136 assert!(
17138 app.s3_state.expanded_prefixes.contains("bucket2"),
17139 "Must expand bucket2, not bucket1"
17140 );
17141 assert_eq!(
17142 app.s3_state.selected_row, 4,
17143 "First child of bucket2 must be row 4"
17144 );
17145 }
17146
17147 #[test]
17148 fn test_s3_next_pane_row_count_matches_calculate_filtered_bucket_rows() {
17149 use crate::s3::Object as S3Object;
17153 use S3Bucket;
17154 let mut app = test_app();
17155 app.current_service = Service::S3Buckets;
17156 app.service_selected = true;
17157 app.mode = Mode::Normal;
17158 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17159
17160 app.s3_state.buckets.items = vec![
17161 S3Bucket {
17162 name: "bucket0".to_string(),
17163 region: "us-east-1".to_string(),
17164 creation_date: String::new(),
17165 },
17166 S3Bucket {
17167 name: "bucket1".to_string(),
17168 region: "us-east-1".to_string(),
17169 creation_date: String::new(),
17170 },
17171 S3Bucket {
17172 name: "bucket2".to_string(),
17173 region: "us-east-1".to_string(),
17174 creation_date: String::new(),
17175 },
17176 ];
17177
17178 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17180 app.s3_state.bucket_preview.insert(
17181 "bucket0".to_string(),
17182 vec![
17183 S3Object {
17184 key: "a.txt".to_string(),
17185 is_prefix: false,
17186 size: 0,
17187 last_modified: String::new(),
17188 storage_class: String::new(),
17189 },
17190 S3Object {
17191 key: "b.txt".to_string(),
17192 is_prefix: false,
17193 size: 0,
17194 last_modified: String::new(),
17195 storage_class: String::new(),
17196 },
17197 S3Object {
17198 key: "c.txt".to_string(),
17199 is_prefix: false,
17200 size: 0,
17201 last_modified: String::new(),
17202 storage_class: String::new(),
17203 },
17204 ],
17205 );
17206
17207 app.s3_state.expanded_prefixes.insert("bucket1".to_string());
17209
17210 let total = crate::ui::s3::calculate_filtered_bucket_rows(&app);
17212 assert_eq!(
17213 total, 6,
17214 "Total rows: bucket0(1)+3children+bucket1(1)+bucket2(1)=6"
17215 );
17216
17217 app.s3_state.selected_row = 5;
17220
17221 app.s3_state.bucket_preview.insert(
17223 "bucket2".to_string(),
17224 vec![S3Object {
17225 key: "x.txt".to_string(),
17226 is_prefix: false,
17227 size: 0,
17228 last_modified: String::new(),
17229 storage_class: String::new(),
17230 }],
17231 );
17232
17233 app.handle_action(Action::NextPane);
17234
17235 assert!(
17236 app.s3_state.expanded_prefixes.contains("bucket2"),
17237 "Must expand bucket2, not bucket1"
17238 );
17239 assert_eq!(
17240 app.s3_state.selected_row, 6,
17241 "First child of bucket2 must be row 6"
17242 );
17243 }
17244
17245 #[test]
17246 fn test_s3_expand_row_index_correct_when_prior_bucket_has_loaded_children() {
17247 use crate::s3::Object as S3Object;
17252 use S3Bucket;
17253 let mut app = test_app();
17254 app.current_service = Service::S3Buckets;
17255 app.service_selected = true;
17256 app.mode = Mode::Normal;
17257 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17258
17259 app.s3_state.buckets.items = vec![
17260 S3Bucket {
17261 name: "bucket0".to_string(),
17262 region: "us-east-1".to_string(),
17263 creation_date: String::new(),
17264 },
17265 S3Bucket {
17266 name: "bucket1".to_string(),
17267 region: "us-east-1".to_string(),
17268 creation_date: String::new(),
17269 },
17270 S3Bucket {
17271 name: "bucket2".to_string(),
17272 region: "us-east-1".to_string(),
17273 creation_date: String::new(),
17274 },
17275 ];
17276
17277 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17279 app.s3_state.bucket_preview.insert(
17280 "bucket0".to_string(),
17281 vec![
17282 S3Object {
17283 key: "a.txt".to_string(),
17284 is_prefix: false,
17285 size: 0,
17286 last_modified: String::new(),
17287 storage_class: String::new(),
17288 },
17289 S3Object {
17290 key: "b.txt".to_string(),
17291 is_prefix: false,
17292 size: 0,
17293 last_modified: String::new(),
17294 storage_class: String::new(),
17295 },
17296 ],
17297 );
17298
17299 app.s3_state.selected_row = 4;
17302
17303 app.s3_state.bucket_preview.insert(
17305 "bucket2".to_string(),
17306 vec![S3Object {
17307 key: "c.txt".to_string(),
17308 is_prefix: false,
17309 size: 0,
17310 last_modified: String::new(),
17311 storage_class: String::new(),
17312 }],
17313 );
17314
17315 app.handle_action(Action::NextPane);
17316
17317 assert!(
17319 app.s3_state.expanded_prefixes.contains("bucket2"),
17320 "bucket2 must be expanded"
17321 );
17322 assert_eq!(
17323 app.s3_state.selected_row, 5,
17324 "Must select first child of bucket2 (row 5), not some wrong row"
17325 );
17326 assert!(
17327 !app.s3_state.expanded_prefixes.contains("bucket1"),
17328 "bucket1 must NOT be expanded"
17329 );
17330 }
17331
17332 #[test]
17333 fn test_s3_expand_with_empty_preview_stays_on_bucket() {
17334 use S3Bucket;
17337 let mut app = test_app();
17338 app.current_service = Service::S3Buckets;
17339 app.service_selected = true;
17340 app.mode = Mode::Normal;
17341 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17342
17343 app.s3_state.buckets.items = vec![
17344 S3Bucket {
17345 name: "bucket0".to_string(),
17346 region: "us-east-1".to_string(),
17347 creation_date: String::new(),
17348 },
17349 S3Bucket {
17350 name: "bucket1".to_string(),
17351 region: "us-east-1".to_string(),
17352 creation_date: String::new(),
17353 },
17354 S3Bucket {
17355 name: "bucket2".to_string(),
17356 region: "us-east-1".to_string(),
17357 creation_date: String::new(),
17358 },
17359 ];
17360 app.s3_state
17362 .bucket_preview
17363 .insert("bucket1".to_string(), vec![]);
17364 app.s3_state.selected_row = 1; app.handle_action(Action::NextPane);
17367
17368 assert_eq!(
17370 app.s3_state.selected_row, 1,
17371 "Empty bucket: selection must stay on bucket row, not advance to next sibling"
17372 );
17373 assert!(
17374 app.s3_state.expanded_prefixes.contains("bucket1"),
17375 "Bucket must be expanded (showing ▼)"
17376 );
17377 }
17378
17379 #[test]
17380 fn test_s3_expand_with_preceding_loading_buckets_on_prior_page() {
17381 use crate::s3::Object as S3Object;
17386 use S3Bucket;
17387 let mut app = test_app();
17388 app.current_service = Service::S3Buckets;
17389 app.service_selected = true;
17390 app.mode = Mode::Normal;
17391 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17392
17393 app.s3_state.buckets.items = (0..60)
17395 .map(|i| S3Bucket {
17396 name: format!("bucket{:02}", i),
17397 region: "us-east-1".to_string(),
17398 creation_date: String::new(),
17399 })
17400 .collect();
17401
17402 for i in 0..3usize {
17404 app.s3_state
17405 .expanded_prefixes
17406 .insert(format!("bucket{:02}", i));
17407 }
17408
17409 app.s3_state.bucket_scroll_offset = 50;
17410
17411 app.s3_state.bucket_preview.insert(
17413 "bucket55".to_string(),
17414 vec![S3Object {
17415 key: "file.txt".to_string(),
17416 is_prefix: false,
17417 size: 0,
17418 last_modified: String::new(),
17419 storage_class: String::new(),
17420 }],
17421 );
17422
17423 app.s3_state.selected_row = 55;
17425
17426 app.handle_action(Action::NextPane);
17427
17428 assert!(
17430 app.s3_state.expanded_prefixes.contains("bucket55"),
17431 "bucket55 must be expanded"
17432 );
17433 assert_eq!(
17434 app.s3_state.selected_row, 56,
17435 "Must select first child of bucket55 (row 56)"
17436 );
17437 }
17438
17439 #[test]
17440 fn test_s3_expand_with_multiple_preceding_loading_buckets() {
17441 use crate::s3::Object as S3Object;
17444 use S3Bucket;
17445 let mut app = test_app();
17446 app.current_service = Service::S3Buckets;
17447 app.service_selected = true;
17448 app.mode = Mode::Normal;
17449 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17450
17451 app.s3_state.buckets.items = (0..5)
17452 .map(|i| S3Bucket {
17453 name: format!("bucket{}", i),
17454 region: "us-east-1".to_string(),
17455 creation_date: String::new(),
17456 })
17457 .collect();
17458
17459 for i in 0..3 {
17461 app.s3_state
17462 .expanded_prefixes
17463 .insert(format!("bucket{}", i));
17464 }
17465
17466 app.s3_state.bucket_preview.insert(
17468 "bucket3".to_string(),
17469 vec![S3Object {
17470 key: "file.txt".to_string(),
17471 is_prefix: false,
17472 size: 0,
17473 last_modified: String::new(),
17474 storage_class: String::new(),
17475 }],
17476 );
17477
17478 app.s3_state.selected_row = 3; app.handle_action(Action::NextPane);
17483
17484 assert!(
17486 app.s3_state.expanded_prefixes.contains("bucket3"),
17487 "bucket3 must be expanded"
17488 );
17489 assert_eq!(
17490 app.s3_state.selected_row, 4,
17491 "Must select first child of bucket3 (row 4)"
17492 );
17493 }
17494
17495 #[test]
17496 fn test_s3_expand_with_preceding_loading_bucket_selects_correct_row() {
17497 use crate::s3::Object as S3Object;
17502 use S3Bucket;
17503 let mut app = test_app();
17504 app.current_service = Service::S3Buckets;
17505 app.service_selected = true;
17506 app.mode = Mode::Normal;
17507 app.s3_state.bucket_visible_rows = std::cell::Cell::new(10);
17508
17509 app.s3_state.buckets.items = vec![
17510 S3Bucket {
17511 name: "bucket0".to_string(),
17512 region: "us-east-1".to_string(),
17513 creation_date: String::new(),
17514 },
17515 S3Bucket {
17516 name: "bucket1".to_string(),
17517 region: "us-east-1".to_string(),
17518 creation_date: String::new(),
17519 },
17520 S3Bucket {
17521 name: "bucket2".to_string(),
17522 region: "us-east-1".to_string(),
17523 creation_date: String::new(),
17524 },
17525 ];
17526
17527 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17529 app.s3_state.buckets.loading = true;
17530
17531 app.s3_state.bucket_preview.insert(
17533 "bucket1".to_string(),
17534 vec![S3Object {
17535 key: "file.txt".to_string(),
17536 is_prefix: false,
17537 size: 0,
17538 last_modified: String::new(),
17539 storage_class: String::new(),
17540 }],
17541 );
17542
17543 app.s3_state.selected_row = 1;
17545
17546 app.handle_action(Action::NextPane);
17548
17549 assert_eq!(
17551 app.s3_state.selected_row, 2,
17552 "Must expand bucket1's first child (row 2), not bucket2 (row 3)"
17553 );
17554 assert!(
17555 app.s3_state.expanded_prefixes.contains("bucket1"),
17556 "bucket1 must be in expanded_prefixes"
17557 );
17558 }
17559
17560 #[test]
17561 fn test_s3_expand_last_row_on_page_shows_first_child_not_next_page() {
17562 use crate::s3::Object as S3Object;
17567 use S3Bucket;
17568 let mut app = test_app();
17569 app.current_service = Service::S3Buckets;
17570 app.service_selected = true;
17571 app.mode = Mode::Normal;
17572
17573 app.s3_state.buckets.items = vec![
17575 S3Bucket {
17576 name: "bucket0".to_string(),
17577 region: "us-east-1".to_string(),
17578 creation_date: String::new(),
17579 },
17580 S3Bucket {
17581 name: "bucket1".to_string(),
17582 region: "us-east-1".to_string(),
17583 creation_date: String::new(),
17584 },
17585 S3Bucket {
17586 name: "bucket2".to_string(),
17587 region: "us-east-1".to_string(),
17588 creation_date: String::new(),
17589 },
17590 ];
17591 app.s3_state.bucket_visible_rows = std::cell::Cell::new(2);
17592 app.s3_state.selected_row = 1;
17594 app.s3_state.bucket_scroll_offset = 0;
17595
17596 app.s3_state.bucket_preview.insert(
17598 "bucket1".to_string(),
17599 vec![S3Object {
17600 key: "file.txt".to_string(),
17601 is_prefix: false,
17602 size: 0,
17603 last_modified: String::new(),
17604 storage_class: String::new(),
17605 }],
17606 );
17607
17608 app.handle_action(Action::NextPane); assert_eq!(app.s3_state.selected_row, 2, "Must select first child");
17612
17613 let visible = app.s3_state.bucket_visible_rows.get();
17615 let in_view = app.s3_state.selected_row >= app.s3_state.bucket_scroll_offset
17616 && app.s3_state.selected_row < app.s3_state.bucket_scroll_offset + visible;
17617 assert!(
17618 in_view,
17619 "First child row {} must be visible in [{}, {})",
17620 app.s3_state.selected_row,
17621 app.s3_state.bucket_scroll_offset,
17622 app.s3_state.bucket_scroll_offset + visible
17623 );
17624 }
17625
17626 #[test]
17627 fn test_apig_detail_tab_navigation() {
17628 use crate::apig::api::RestApi;
17629 use crate::ui::apig::ApiDetailTab;
17630
17631 let mut app = test_app();
17632 app.current_service = Service::ApiGatewayApis;
17633 app.apig_state.current_api = Some(RestApi {
17634 id: "test123".to_string(),
17635 name: "Test API".to_string(),
17636 description: "Test".to_string(),
17637 created_date: "2024-01-01".to_string(),
17638 api_key_source: "HEADER".to_string(),
17639 endpoint_configuration: "REGIONAL".to_string(),
17640 protocol_type: "REST".to_string(),
17641 disable_execute_api_endpoint: false,
17642 status: "AVAILABLE".to_string(),
17643 });
17644
17645 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17647
17648 app.handle_action(Action::NextDetailTab);
17650 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17651
17652 app.handle_action(Action::PrevDetailTab);
17654 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17655
17656 app.handle_action(Action::NextDetailTab);
17658 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17659
17660 app.handle_action(Action::PrevDetailTab);
17662 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17663
17664 app.handle_action(Action::NextDetailTab);
17666 assert_eq!(app.apig_state.detail_tab, ApiDetailTab::Routes);
17667 }
17668
17669 #[test]
17670 fn test_apig_routes_expand_collapse() {
17671 use crate::apig::api::RestApi;
17672 use crate::apig::route::Route;
17673 use crate::ui::apig::ApiDetailTab;
17674
17675 let mut app = test_app();
17676 app.current_service = Service::ApiGatewayApis;
17677 app.apig_state.current_api = Some(RestApi {
17678 id: "test123".to_string(),
17679 name: "Test API".to_string(),
17680 description: "Test".to_string(),
17681 created_date: "2024-01-01".to_string(),
17682 api_key_source: "HEADER".to_string(),
17683 endpoint_configuration: "REGIONAL".to_string(),
17684 protocol_type: "HTTP".to_string(),
17685 disable_execute_api_endpoint: false,
17686 status: "AVAILABLE".to_string(),
17687 });
17688 app.apig_state.detail_tab = ApiDetailTab::Routes;
17689
17690 let virtual_parent = Route {
17692 route_id: "virtual_/api".to_string(),
17693 route_key: "/api".to_string(),
17694 target: String::new(), authorization_type: String::new(),
17696 api_key_required: false,
17697 display_name: String::new(),
17698 arn: String::new(),
17699 };
17700 let child_route = Route {
17701 route_id: "1".to_string(),
17702 route_key: "/api/users".to_string(),
17703 target: "integration1".to_string(),
17704 authorization_type: "NONE".to_string(),
17705 api_key_required: false,
17706 display_name: String::new(),
17707 arn: String::new(),
17708 };
17709
17710 app.apig_state.routes.items = vec![virtual_parent];
17711 app.apig_state
17712 .route_children
17713 .insert("/api".to_string(), vec![child_route]);
17714
17715 assert!(app.apig_state.expanded_routes.is_empty());
17717
17718 app.apig_state.routes.selected = 0;
17720 app.handle_action(Action::ExpandRow);
17721 assert!(app.apig_state.expanded_routes.contains("/api"));
17722
17723 app.handle_action(Action::CollapseRow);
17725 assert!(!app.apig_state.expanded_routes.contains("/api"));
17726
17727 app.handle_action(Action::ExpandRow);
17729 assert!(app.apig_state.expanded_routes.contains("/api"));
17730 }
17731
17732 #[test]
17733 fn test_apig_routes_navigation() {
17734 use crate::apig::api::RestApi;
17735 use crate::apig::route::Route;
17736 use crate::ui::apig::ApiDetailTab;
17737
17738 let mut app = test_app();
17739 app.mode = Mode::Normal;
17740 app.service_selected = true;
17741 app.current_service = Service::ApiGatewayApis;
17742 app.apig_state.current_api = Some(RestApi {
17743 id: "test123".to_string(),
17744 name: "Test API".to_string(),
17745 description: "Test".to_string(),
17746 created_date: "2024-01-01".to_string(),
17747 api_key_source: "HEADER".to_string(),
17748 endpoint_configuration: "REGIONAL".to_string(),
17749 protocol_type: "HTTP".to_string(),
17750 disable_execute_api_endpoint: false,
17751 status: "AVAILABLE".to_string(),
17752 });
17753 app.apig_state.detail_tab = ApiDetailTab::Routes;
17754 app.apig_state.routes.items = vec![
17755 Route {
17756 route_id: "1".to_string(),
17757 route_key: "/api/users".to_string(),
17758 target: "integration1".to_string(),
17759 authorization_type: "NONE".to_string(),
17760 api_key_required: false,
17761 display_name: String::new(),
17762 arn: String::new(),
17763 },
17764 Route {
17765 route_id: "2".to_string(),
17766 route_key: "/health".to_string(),
17767 target: "integration2".to_string(),
17768 authorization_type: "NONE".to_string(),
17769 api_key_required: false,
17770 display_name: String::new(),
17771 arn: String::new(),
17772 },
17773 ];
17774
17775 assert_eq!(app.apig_state.routes.selected, 0);
17777
17778 app.handle_action(Action::NextItem);
17780 assert_eq!(app.apig_state.routes.selected, 1);
17781
17782 app.handle_action(Action::NextItem);
17784 assert_eq!(app.apig_state.routes.selected, 1);
17785
17786 app.handle_action(Action::PrevItem);
17788 assert_eq!(app.apig_state.routes.selected, 0);
17789
17790 app.handle_action(Action::PrevItem);
17792 assert_eq!(app.apig_state.routes.selected, 0);
17793 }
17794
17795 #[test]
17796 fn test_apig_routes_expand_jumps_to_child() {
17797 use crate::apig::api::RestApi;
17798 use crate::apig::route::Route;
17799 use crate::ui::apig::ApiDetailTab;
17800 use std::collections::HashMap;
17801
17802 let mut app = test_app();
17803 app.mode = Mode::Normal;
17804 app.service_selected = true;
17805 app.current_service = Service::ApiGatewayApis;
17806 app.apig_state.current_api = Some(RestApi {
17807 id: "test123".to_string(),
17808 name: "Test API".to_string(),
17809 description: "Test".to_string(),
17810 created_date: "2024-01-01".to_string(),
17811 api_key_source: "HEADER".to_string(),
17812 endpoint_configuration: "REGIONAL".to_string(),
17813 protocol_type: "HTTP".to_string(),
17814 disable_execute_api_endpoint: false,
17815 status: "AVAILABLE".to_string(),
17816 });
17817 app.apig_state.detail_tab = ApiDetailTab::Routes;
17818
17819 app.apig_state.routes.items = vec![Route {
17821 route_id: "virtual_/api".to_string(),
17822 route_key: "/api".to_string(),
17823 target: String::new(),
17824 authorization_type: String::new(),
17825 api_key_required: false,
17826 display_name: String::new(),
17827 arn: String::new(),
17828 }];
17829
17830 let mut children = HashMap::new();
17831 children.insert(
17832 "/api".to_string(),
17833 vec![Route {
17834 route_id: "1".to_string(),
17835 route_key: "/api/users".to_string(),
17836 target: "integration1".to_string(),
17837 authorization_type: "NONE".to_string(),
17838 api_key_required: false,
17839 display_name: String::new(),
17840 arn: String::new(),
17841 }],
17842 );
17843 app.apig_state.route_children = children;
17844
17845 assert_eq!(app.apig_state.routes.selected, 0);
17847 assert!(!app.apig_state.expanded_routes.contains("/api"));
17848
17849 app.handle_action(Action::ExpandRow);
17851 assert!(app.apig_state.expanded_routes.contains("/api"));
17852 assert_eq!(app.apig_state.routes.selected, 0);
17853
17854 app.handle_action(Action::ExpandRow);
17856 assert_eq!(app.apig_state.routes.selected, 1);
17857 }
17858
17859 #[test]
17860 fn test_apig_filter_only_when_focused() {
17861 use crate::apig::api::RestApi;
17862 use crate::ui::apig::ApiDetailTab;
17863
17864 let mut app = test_app();
17865 app.current_service = Service::ApiGatewayApis;
17866 app.apig_state.current_api = Some(RestApi {
17867 id: "test".to_string(),
17868 name: "Test API".to_string(),
17869 description: "Test".to_string(),
17870 created_date: "2024-01-01".to_string(),
17871 api_key_source: "HEADER".to_string(),
17872 endpoint_configuration: "REGIONAL".to_string(),
17873 protocol_type: "HTTP".to_string(),
17874 disable_execute_api_endpoint: false,
17875 status: "AVAILABLE".to_string(),
17876 });
17877 app.apig_state.detail_tab = ApiDetailTab::Routes;
17878 app.mode = Mode::FilterInput;
17879
17880 app.apig_state.input_focus = InputFocus::Pagination;
17882 app.handle_action(Action::FilterInput('x'));
17883 assert_eq!(app.apig_state.route_filter, "");
17884
17885 app.apig_state.input_focus = InputFocus::Filter;
17887 app.handle_action(Action::FilterInput('x'));
17888 assert_eq!(app.apig_state.route_filter, "x");
17889 }
17890
17891 #[test]
17892 fn test_apig_routes_and_resources_use_same_render_function() {
17893 let source = include_str!("ui/apig.rs");
17895 let render_calls: Vec<_> = source
17896 .match_indices("crate::ui::table::render_tree_table")
17897 .collect();
17898
17899 assert_eq!(
17901 render_calls.len(),
17902 2,
17903 "Both routes and resources must use render_tree_table"
17904 );
17905 }
17906
17907 #[test]
17908 fn test_s3_uses_same_render_function() {
17909 let source = include_str!("ui/s3.rs");
17911 let render_calls: Vec<_> = source
17912 .match_indices("crate::ui::table::render_tree_table")
17913 .collect();
17914
17915 assert!(!render_calls.is_empty(), "S3 must use render_tree_table");
17917 }
17918
17919 #[test]
17920 fn test_s3_page_down_skips_expanded_children() {
17921 use crate::keymap::Action;
17926
17927 let mut app = test_app();
17928 app.current_service = Service::S3Buckets;
17929 app.service_selected = true;
17930 app.mode = Mode::Normal;
17931 app.s3_state.bucket_visible_rows = std::cell::Cell::new(30);
17932
17933 app.s3_state.buckets.items = (0..3)
17934 .map(|i| S3Bucket {
17935 name: format!("bucket{}", i),
17936 region: "us-east-1".to_string(),
17937 creation_date: String::new(),
17938 })
17939 .collect();
17940
17941 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17943 app.s3_state.bucket_preview.insert(
17944 "bucket0".to_string(),
17945 (0..20)
17946 .map(|i| crate::app::S3Object {
17947 key: format!("key{}", i),
17948 size: 0,
17949 last_modified: String::new(),
17950 storage_class: String::new(),
17951 is_prefix: false,
17952 })
17953 .collect(),
17954 );
17955
17956 app.s3_state.selected_row = 0;
17957
17958 app.handle_action(Action::PageDown);
17960 let row = app.s3_state.selected_row;
17962 assert_eq!(
17963 row, 22,
17964 "PageDown must land on bucket2 (visual row 22), got {}",
17965 row
17966 );
17967
17968 app.handle_action(Action::PageUp);
17970 assert_eq!(
17971 app.s3_state.selected_row, 0,
17972 "PageUp must return to bucket0"
17973 );
17974 }
17975
17976 #[test]
17977 fn test_s3_page_down_with_many_buckets_skips_expanded() {
17978 use crate::keymap::Action;
17981
17982 let mut app = test_app();
17983 app.current_service = Service::S3Buckets;
17984 app.service_selected = true;
17985 app.mode = Mode::Normal;
17986 app.s3_state.bucket_visible_rows = std::cell::Cell::new(30);
17987
17988 app.s3_state.buckets.items = (0..25)
17989 .map(|i| S3Bucket {
17990 name: format!("bucket{}", i),
17991 region: "us-east-1".to_string(),
17992 creation_date: String::new(),
17993 })
17994 .collect();
17995
17996 app.s3_state.expanded_prefixes.insert("bucket0".to_string());
17998 app.s3_state.bucket_preview.insert(
17999 "bucket0".to_string(),
18000 (0..100)
18001 .map(|i| crate::app::S3Object {
18002 key: format!("key{}", i),
18003 size: 0,
18004 last_modified: String::new(),
18005 storage_class: String::new(),
18006 is_prefix: false,
18007 })
18008 .collect(),
18009 );
18010
18011 app.s3_state.selected_row = 0;
18012
18013 app.handle_action(Action::PageDown);
18015 let row = app.s3_state.selected_row;
18016 assert_eq!(
18018 row, 110,
18019 "PageDown must land on bucket10 (row 110), got {}",
18020 row
18021 );
18022 }
18023
18024 #[test]
18025 fn test_search_icon_has_proper_border_spacing() {
18026 use crate::ui::SEARCH_ICON;
18029
18030 assert!(SEARCH_ICON.starts_with("─"), "Should start with dash");
18031 assert!(
18032 SEARCH_ICON.ends_with("─"),
18033 "Should end with dash for proper border spacing"
18034 );
18035 assert!(SEARCH_ICON.contains("🔍"), "Should contain search icon");
18036 }
18037
18038 #[test]
18039 fn test_apig_expand_with_filter() {
18040 use crate::apig::api::RestApi;
18041 use crate::apig::route::Route;
18042 use crate::ui::apig::ApiDetailTab;
18043 use std::collections::HashMap;
18044
18045 let mut app = test_app();
18046 app.current_service = Service::ApiGatewayApis;
18047 app.apig_state.current_api = Some(RestApi {
18048 id: "test123".to_string(),
18049 name: "Test API".to_string(),
18050 description: "Test".to_string(),
18051 created_date: "2024-01-01".to_string(),
18052 api_key_source: "HEADER".to_string(),
18053 endpoint_configuration: "REGIONAL".to_string(),
18054 protocol_type: "HTTP".to_string(),
18055 disable_execute_api_endpoint: false,
18056 status: "AVAILABLE".to_string(),
18057 });
18058 app.apig_state.detail_tab = ApiDetailTab::Routes;
18059
18060 app.apig_state.routes.items = vec![Route {
18062 route_id: "0".to_string(),
18063 route_key: "/api".to_string(),
18064 target: "".to_string(),
18065 authorization_type: "NONE".to_string(),
18066 api_key_required: false,
18067 display_name: "/api".to_string(),
18068 arn: String::new(),
18069 }];
18070
18071 let mut children = HashMap::new();
18072 children.insert(
18073 "/api".to_string(),
18074 vec![Route {
18075 route_id: "1".to_string(),
18076 route_key: "GET".to_string(),
18077 target: "integration1".to_string(),
18078 authorization_type: "NONE".to_string(),
18079 api_key_required: false,
18080 display_name: "GET".to_string(),
18081 arn: String::new(),
18082 }],
18083 );
18084 app.apig_state.route_children = children;
18085
18086 app.apig_state.route_filter = "GET".to_string();
18088
18089 assert_eq!(app.apig_state.routes.selected, 0);
18092 assert!(!app.apig_state.expanded_routes.contains("/api"));
18093
18094 app.handle_action(Action::ExpandRow);
18095
18096 assert!(app.apig_state.expanded_routes.contains("/api"));
18098 }
18099
18100 #[test]
18101 fn test_apig_console_url_routes() {
18102 use crate::apig::api::RestApi;
18103 use crate::apig::route::Route;
18104 use crate::ui::apig::ApiDetailTab;
18105
18106 let mut app = test_app();
18107 app.current_service = Service::ApiGatewayApis;
18108 app.config.region = "us-east-1".to_string();
18109
18110 let url = app.get_console_url();
18112 assert!(url.contains("apigateway/main/apis"));
18113 assert!(url.contains("region=us-east-1"));
18114
18115 app.apig_state.current_api = Some(RestApi {
18117 id: "2todvod3n0".to_string(),
18118 name: "Test API".to_string(),
18119 description: "Test".to_string(),
18120 created_date: "2024-01-01".to_string(),
18121 api_key_source: "HEADER".to_string(),
18122 endpoint_configuration: "REGIONAL".to_string(),
18123 protocol_type: "HTTP".to_string(),
18124 disable_execute_api_endpoint: false,
18125 status: "AVAILABLE".to_string(),
18126 });
18127 app.apig_state.detail_tab = ApiDetailTab::Routes;
18128 app.apig_state.routes.items = vec![Route {
18129 route_id: "eizmisr".to_string(),
18130 route_key: "GET /test".to_string(),
18131 target: "integration1".to_string(),
18132 authorization_type: "NONE".to_string(),
18133 api_key_required: false,
18134 display_name: "GET /test".to_string(),
18135 arn: String::new(),
18136 }];
18137 app.apig_state.routes.selected = 0;
18138
18139 let url = app.get_console_url();
18140 assert!(url.contains("apigateway/main/develop/routes"));
18141 assert!(url.contains("api=2todvod3n0"));
18142 assert!(url.contains("routes=eizmisr"));
18143 assert!(url.contains("region=us-east-1"));
18144 }
18145
18146 #[test]
18147 fn test_apig_console_url_resources() {
18148 use crate::apig::api::RestApi;
18149 use crate::apig::resource::Resource;
18150 use crate::ui::apig::ApiDetailTab;
18151
18152 let mut app = test_app();
18153 app.current_service = Service::ApiGatewayApis;
18154 app.config.region = "us-east-1".to_string();
18155
18156 app.apig_state.current_api = Some(RestApi {
18158 id: "2j9j50ze47".to_string(),
18159 name: "Test API".to_string(),
18160 description: "Test".to_string(),
18161 created_date: "2024-01-01".to_string(),
18162 api_key_source: "HEADER".to_string(),
18163 endpoint_configuration: "REGIONAL".to_string(),
18164 protocol_type: "REST".to_string(),
18165 disable_execute_api_endpoint: false,
18166 status: "AVAILABLE".to_string(),
18167 });
18168 app.apig_state.detail_tab = ApiDetailTab::Routes;
18169 app.apig_state.resources.items = vec![Resource {
18170 id: "abc123".to_string(),
18171 path: "/test".to_string(),
18172 parent_id: None,
18173 methods: vec![],
18174 display_name: "/test".to_string(),
18175 arn: String::new(),
18176 }];
18177 app.apig_state.resources.selected = 0;
18178
18179 let url = app.get_console_url();
18180 assert!(url.contains("apigateway/main/apis/2j9j50ze47/resources"));
18181 assert!(url.contains("api=2j9j50ze47"));
18182 assert!(url.contains("#abc123"));
18183 assert!(url.contains("region=us-east-1"));
18184 }
18185
18186 #[test]
18187 fn test_apig_console_url_routes_parent_vs_leaf() {
18188 use crate::apig::api::RestApi;
18189 use crate::apig::route::Route;
18190 use crate::ui::apig::ApiDetailTab;
18191 use std::collections::HashMap;
18192
18193 let mut app = test_app();
18194 app.current_service = Service::ApiGatewayApis;
18195 app.config.region = "us-east-1".to_string();
18196
18197 app.apig_state.current_api = Some(RestApi {
18198 id: "2todvod3n0".to_string(),
18199 name: "Test API".to_string(),
18200 description: "Test".to_string(),
18201 created_date: "2024-01-01".to_string(),
18202 api_key_source: "HEADER".to_string(),
18203 endpoint_configuration: "REGIONAL".to_string(),
18204 protocol_type: "HTTP".to_string(),
18205 disable_execute_api_endpoint: false,
18206 status: "AVAILABLE".to_string(),
18207 });
18208 app.apig_state.detail_tab = ApiDetailTab::Routes;
18209
18210 app.apig_state.routes.items = vec![Route {
18212 route_id: "parent".to_string(),
18213 route_key: "/v1/get/jobs".to_string(),
18214 target: "".to_string(), authorization_type: "NONE".to_string(),
18216 api_key_required: false,
18217 display_name: "/v1/get/jobs".to_string(),
18218 arn: String::new(),
18219 }];
18220
18221 let mut children = HashMap::new();
18223 children.insert(
18224 "/v1/get/jobs".to_string(),
18225 vec![Route {
18226 route_id: "1iz9vtl".to_string(),
18227 route_key: "GET".to_string(),
18228 target: "integration1".to_string(), authorization_type: "NONE".to_string(),
18230 api_key_required: false,
18231 display_name: "GET".to_string(),
18232 arn: String::new(),
18233 }],
18234 );
18235 app.apig_state.route_children = children;
18236
18237 app.apig_state.routes.selected = 0;
18239 let url = app.get_console_url();
18240 assert!(url.contains("apigateway/main/develop/routes"));
18241 assert!(url.contains("api=2todvod3n0"));
18242 assert!(
18243 !url.contains("routes="),
18244 "Parent node should not include routes parameter"
18245 );
18246
18247 app.apig_state
18249 .expanded_routes
18250 .insert("/v1/get/jobs".to_string());
18251 app.apig_state.routes.selected = 1;
18252 let url = app.get_console_url();
18253 assert!(
18254 url.contains("routes=1iz9vtl"),
18255 "Leaf node should include routes parameter: {}",
18256 url
18257 );
18258 }
18259
18260 #[test]
18261 fn test_apig_preferences_context() {
18262 use crate::apig::api::RestApi;
18263 use crate::ui::apig::ApiDetailTab;
18264
18265 let mut app = test_app();
18266 app.current_service = Service::ApiGatewayApis;
18267
18268 assert!(
18270 app.apig_state.current_api.is_none(),
18271 "Should be in list view"
18272 );
18273
18274 app.apig_state.current_api = Some(RestApi {
18276 id: "test123".to_string(),
18277 name: "Test API".to_string(),
18278 description: "Test".to_string(),
18279 created_date: "2024-01-01".to_string(),
18280 api_key_source: "HEADER".to_string(),
18281 endpoint_configuration: "REGIONAL".to_string(),
18282 protocol_type: "HTTP".to_string(),
18283 disable_execute_api_endpoint: false,
18284 status: "AVAILABLE".to_string(),
18285 });
18286 app.apig_state.detail_tab = ApiDetailTab::Routes;
18287
18288 assert!(
18289 app.apig_state.current_api.is_some(),
18290 "Should be in detail view"
18291 );
18292 }
18294
18295 #[test]
18296 fn test_apig_route_columns() {
18297 use crate::apig::route::Column as RouteColumn;
18298
18299 let cols = RouteColumn::all();
18301 assert_eq!(cols.len(), 5);
18302
18303 assert_eq!(RouteColumn::RouteKey.id(), "route_key");
18305 assert_eq!(RouteColumn::RouteId.id(), "route_id");
18306 assert_eq!(RouteColumn::Arn.id(), "arn");
18307 assert_eq!(RouteColumn::AuthorizationType.id(), "authorization_type");
18308 assert_eq!(RouteColumn::Target.id(), "target");
18309
18310 assert_eq!(
18312 RouteColumn::from_id("route_key"),
18313 Some(RouteColumn::RouteKey)
18314 );
18315 assert_eq!(RouteColumn::from_id("arn"), Some(RouteColumn::Arn));
18316 assert_eq!(RouteColumn::from_id("invalid"), None);
18317 }
18318
18319 #[test]
18320 fn test_apig_yank_copies_route_arn() {
18321 use crate::apig::api::RestApi;
18322 use crate::apig::route::Route;
18323 use crate::ui::apig::ApiDetailTab;
18324
18325 let mut app = test_app();
18326 app.current_service = Service::ApiGatewayApis;
18327 app.apig_state.current_api = Some(RestApi {
18328 id: "test123".to_string(),
18329 name: "Test API".to_string(),
18330 description: "Test".to_string(),
18331 created_date: "2024-01-01".to_string(),
18332 api_key_source: "HEADER".to_string(),
18333 endpoint_configuration: "REGIONAL".to_string(),
18334 protocol_type: "HTTP".to_string(),
18335 disable_execute_api_endpoint: false,
18336 status: "AVAILABLE".to_string(),
18337 });
18338 app.apig_state.detail_tab = ApiDetailTab::Routes;
18339
18340 app.apig_state.routes.items = vec![
18342 Route {
18343 route_id: "route1".to_string(),
18344 route_key: "GET /users".to_string(),
18345 target: "integrations/abc".to_string(),
18346 authorization_type: "NONE".to_string(),
18347 api_key_required: false,
18348 display_name: "GET /users".to_string(),
18349 arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1".to_string(),
18350 },
18351 Route {
18352 route_id: "route2".to_string(),
18353 route_key: "POST /users".to_string(),
18354 target: "integrations/def".to_string(),
18355 authorization_type: "AWS_IAM".to_string(),
18356 api_key_required: true,
18357 display_name: "POST /users".to_string(),
18358 arn: "arn:aws:apigateway:us-east-1::/apis/test123/routes/route2".to_string(),
18359 },
18360 ];
18361
18362 app.apig_state.routes.selected = 0;
18364
18365 assert_eq!(
18367 app.apig_state.routes.items[0].arn,
18368 "arn:aws:apigateway:us-east-1::/apis/test123/routes/route1"
18369 );
18370 }
18371
18372 #[test]
18373 fn test_apig_yank_ignores_empty_arn() {
18374 use crate::apig::api::RestApi;
18375 use crate::apig::route::Route;
18376 use crate::ui::apig::ApiDetailTab;
18377
18378 let mut app = test_app();
18379 app.current_service = Service::ApiGatewayApis;
18380 app.apig_state.current_api = Some(RestApi {
18381 id: "test123".to_string(),
18382 name: "Test API".to_string(),
18383 description: "Test".to_string(),
18384 created_date: "2024-01-01".to_string(),
18385 api_key_source: "HEADER".to_string(),
18386 endpoint_configuration: "REGIONAL".to_string(),
18387 protocol_type: "HTTP".to_string(),
18388 disable_execute_api_endpoint: false,
18389 status: "AVAILABLE".to_string(),
18390 });
18391 app.apig_state.detail_tab = ApiDetailTab::Routes;
18392
18393 app.apig_state.routes.items = vec![Route {
18395 route_id: String::new(),
18396 route_key: "/users".to_string(),
18397 target: String::new(), authorization_type: String::new(),
18399 api_key_required: false,
18400 display_name: "/users".to_string(),
18401 arn: String::new(), }];
18403
18404 app.apig_state.routes.selected = 0;
18405
18406 assert!(
18408 app.apig_state.routes.items[0].arn.is_empty(),
18409 "Virtual parent should have empty ARN"
18410 );
18411 }
18412
18413 #[test]
18414 fn test_apig_route_column_toggle() {
18415 use crate::apig::api::RestApi;
18416 use crate::apig::route::Column as RouteColumn;
18417 use crate::keymap::Action;
18418 use crate::ui::apig::ApiDetailTab;
18419
18420 let mut app = test_app();
18421 app.current_service = Service::ApiGatewayApis;
18422 app.mode = Mode::ColumnSelector;
18423 app.apig_state.current_api = Some(RestApi {
18424 id: "test123".to_string(),
18425 name: "Test API".to_string(),
18426 description: "Test".to_string(),
18427 created_date: "2024-01-01".to_string(),
18428 api_key_source: "HEADER".to_string(),
18429 endpoint_configuration: "REGIONAL".to_string(),
18430 protocol_type: "HTTP".to_string(),
18431 disable_execute_api_endpoint: false,
18432 status: "AVAILABLE".to_string(),
18433 });
18434 app.apig_state.detail_tab = ApiDetailTab::Routes;
18435
18436 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
18438
18439 app.column_selector_index = 1;
18441 app.handle_action(Action::ToggleColumn);
18442 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
18443 assert!(app
18444 .apig_route_visible_column_ids
18445 .contains(&RouteColumn::RouteKey.id()));
18446
18447 app.column_selector_index = 3;
18449 app.handle_action(Action::ToggleColumn);
18450
18451 assert_eq!(app.apig_route_visible_column_ids.len(), 4);
18453 assert!(!app
18454 .apig_route_visible_column_ids
18455 .contains(&RouteColumn::Arn.id()));
18456
18457 app.handle_action(Action::ToggleColumn);
18459 assert_eq!(app.apig_route_visible_column_ids.len(), 5);
18460 assert!(app
18461 .apig_route_visible_column_ids
18462 .contains(&RouteColumn::Arn.id()));
18463 }
18464
18465 #[test]
18466 fn test_apig_resource_column_toggle() {
18467 use crate::apig::api::RestApi;
18468 use crate::apig::resource::Column as ResourceColumn;
18469 use crate::keymap::Action;
18470 use crate::ui::apig::ApiDetailTab;
18471
18472 let mut app = test_app();
18473 app.current_service = Service::ApiGatewayApis;
18474 app.mode = Mode::ColumnSelector;
18475 app.apig_state.current_api = Some(RestApi {
18476 id: "test123".to_string(),
18477 name: "Test API".to_string(),
18478 description: "Test".to_string(),
18479 created_date: "2024-01-01".to_string(),
18480 api_key_source: "HEADER".to_string(),
18481 endpoint_configuration: "REGIONAL".to_string(),
18482 protocol_type: "REST".to_string(), disable_execute_api_endpoint: false,
18484 status: "AVAILABLE".to_string(),
18485 });
18486 app.apig_state.detail_tab = ApiDetailTab::Routes; assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
18490
18491 app.column_selector_index = 1;
18493 app.handle_action(Action::ToggleColumn);
18494 assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
18495 assert!(app
18496 .apig_resource_visible_column_ids
18497 .contains(&ResourceColumn::Path.id()));
18498
18499 app.column_selector_index = 3;
18501 app.handle_action(Action::ToggleColumn);
18502
18503 assert_eq!(app.apig_resource_visible_column_ids.len(), 2);
18505 assert!(!app
18506 .apig_resource_visible_column_ids
18507 .contains(&ResourceColumn::Arn.id()));
18508
18509 app.handle_action(Action::ToggleColumn);
18511 assert_eq!(app.apig_resource_visible_column_ids.len(), 3);
18512 assert!(app
18513 .apig_resource_visible_column_ids
18514 .contains(&ResourceColumn::Arn.id()));
18515 }
18516
18517 #[test]
18518 fn test_cloudtrail_filter_input() {
18519 let mut app = App::new_without_client("default".to_string(), None);
18520 app.service_selected = true;
18521 app.current_service = Service::CloudTrailEvents;
18522
18523 app.handle_action(Action::StartFilter);
18524 assert_eq!(app.mode, Mode::FilterInput);
18525
18526 app.cloudtrail_state.table.filter = "test".to_string();
18527
18528 assert_eq!(app.cloudtrail_state.table.filter, "test");
18529 }
18530
18531 #[test]
18532 fn test_cloudtrail_row_expansion() {
18533 let mut app = App::new_without_client("default".to_string(), None);
18534 app.service_selected = true;
18535 app.current_service = Service::CloudTrailEvents;
18536 app.cloudtrail_state.table.items = vec![CloudTrailEvent {
18537 event_name: "Event1".to_string(),
18538 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18539 username: "user1".to_string(),
18540 event_source: "s3.amazonaws.com".to_string(),
18541 resource_type: "Bucket".to_string(),
18542 resource_name: "my-bucket".to_string(),
18543 read_only: "false".to_string(),
18544 aws_region: "us-east-1".to_string(),
18545 event_id: "abc123".to_string(),
18546 access_key_id: "AKIA...".to_string(),
18547 source_ip_address: "1.2.3.4".to_string(),
18548 error_code: "".to_string(),
18549 request_id: "req-123".to_string(),
18550 event_type: "AwsApiCall".to_string(),
18551 cloud_trail_event_json: "{}".to_string(),
18552 }];
18553
18554 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
18555
18556 app.expand_row();
18557 assert_eq!(app.cloudtrail_state.table.expanded_item, Some(0));
18558
18559 app.collapse_row();
18560 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
18561 }
18562
18563 #[test]
18564 fn test_cloudtrail_service_initialization() {
18565 let app = App::new_without_client("default".to_string(), None);
18566 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
18567 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
18568 }
18569
18570 #[test]
18571 fn test_cloudtrail_service_name() {
18572 assert_eq!(
18573 Service::CloudTrailEvents.name(),
18574 "CloudTrail › Event History"
18575 );
18576 }
18577
18578 #[test]
18579 fn test_cloudtrail_in_service_picker() {
18580 let app = App::new_without_client("default".to_string(), None);
18581 assert!(app
18582 .service_picker
18583 .services
18584 .contains(&"CloudTrail › Event History"));
18585 }
18586
18587 #[test]
18588 fn test_cloudtrail_service_selection() {
18589 let mut app = App::new_without_client("default".to_string(), None);
18590 app.current_service = Service::CloudTrailEvents;
18591 app.service_selected = true;
18592
18593 assert_eq!(app.current_service, Service::CloudTrailEvents);
18594 assert!(app.service_selected);
18595 }
18596
18597 #[test]
18598 fn test_cloudtrail_filter_resets_selection() {
18599 let mut app = App::new_without_client("default".to_string(), None);
18600 app.service_selected = true;
18601 app.current_service = Service::CloudTrailEvents;
18602 app.cloudtrail_state.table.selected = 5;
18603 app.cloudtrail_state.table.expanded_item = Some(3);
18604
18605 app.handle_action(Action::StartFilter);
18606 app.apply_filter_operation(|_| {});
18607
18608 assert_eq!(app.cloudtrail_state.table.selected, 0);
18609 assert_eq!(app.cloudtrail_state.table.expanded_item, None);
18610 }
18611
18612 #[test]
18613 fn test_cloudtrail_column_toggle() {
18614 let mut app = App::new_without_client("default".to_string(), None);
18615 app.service_selected = true;
18616 app.current_service = Service::CloudTrailEvents;
18617 app.mode = Mode::ColumnSelector;
18618
18619 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 6);
18621
18622 app.column_selector_index = 7;
18624 app.handle_action(Action::ToggleColumn);
18625
18626 assert_eq!(app.cloudtrail_event_visible_column_ids.len(), 7);
18628 }
18629
18630 #[test]
18631 fn test_cloudtrail_tab_cycles_filter_focus() {
18632 let mut app = App::new_without_client("default".to_string(), None);
18633 app.service_selected = true;
18634 app.current_service = Service::CloudTrailEvents;
18635 app.mode = Mode::FilterInput;
18636 app.cloudtrail_state.input_focus = InputFocus::Filter;
18637
18638 app.handle_action(Action::NextFilterFocus);
18640 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
18641
18642 app.handle_action(Action::NextFilterFocus);
18644 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
18645 }
18646
18647 #[test]
18648 fn test_cloudtrail_shift_tab_cycles_filter_focus() {
18649 let mut app = App::new_without_client("default".to_string(), None);
18650 app.service_selected = true;
18651 app.current_service = Service::CloudTrailEvents;
18652 app.mode = Mode::FilterInput;
18653 app.cloudtrail_state.input_focus = InputFocus::Filter;
18654
18655 app.handle_action(Action::PrevFilterFocus);
18657 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Pagination);
18658
18659 app.handle_action(Action::PrevFilterFocus);
18661 assert_eq!(app.cloudtrail_state.input_focus, InputFocus::Filter);
18662 }
18663
18664 #[test]
18665 fn test_cloudtrail_detail_view_tab_cycles_focus() {
18666 let mut app = App::new_without_client("default".to_string(), None);
18667 app.service_selected = true;
18668 app.current_service = Service::CloudTrailEvents;
18669 app.mode = Mode::Normal;
18670 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18671 event_name: "PutObject".to_string(),
18672 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18673 username: "user".to_string(),
18674 event_source: "s3.amazonaws.com".to_string(),
18675 resource_type: "AWS::S3::Bucket".to_string(),
18676 resource_name: "my-bucket".to_string(),
18677 read_only: "false".to_string(),
18678 aws_region: "us-east-1".to_string(),
18679 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18680 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18681 source_ip_address: "192.0.2.1".to_string(),
18682 error_code: "".to_string(),
18683 request_id: "req-123".to_string(),
18684 event_type: "AwsApiCall".to_string(),
18685 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18686 });
18687
18688 assert_eq!(
18690 app.cloudtrail_state.detail_focus,
18691 CloudTrailDetailFocus::Resources
18692 );
18693
18694 app.handle_action(Action::NextDetailTab);
18696 assert_eq!(
18697 app.cloudtrail_state.detail_focus,
18698 CloudTrailDetailFocus::EventRecord
18699 );
18700
18701 app.handle_action(Action::NextDetailTab);
18703 assert_eq!(
18704 app.cloudtrail_state.detail_focus,
18705 CloudTrailDetailFocus::Resources
18706 );
18707 }
18708
18709 #[test]
18710 fn test_cloudtrail_detail_view_shift_tab_cycles_focus() {
18711 let mut app = App::new_without_client("default".to_string(), None);
18712 app.service_selected = true;
18713 app.current_service = Service::CloudTrailEvents;
18714 app.mode = Mode::Normal;
18715 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18716 event_name: "PutObject".to_string(),
18717 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18718 username: "user".to_string(),
18719 event_source: "s3.amazonaws.com".to_string(),
18720 resource_type: "AWS::S3::Bucket".to_string(),
18721 resource_name: "my-bucket".to_string(),
18722 read_only: "false".to_string(),
18723 aws_region: "us-east-1".to_string(),
18724 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18725 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18726 source_ip_address: "192.0.2.1".to_string(),
18727 error_code: "".to_string(),
18728 request_id: "req-123".to_string(),
18729 event_type: "AwsApiCall".to_string(),
18730 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18731 });
18732
18733 assert_eq!(
18735 app.cloudtrail_state.detail_focus,
18736 CloudTrailDetailFocus::Resources
18737 );
18738
18739 app.handle_action(Action::PrevDetailTab);
18741 assert_eq!(
18742 app.cloudtrail_state.detail_focus,
18743 CloudTrailDetailFocus::EventRecord
18744 );
18745
18746 app.handle_action(Action::PrevDetailTab);
18748 assert_eq!(
18749 app.cloudtrail_state.detail_focus,
18750 CloudTrailDetailFocus::Resources
18751 );
18752 }
18753
18754 #[test]
18755 fn test_cloudtrail_json_scroll_with_arrow_keys() {
18756 let mut app = App::new_without_client("default".to_string(), None);
18757 app.service_selected = true;
18758 app.current_service = Service::CloudTrailEvents;
18759 app.mode = Mode::Normal;
18760 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18761 event_name: "PutObject".to_string(),
18762 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18763 username: "user".to_string(),
18764 event_source: "s3.amazonaws.com".to_string(),
18765 resource_type: "AWS::S3::Bucket".to_string(),
18766 resource_name: "my-bucket".to_string(),
18767 read_only: "false".to_string(),
18768 aws_region: "us-east-1".to_string(),
18769 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18770 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18771 source_ip_address: "192.0.2.1".to_string(),
18772 error_code: "".to_string(),
18773 request_id: "req-123".to_string(),
18774 event_type: "AwsApiCall".to_string(),
18775 cloud_trail_event_json: (0..50)
18776 .map(|i| format!("line {}", i))
18777 .collect::<Vec<_>>()
18778 .join("\n"),
18779 });
18780 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
18781 app.cloudtrail_state.event_json_scroll = 0;
18782
18783 app.handle_action(Action::NextItem);
18785 assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
18786
18787 app.handle_action(Action::NextItem);
18788 assert_eq!(app.cloudtrail_state.event_json_scroll, 2);
18789
18790 app.handle_action(Action::PrevItem);
18792 assert_eq!(app.cloudtrail_state.event_json_scroll, 1);
18793
18794 app.handle_action(Action::PrevItem);
18795 assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
18796
18797 app.handle_action(Action::PrevItem);
18799 assert_eq!(app.cloudtrail_state.event_json_scroll, 0);
18800 }
18801
18802 #[test]
18803 fn test_cloudtrail_tab_works_with_no_resources() {
18804 let mut app = App::new_without_client("default".to_string(), None);
18805 app.service_selected = true;
18806 app.current_service = Service::CloudTrailEvents;
18807 app.mode = Mode::Normal;
18808 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18809 event_name: "PutObject".to_string(),
18810 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18811 username: "user".to_string(),
18812 event_source: "s3.amazonaws.com".to_string(),
18813 resource_type: "".to_string(), resource_name: "".to_string(), read_only: "false".to_string(),
18816 aws_region: "us-east-1".to_string(),
18817 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18818 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18819 source_ip_address: "192.0.2.1".to_string(),
18820 error_code: "".to_string(),
18821 request_id: "req-123".to_string(),
18822 event_type: "AwsApiCall".to_string(),
18823 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18824 });
18825 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
18826
18827 app.handle_action(Action::NextDetailTab);
18829 assert_eq!(
18830 app.cloudtrail_state.detail_focus,
18831 CloudTrailDetailFocus::Resources
18832 );
18833
18834 app.handle_action(Action::NextDetailTab);
18836 assert_eq!(
18837 app.cloudtrail_state.detail_focus,
18838 CloudTrailDetailFocus::EventRecord
18839 );
18840 }
18841
18842 #[test]
18843 fn test_cloudtrail_resources_expand_collapse() {
18844 let mut app = App::new_without_client("default".to_string(), None);
18845 app.service_selected = true;
18846 app.current_service = Service::CloudTrailEvents;
18847 app.mode = Mode::Normal;
18848 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18849 event_name: "PutObject".to_string(),
18850 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18851 username: "user".to_string(),
18852 event_source: "s3.amazonaws.com".to_string(),
18853 resource_type: "AWS::S3::Bucket".to_string(),
18854 resource_name: "my-bucket".to_string(),
18855 read_only: "false".to_string(),
18856 aws_region: "us-east-1".to_string(),
18857 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18858 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18859 source_ip_address: "192.0.2.1".to_string(),
18860 error_code: "".to_string(),
18861 request_id: "req-123".to_string(),
18862 event_type: "AwsApiCall".to_string(),
18863 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18864 });
18865 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
18866 app.cloudtrail_state.resources_expanded_index = None;
18867
18868 app.handle_action(Action::ExpandRow);
18870 assert_eq!(app.cloudtrail_state.resources_expanded_index, Some(0));
18871
18872 app.handle_action(Action::CollapseRow);
18874 assert_eq!(app.cloudtrail_state.resources_expanded_index, None);
18875 }
18876
18877 #[test]
18878 fn test_cloudtrail_event_json_no_column_selector() {
18879 let mut app = App::new_without_client("default".to_string(), None);
18880 app.service_selected = true;
18881 app.current_service = Service::CloudTrailEvents;
18882 app.mode = Mode::Normal;
18883 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18884 event_name: "PutObject".to_string(),
18885 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18886 username: "user".to_string(),
18887 event_source: "s3.amazonaws.com".to_string(),
18888 resource_type: "AWS::S3::Bucket".to_string(),
18889 resource_name: "my-bucket".to_string(),
18890 read_only: "false".to_string(),
18891 aws_region: "us-east-1".to_string(),
18892 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18893 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18894 source_ip_address: "192.0.2.1".to_string(),
18895 error_code: "".to_string(),
18896 request_id: "req-123".to_string(),
18897 event_type: "AwsApiCall".to_string(),
18898 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18899 });
18900 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::EventRecord;
18901
18902 app.handle_action(Action::OpenColumnSelector);
18904 assert_eq!(app.mode, Mode::Normal);
18905
18906 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
18908 app.handle_action(Action::OpenColumnSelector);
18909 assert_eq!(app.mode, Mode::ColumnSelector);
18910 }
18911
18912 #[test]
18913 fn test_cloudtrail_resources_preferences_show_only_3_columns() {
18914 let mut app = App::new_without_client("default".to_string(), None);
18915 app.service_selected = true;
18916 app.current_service = Service::CloudTrailEvents;
18917 app.mode = Mode::ColumnSelector;
18918 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18919 event_name: "PutObject".to_string(),
18920 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18921 username: "user".to_string(),
18922 event_source: "s3.amazonaws.com".to_string(),
18923 resource_type: "AWS::S3::Bucket".to_string(),
18924 resource_name: "my-bucket".to_string(),
18925 read_only: "false".to_string(),
18926 aws_region: "us-east-1".to_string(),
18927 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18928 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18929 source_ip_address: "192.0.2.1".to_string(),
18930 error_code: "".to_string(),
18931 request_id: "req-123".to_string(),
18932 event_type: "AwsApiCall".to_string(),
18933 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18934 });
18935 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
18936
18937 assert_eq!(app.get_column_count(), 3);
18939 assert_eq!(app.get_column_selector_max(), 3);
18940
18941 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 3);
18943
18944 app.column_selector_index = 1;
18946 app.handle_action(Action::ToggleColumn);
18947 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
18948
18949 app.column_selector_index = 2;
18951 app.handle_action(Action::ToggleColumn);
18952 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
18953
18954 app.column_selector_index = 3;
18956 app.handle_action(Action::ToggleColumn);
18957 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 1);
18958
18959 app.column_selector_index = 1;
18961 app.handle_action(Action::ToggleColumn);
18962 assert_eq!(app.cloudtrail_resource_visible_column_ids.len(), 2);
18963 }
18964
18965 #[test]
18966 fn test_cloudtrail_resources_preferences_tab_cycles() {
18967 let mut app = App::new_without_client("default".to_string(), None);
18968 app.service_selected = true;
18969 app.current_service = Service::CloudTrailEvents;
18970 app.mode = Mode::ColumnSelector;
18971 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
18972 event_name: "PutObject".to_string(),
18973 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
18974 username: "user".to_string(),
18975 event_source: "s3.amazonaws.com".to_string(),
18976 resource_type: "AWS::S3::Bucket".to_string(),
18977 resource_name: "my-bucket".to_string(),
18978 read_only: "false".to_string(),
18979 aws_region: "us-east-1".to_string(),
18980 event_id: "90c72977-31e0-4079-9a74-ee25e5d7aadf".to_string(),
18981 access_key_id: "AKIAIOSFODNN7EXAMPLE".to_string(),
18982 source_ip_address: "192.0.2.1".to_string(),
18983 error_code: "".to_string(),
18984 request_id: "req-123".to_string(),
18985 event_type: "AwsApiCall".to_string(),
18986 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
18987 });
18988 app.cloudtrail_state.detail_focus = CloudTrailDetailFocus::Resources;
18989 app.column_selector_index = 1;
18990
18991 app.handle_action(Action::NextPreferences);
18993 assert_eq!(app.column_selector_index, 0);
18994
18995 app.column_selector_index = 1;
18997 app.handle_action(Action::PrevPreferences);
18998 assert_eq!(app.column_selector_index, 0);
18999 }
19000
19001 #[test]
19002 fn test_cloudtrail_resources_height_stays_constant_when_expanding() {
19003 let visible_cols_3 = 3;
19011 let height_3 = (1 + visible_cols_3 - 1 + 1 + 2 + 1) as u16;
19012 assert_eq!(height_3, 7);
19013
19014 let visible_cols_2 = 2;
19016 let height_2 = (1 + visible_cols_2 - 1 + 1 + 2 + 1) as u16;
19017 assert_eq!(height_2, 6);
19018
19019 let visible_cols_1 = 1;
19021 let height_1 = (1 + visible_cols_1 - 1 + 1 + 2 + 1) as u16;
19022 assert_eq!(height_1, 5);
19023 }
19024
19025 #[test]
19026 fn test_cloudtrail_default_focus_is_resources() {
19027 let app = App::new_without_client("default".to_string(), None);
19028 assert_eq!(
19029 app.cloudtrail_state.detail_focus,
19030 CloudTrailDetailFocus::Resources
19031 );
19032 }
19033
19034 fn setup_cloudtrail_pagination_test() -> App {
19035 let mut app = App::new_without_client("default".to_string(), None);
19036 app.service_selected = true;
19037 app.current_service = Service::CloudTrailEvents;
19038 app.mode = Mode::FilterInput;
19039 app.cloudtrail_state.input_focus = InputFocus::Pagination;
19040 app.cloudtrail_state.table.page_size = PageSize::Ten;
19041 app.cloudtrail_state.table.items = (0..25)
19042 .map(|i| CloudTrailEvent {
19043 event_name: format!("Event{}", i),
19044 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19045 username: "user".to_string(),
19046 event_source: "s3.amazonaws.com".to_string(),
19047 resource_type: "Bucket".to_string(),
19048 resource_name: "bucket".to_string(),
19049 read_only: "false".to_string(),
19050 aws_region: "us-east-1".to_string(),
19051 event_id: "id".to_string(),
19052 access_key_id: "key".to_string(),
19053 source_ip_address: "1.2.3.4".to_string(),
19054 error_code: "".to_string(),
19055 request_id: "req".to_string(),
19056 event_type: "AwsApiCall".to_string(),
19057 cloud_trail_event_json: "{}".to_string(),
19058 })
19059 .collect();
19060 app
19061 }
19062
19063 #[test]
19064 fn test_cloudtrail_pagination_navigation() {
19065 let mut app = setup_cloudtrail_pagination_test();
19067 assert_eq!(app.cloudtrail_state.table.selected, 0);
19068 app.handle_action(Action::NextItem);
19069 assert_eq!(app.cloudtrail_state.table.selected, 10);
19070 app.handle_action(Action::NextItem);
19071 assert_eq!(app.cloudtrail_state.table.selected, 20);
19072 app.handle_action(Action::NextItem);
19073 assert_eq!(app.cloudtrail_state.table.selected, 20);
19074
19075 app.handle_action(Action::PrevItem);
19077 assert_eq!(app.cloudtrail_state.table.selected, 10);
19078 app.handle_action(Action::PrevItem);
19079 assert_eq!(app.cloudtrail_state.table.selected, 0);
19080 app.handle_action(Action::PrevItem);
19081 assert_eq!(app.cloudtrail_state.table.selected, 0);
19082 }
19083
19084 #[test]
19085 fn test_cloudtrail_pagination_navigation_right_arrow() {
19086 let mut app = setup_cloudtrail_pagination_test();
19087 app.current_service = Service::CloudTrailEvents;
19088 app.mode = Mode::FilterInput;
19089 app.cloudtrail_state.input_focus = InputFocus::Pagination;
19090 app.cloudtrail_state.table.page_size = PageSize::Ten;
19091
19092 app.cloudtrail_state.table.items = (0..25)
19094 .map(|i| CloudTrailEvent {
19095 event_name: format!("Event{}", i),
19096 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19097 username: "user".to_string(),
19098 event_source: "s3.amazonaws.com".to_string(),
19099 resource_type: "Bucket".to_string(),
19100 resource_name: "bucket".to_string(),
19101 read_only: "false".to_string(),
19102 aws_region: "us-east-1".to_string(),
19103 event_id: "id".to_string(),
19104 access_key_id: "key".to_string(),
19105 source_ip_address: "1.2.3.4".to_string(),
19106 error_code: "".to_string(),
19107 request_id: "req".to_string(),
19108 event_type: "AwsApiCall".to_string(),
19109 cloud_trail_event_json: "{}".to_string(),
19110 })
19111 .collect();
19112
19113 assert_eq!(app.cloudtrail_state.table.selected, 0);
19115
19116 app.handle_action(Action::NextItem);
19118 assert_eq!(app.cloudtrail_state.table.selected, 10);
19119
19120 app.handle_action(Action::NextItem);
19122 assert_eq!(app.cloudtrail_state.table.selected, 20);
19123
19124 app.handle_action(Action::NextItem);
19126 assert_eq!(app.cloudtrail_state.table.selected, 20);
19127 }
19128
19129 #[test]
19130 fn test_cloudtrail_pagination_navigation_left_arrow() {
19131 let mut app = App::new_without_client("default".to_string(), None);
19132 app.service_selected = true;
19133 app.current_service = Service::CloudTrailEvents;
19134 app.mode = Mode::FilterInput;
19135 app.cloudtrail_state.input_focus = InputFocus::Pagination;
19136 app.cloudtrail_state.table.page_size = PageSize::Ten;
19137
19138 app.cloudtrail_state.table.items = (0..25)
19140 .map(|i| CloudTrailEvent {
19141 event_name: format!("Event{}", i),
19142 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19143 username: "user".to_string(),
19144 event_source: "s3.amazonaws.com".to_string(),
19145 resource_type: "Bucket".to_string(),
19146 resource_name: "bucket".to_string(),
19147 read_only: "false".to_string(),
19148 aws_region: "us-east-1".to_string(),
19149 event_id: "id".to_string(),
19150 access_key_id: "key".to_string(),
19151 source_ip_address: "1.2.3.4".to_string(),
19152 error_code: "".to_string(),
19153 request_id: "req".to_string(),
19154 event_type: "AwsApiCall".to_string(),
19155 cloud_trail_event_json: "{}".to_string(),
19156 })
19157 .collect();
19158
19159 app.cloudtrail_state.table.selected = 20;
19161
19162 app.handle_action(Action::PrevItem);
19164 assert_eq!(app.cloudtrail_state.table.selected, 10);
19165
19166 app.handle_action(Action::PrevItem);
19168 assert_eq!(app.cloudtrail_state.table.selected, 0);
19169
19170 app.handle_action(Action::PrevItem);
19172 assert_eq!(app.cloudtrail_state.table.selected, 0);
19173 }
19174
19175 #[test]
19176 fn test_cloudtrail_arrow_navigation() {
19177 let mut app = App::new_without_client("default".to_string(), None);
19178 app.service_selected = true;
19179 app.current_service = Service::CloudTrailEvents;
19180 app.mode = Mode::Normal;
19181
19182 app.cloudtrail_state.table.items = (0..5)
19184 .map(|i| CloudTrailEvent {
19185 event_name: format!("Event{}", i),
19186 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19187 username: "user".to_string(),
19188 event_source: "s3.amazonaws.com".to_string(),
19189 resource_type: "Bucket".to_string(),
19190 resource_name: "bucket".to_string(),
19191 read_only: "false".to_string(),
19192 aws_region: "us-east-1".to_string(),
19193 event_id: "id".to_string(),
19194 access_key_id: "key".to_string(),
19195 source_ip_address: "1.2.3.4".to_string(),
19196 error_code: "".to_string(),
19197 request_id: "req".to_string(),
19198 event_type: "AwsApiCall".to_string(),
19199 cloud_trail_event_json: "{}".to_string(),
19200 })
19201 .collect();
19202
19203 app.handle_action(Action::NextItem);
19205 assert_eq!(app.cloudtrail_state.table.selected, 1);
19206
19207 app.handle_action(Action::NextItem);
19208 assert_eq!(app.cloudtrail_state.table.selected, 2);
19209
19210 app.handle_action(Action::PrevItem);
19212 assert_eq!(app.cloudtrail_state.table.selected, 1);
19213 }
19214
19215 #[test]
19216 fn test_cloudtrail_page_down_navigation() {
19217 let mut app = App::new_without_client("default".to_string(), None);
19218 app.service_selected = true;
19219 app.current_service = Service::CloudTrailEvents;
19220 app.mode = Mode::Normal;
19221 app.cloudtrail_state.table.page_size = PageSize::Ten;
19222
19223 app.cloudtrail_state.table.items = (0..25)
19225 .map(|i| CloudTrailEvent {
19226 event_name: format!("Event{}", i),
19227 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19228 username: "user".to_string(),
19229 event_source: "s3.amazonaws.com".to_string(),
19230 resource_type: "Bucket".to_string(),
19231 resource_name: "bucket".to_string(),
19232 read_only: "false".to_string(),
19233 aws_region: "us-east-1".to_string(),
19234 event_id: "id".to_string(),
19235 access_key_id: "key".to_string(),
19236 source_ip_address: "1.2.3.4".to_string(),
19237 error_code: "".to_string(),
19238 request_id: "req".to_string(),
19239 event_type: "AwsApiCall".to_string(),
19240 cloud_trail_event_json: "{}".to_string(),
19241 })
19242 .collect();
19243
19244 app.handle_action(Action::PageDown);
19246 assert_eq!(app.cloudtrail_state.table.selected, 10);
19247
19248 app.handle_action(Action::PageDown);
19249 assert_eq!(app.cloudtrail_state.table.selected, 20);
19250 }
19251
19252 #[test]
19253 fn test_cloudtrail_page_up_navigation() {
19254 let mut app = App::new_without_client("default".to_string(), None);
19255 app.service_selected = true;
19256 app.current_service = Service::CloudTrailEvents;
19257 app.mode = Mode::Normal;
19258 app.cloudtrail_state.table.page_size = PageSize::Ten;
19259
19260 app.cloudtrail_state.table.items = (0..25)
19262 .map(|i| CloudTrailEvent {
19263 event_name: format!("Event{}", i),
19264 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19265 username: "user".to_string(),
19266 event_source: "s3.amazonaws.com".to_string(),
19267 resource_type: "Bucket".to_string(),
19268 resource_name: "bucket".to_string(),
19269 read_only: "false".to_string(),
19270 aws_region: "us-east-1".to_string(),
19271 event_id: "id".to_string(),
19272 access_key_id: "key".to_string(),
19273 source_ip_address: "1.2.3.4".to_string(),
19274 error_code: "".to_string(),
19275 request_id: "req".to_string(),
19276 event_type: "AwsApiCall".to_string(),
19277 cloud_trail_event_json: "{}".to_string(),
19278 })
19279 .collect();
19280
19281 app.cloudtrail_state.table.selected = 20;
19283
19284 app.handle_action(Action::PageUp);
19286 assert_eq!(app.cloudtrail_state.table.selected, 10);
19287
19288 app.handle_action(Action::PageUp);
19289 assert_eq!(app.cloudtrail_state.table.selected, 0);
19290 }
19291
19292 #[test]
19293 fn test_cloudtrail_page_size_change_updates_display() {
19294 let mut app = App::new_without_client("default".to_string(), None);
19295 app.service_selected = true;
19296 app.current_service = Service::CloudTrailEvents;
19297 app.mode = Mode::ColumnSelector;
19298
19299 app.cloudtrail_state.table.items = (0..50)
19301 .map(|i| CloudTrailEvent {
19302 event_name: format!("Event{}", i),
19303 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19304 username: "user".to_string(),
19305 event_source: "s3.amazonaws.com".to_string(),
19306 resource_type: "Bucket".to_string(),
19307 resource_name: "bucket".to_string(),
19308 read_only: "false".to_string(),
19309 aws_region: "us-east-1".to_string(),
19310 event_id: "id".to_string(),
19311 access_key_id: "key".to_string(),
19312 source_ip_address: "1.2.3.4".to_string(),
19313 error_code: "".to_string(),
19314 request_id: "req".to_string(),
19315 event_type: "AwsApiCall".to_string(),
19316 cloud_trail_event_json: "{}".to_string(),
19317 })
19318 .collect();
19319
19320 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Fifty);
19322
19323 app.column_selector_index = 17;
19325 app.handle_action(Action::ToggleColumn);
19326
19327 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
19328 }
19330
19331 #[test]
19332 fn test_cloudtrail_all_columns_toggleable() {
19333 let mut app = App::new_without_client("default".to_string(), None);
19334 app.service_selected = true;
19335 app.current_service = Service::CloudTrailEvents;
19336 app.mode = Mode::ColumnSelector;
19337
19338 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
19340
19341 for idx in 1..=14 {
19344 app.column_selector_index = idx;
19345 let initial_visible = app.cloudtrail_event_visible_column_ids.clone();
19346 let initial_count = initial_visible.len();
19347
19348 let col = app.cloudtrail_event_column_ids.get(idx - 1).unwrap();
19350 let is_visible = initial_visible.contains(col);
19351
19352 app.handle_action(Action::ToggleColumn);
19353
19354 if is_visible && initial_count == 1 {
19356 assert_eq!(
19357 app.cloudtrail_event_visible_column_ids, initial_visible,
19358 "Last visible column at index {} should not be toggleable",
19359 idx
19360 );
19361 } else {
19362 assert_ne!(
19364 app.cloudtrail_event_visible_column_ids, initial_visible,
19365 "Column at index {} should be toggleable when not the last one",
19366 idx
19367 );
19368 }
19369 }
19370 }
19371
19372 #[test]
19373 fn test_cloudtrail_readonly_column_toggleable() {
19374 use crate::cloudtrail::events::CloudTrailEventColumn;
19375
19376 let mut app = App::new_without_client("default".to_string(), None);
19377 app.service_selected = true;
19378 app.current_service = Service::CloudTrailEvents;
19379 app.mode = Mode::ColumnSelector;
19380
19381 let readonly_id = CloudTrailEventColumn::ReadOnly.id();
19384
19385 assert!(app.cloudtrail_event_column_ids.contains(&readonly_id));
19387
19388 assert!(!app
19390 .cloudtrail_event_visible_column_ids
19391 .contains(&readonly_id));
19392
19393 app.column_selector_index = 7;
19395 app.handle_action(Action::ToggleColumn);
19396
19397 assert!(
19399 app.cloudtrail_event_visible_column_ids
19400 .contains(&readonly_id),
19401 "ReadOnly column should be toggleable at index 7"
19402 );
19403
19404 app.handle_action(Action::ToggleColumn);
19406
19407 assert!(!app
19409 .cloudtrail_event_visible_column_ids
19410 .contains(&readonly_id));
19411 }
19412
19413 #[test]
19414 fn test_cloudtrail_pagination_limits_displayed_items() {
19415 let mut app = App::new_without_client("default".to_string(), None);
19416 app.service_selected = true;
19417 app.current_service = Service::CloudTrailEvents;
19418
19419 app.cloudtrail_state.table.items = (0..50)
19421 .map(|i| CloudTrailEvent {
19422 event_name: format!("Event{}", i),
19423 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19424 username: "user".to_string(),
19425 event_source: "s3.amazonaws.com".to_string(),
19426 resource_type: "Bucket".to_string(),
19427 resource_name: "bucket".to_string(),
19428 read_only: "false".to_string(),
19429 aws_region: "us-east-1".to_string(),
19430 event_id: "id".to_string(),
19431 access_key_id: "key".to_string(),
19432 source_ip_address: "1.2.3.4".to_string(),
19433 error_code: "".to_string(),
19434 request_id: "req".to_string(),
19435 event_type: "AwsApiCall".to_string(),
19436 cloud_trail_event_json: "{}".to_string(),
19437 })
19438 .collect();
19439
19440 app.cloudtrail_state.table.page_size = PageSize::Ten;
19442
19443 use ratatui::backend::TestBackend;
19445 use ratatui::Terminal;
19446
19447 let backend = TestBackend::new(100, 30);
19448 let mut terminal = Terminal::new(backend).unwrap();
19449
19450 terminal
19451 .draw(|frame| {
19452 let area = frame.area();
19453 crate::ui::cloudtrail::render_events(frame, &app, area);
19454 })
19455 .unwrap();
19456
19457 assert_eq!(app.cloudtrail_state.table.page_size, PageSize::Ten);
19460 }
19461
19462 #[test]
19463 fn test_cloudtrail_readonly_column_selectable_in_preferences() {
19464 let mut app = App::new_without_client("default".to_string(), None);
19465 app.service_selected = true;
19466 app.current_service = Service::CloudTrailEvents;
19467 app.mode = Mode::ColumnSelector;
19468
19469 assert_eq!(app.cloudtrail_event_column_ids.len(), 14);
19471
19472 app.column_selector_index = 7;
19474
19475 let max_index = app.get_column_selector_max();
19477 assert!(
19478 app.column_selector_index <= max_index,
19479 "Read-only column index {} should be <= max {}",
19480 app.column_selector_index,
19481 max_index
19482 );
19483
19484 let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
19486 let col = CloudTrailEventColumn::from_id(col_id);
19487 assert!(col.is_some(), "Column should exist at index 7");
19488 assert_eq!(
19489 col.unwrap().default_name(),
19490 "Read-only",
19491 "Column at index 7 should be Read-only"
19492 );
19493 }
19494
19495 #[test]
19496 fn test_cloudtrail_navigate_to_readonly_column() {
19497 let mut app = App::new_without_client("default".to_string(), None);
19498 app.service_selected = true;
19499 app.current_service = Service::CloudTrailEvents;
19500 app.mode = Mode::ColumnSelector;
19501 app.column_selector_index = 1; println!(
19505 "cloudtrail_event_column_ids.len() = {}",
19506 app.cloudtrail_event_column_ids.len()
19507 );
19508 assert_eq!(
19509 app.cloudtrail_event_column_ids.len(),
19510 14,
19511 "Should have 14 columns"
19512 );
19513
19514 let column_count = app.get_column_count();
19516 println!("Column count from get_column_count(): {}", column_count);
19517 assert_eq!(column_count, 14, "Column count should be 14");
19518
19519 assert!(!app.is_blank_row_index(7), "Index 7 should NOT be blank");
19521 assert!(app.is_blank_row_index(15), "Index 15 should be blank");
19522
19523 for _ in 0..6 {
19525 app.handle_action(Action::NextItem);
19526 }
19527
19528 assert_eq!(
19529 app.column_selector_index, 7,
19530 "Should navigate to Read-only column at index 7"
19531 );
19532
19533 let col_id = &app.cloudtrail_event_column_ids[app.column_selector_index - 1];
19535 let col = CloudTrailEventColumn::from_id(col_id).unwrap();
19536 assert_eq!(col.default_name(), "Read-only");
19537 }
19538
19539 #[test]
19540 fn test_cloudtrail_navigate_beyond_loaded_items() {
19541 let mut app = App::new_without_client("default".to_string(), None);
19542 app.service_selected = true;
19543 app.current_service = Service::CloudTrailEvents;
19544
19545 app.cloudtrail_state.table.items = (0..50)
19547 .map(|i| CloudTrailEvent {
19548 event_name: format!("Event{}", i),
19549 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19550 username: "user".to_string(),
19551 event_source: "s3.amazonaws.com".to_string(),
19552 resource_type: "Bucket".to_string(),
19553 resource_name: "bucket".to_string(),
19554 read_only: "false".to_string(),
19555 aws_region: "us-east-1".to_string(),
19556 event_id: "id".to_string(),
19557 access_key_id: "key".to_string(),
19558 source_ip_address: "1.2.3.4".to_string(),
19559 error_code: "".to_string(),
19560 request_id: "req".to_string(),
19561 event_type: "AwsApiCall".to_string(),
19562 cloud_trail_event_json: "{}".to_string(),
19563 })
19564 .collect();
19565
19566 let initial_selected = app.cloudtrail_state.table.selected;
19569 app.page_input = "10".to_string();
19570 app.go_to_page(10);
19571
19572 assert_eq!(
19574 app.cloudtrail_state.table.selected, initial_selected,
19575 "Should ignore navigation to page 10 when only 1 page is loaded"
19576 );
19577
19578 app.go_to_page(2);
19580 let page_size = app.cloudtrail_state.table.page_size.value();
19581 assert_eq!(
19582 app.cloudtrail_state.table.selected, page_size,
19583 "Should allow navigation to page 2 (loaded + 1)"
19584 );
19585 }
19586
19587 #[test]
19588 fn test_cloudtrail_page_change_resets_expansion() {
19589 let mut app = App::new_without_client("default".to_string(), None);
19590 app.service_selected = true;
19591 app.current_service = Service::CloudTrailEvents;
19592
19593 app.cloudtrail_state.table.items = (0..100)
19595 .map(|i| CloudTrailEvent {
19596 event_name: format!("Event{}", i),
19597 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19598 username: "user".to_string(),
19599 event_source: "s3.amazonaws.com".to_string(),
19600 resource_type: "Bucket".to_string(),
19601 resource_name: "bucket".to_string(),
19602 read_only: "false".to_string(),
19603 aws_region: "us-east-1".to_string(),
19604 event_id: "id".to_string(),
19605 access_key_id: "key".to_string(),
19606 source_ip_address: "1.2.3.4".to_string(),
19607 error_code: "".to_string(),
19608 request_id: "req".to_string(),
19609 event_type: "AwsApiCall".to_string(),
19610 cloud_trail_event_json: "{}".to_string(),
19611 })
19612 .collect();
19613
19614 app.cloudtrail_state.table.expanded_item = Some(5);
19616 assert_eq!(app.cloudtrail_state.table.expanded_item, Some(5));
19617
19618 app.go_to_page(2);
19620
19621 assert_eq!(
19623 app.cloudtrail_state.table.expanded_item, None,
19624 "Page change should reset expanded_item"
19625 );
19626 }
19627
19628 #[test]
19629 fn test_cloudtrail_pagination_right_left_arrow_works_when_filter_focused() {
19630 let mut app = App::new_without_client("default".to_string(), None);
19634 app.service_selected = true;
19635 app.current_service = Service::CloudTrailEvents;
19636 app.mode = Mode::FilterInput;
19637 app.cloudtrail_state.input_focus = InputFocus::Filter;
19639 app.cloudtrail_state.table.page_size = PageSize::Ten;
19640
19641 app.cloudtrail_state.table.items = (0..25)
19642 .map(|i| crate::cloudtrail::CloudTrailEvent {
19643 event_name: format!("Event{}", i),
19644 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19645 username: "user".to_string(),
19646 event_source: "s3.amazonaws.com".to_string(),
19647 resource_type: "Bucket".to_string(),
19648 resource_name: "bucket".to_string(),
19649 read_only: "false".to_string(),
19650 aws_region: "us-east-1".to_string(),
19651 event_id: format!("id-{}", i),
19652 access_key_id: "key".to_string(),
19653 source_ip_address: "1.2.3.4".to_string(),
19654 error_code: String::new(),
19655 request_id: "req".to_string(),
19656 event_type: "AwsApiCall".to_string(),
19657 cloud_trail_event_json: "{}".to_string(),
19658 })
19659 .collect();
19660
19661 assert_eq!(app.cloudtrail_state.table.selected, 0, "starts on page 1");
19662
19663 app.handle_action(Action::PageDown);
19665 assert_eq!(
19666 app.cloudtrail_state.table.selected, 10,
19667 "Right arrow must advance to page 2 even when filter text box is focused"
19668 );
19669
19670 app.handle_action(Action::PageUp);
19672 assert_eq!(
19673 app.cloudtrail_state.table.selected, 0,
19674 "Left arrow must go back to page 1 even when filter text box is focused"
19675 );
19676 }
19677
19678 #[test]
19679 fn test_cloudtrail_yank_in_event_detail_copies_json() {
19680 let mut app = test_app();
19682 app.current_service = Service::CloudTrailEvents;
19683 app.service_selected = true;
19684 app.cloudtrail_state.current_event = Some(CloudTrailEvent {
19685 event_name: "PutObject".to_string(),
19686 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19687 username: "alice".to_string(),
19688 event_source: "s3.amazonaws.com".to_string(),
19689 resource_type: "Bucket".to_string(),
19690 resource_name: "my-bucket".to_string(),
19691 read_only: "false".to_string(),
19692 aws_region: "us-east-1".to_string(),
19693 event_id: "evt-001".to_string(),
19694 access_key_id: "AKIA".to_string(),
19695 source_ip_address: "1.2.3.4".to_string(),
19696 error_code: String::new(),
19697 request_id: "req-001".to_string(),
19698 event_type: "AwsApiCall".to_string(),
19699 cloud_trail_event_json: r#"{"eventName":"PutObject"}"#.to_string(),
19700 });
19701
19702 app.handle_action(Action::Yank);
19705 assert!(
19707 app.cloudtrail_state.current_event.is_some(),
19708 "Event must remain open after yank"
19709 );
19710 }
19711
19712 #[test]
19713 fn test_cloudtrail_filter_reduces_visible_events() {
19714 let mut app = test_app();
19719 app.current_service = Service::CloudTrailEvents;
19720 app.service_selected = true;
19721
19722 let items: Vec<CloudTrailEvent> = vec![
19723 CloudTrailEvent {
19724 event_name: "PutObject".to_string(),
19725 event_time: "2024-01-01 10:00:00 (UTC)".to_string(),
19726 username: "alice".to_string(),
19727 event_source: "s3.amazonaws.com".to_string(),
19728 resource_type: "Bucket".to_string(),
19729 resource_name: "my-bucket".to_string(),
19730 read_only: "false".to_string(),
19731 aws_region: "us-east-1".to_string(),
19732 event_id: "evt-001".to_string(),
19733 access_key_id: "AKIA".to_string(),
19734 source_ip_address: "1.2.3.4".to_string(),
19735 error_code: String::new(),
19736 request_id: "req-001".to_string(),
19737 event_type: "AwsApiCall".to_string(),
19738 cloud_trail_event_json: "{}".to_string(),
19739 },
19740 CloudTrailEvent {
19741 event_name: "GetObject".to_string(),
19742 event_time: "2024-01-01 11:00:00 (UTC)".to_string(),
19743 username: "bob".to_string(),
19744 event_source: "s3.amazonaws.com".to_string(),
19745 resource_type: "Object".to_string(),
19746 resource_name: "my-key".to_string(),
19747 read_only: "true".to_string(),
19748 aws_region: "us-east-1".to_string(),
19749 event_id: "evt-002".to_string(),
19750 access_key_id: "AKIA2".to_string(),
19751 source_ip_address: "5.6.7.8".to_string(),
19752 error_code: String::new(),
19753 request_id: "req-002".to_string(),
19754 event_type: "AwsApiCall".to_string(),
19755 cloud_trail_event_json: "{}".to_string(),
19756 },
19757 ];
19758 app.cloudtrail_state.table.items = items;
19759
19760 let q = app.cloudtrail_state.table.filter.to_lowercase();
19762 let filtered: Vec<_> = app
19763 .cloudtrail_state
19764 .table
19765 .items
19766 .iter()
19767 .filter(|e| {
19768 q.is_empty()
19769 || e.event_name.to_lowercase().contains(&q)
19770 || e.username.to_lowercase().contains(&q)
19771 })
19772 .collect();
19773 assert_eq!(
19774 filtered.len(),
19775 2,
19776 "No filter: both events should be visible"
19777 );
19778
19779 app.cloudtrail_state.table.filter = "PutObject".to_string();
19781 let q = app.cloudtrail_state.table.filter.to_lowercase();
19782 let filtered: Vec<_> = app
19783 .cloudtrail_state
19784 .table
19785 .items
19786 .iter()
19787 .filter(|e| {
19788 q.is_empty()
19789 || e.event_name.to_lowercase().contains(&q)
19790 || e.username.to_lowercase().contains(&q)
19791 })
19792 .collect();
19793 assert_eq!(
19794 filtered.len(),
19795 1,
19796 "Filter 'PutObject' should show only 1 event"
19797 );
19798 assert_eq!(filtered[0].event_name, "PutObject");
19799
19800 app.cloudtrail_state.table.filter = "bob".to_string();
19802 let q = app.cloudtrail_state.table.filter.to_lowercase();
19803 let filtered: Vec<_> = app
19804 .cloudtrail_state
19805 .table
19806 .items
19807 .iter()
19808 .filter(|e| {
19809 q.is_empty()
19810 || e.event_name.to_lowercase().contains(&q)
19811 || e.username.to_lowercase().contains(&q)
19812 })
19813 .collect();
19814 assert_eq!(filtered.len(), 1, "Filter 'bob' should show only 1 event");
19815 assert_eq!(filtered[0].username, "bob");
19816 }
19817
19818 #[test]
19819 fn test_lambda_application_resources_have_columns() {
19820 let app = test_app();
19821
19822 assert_eq!(app.lambda_resource_visible_column_ids.len(), 4);
19824 assert_eq!(app.lambda_resource_column_ids.len(), 4);
19825
19826 assert!(app
19828 .lambda_resource_visible_column_ids
19829 .contains(&"column.lambda.resource.logical_id"));
19830 assert!(app
19831 .lambda_resource_column_ids
19832 .contains(&"column.lambda.resource.logical_id"));
19833 }
19834
19835 #[test]
19836 fn test_lambda_functions_list_right_expands_left_collapses() {
19837 use crate::lambda::Function;
19838 let mut app = test_app();
19839 app.mode = Mode::Normal;
19840 app.current_service = Service::LambdaFunctions;
19841 app.service_selected = true;
19842 app.lambda_state.table.items = vec![Function {
19843 name: "fn1".to_string(),
19844 arn: "arn::fn1".to_string(),
19845 application: None,
19846 description: String::new(),
19847 package_type: String::new(),
19848 runtime: "python3.12".to_string(),
19849 architecture: String::new(),
19850 code_size: 0,
19851 code_sha256: String::new(),
19852 memory_mb: 128,
19853 timeout_seconds: 3,
19854 last_modified: String::new(),
19855 layers: vec![],
19856 }];
19857 app.lambda_state.table.selected = 0;
19858
19859 app.handle_action(Action::NextPane);
19861 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
19862
19863 app.handle_action(Action::NextPane);
19865 assert_eq!(app.lambda_state.table.expanded_item, Some(0));
19866
19867 app.handle_action(Action::CollapseRow);
19869 assert_eq!(app.lambda_state.table.expanded_item, None);
19870 }
19871
19872 #[test]
19873 fn test_lambda_applications_list_right_expands_left_collapses() {
19874 use crate::lambda::Application;
19875 let mut app = test_app();
19876 app.mode = Mode::Normal;
19877 app.current_service = Service::LambdaApplications;
19878 app.service_selected = true;
19879 app.lambda_application_state.table.items = vec![Application {
19880 name: "my-app".to_string(),
19881 arn: String::new(),
19882 description: String::new(),
19883 status: "ACTIVE".to_string(),
19884 last_modified: String::new(),
19885 }];
19886 app.lambda_application_state.table.selected = 0;
19887
19888 app.handle_action(Action::NextPane);
19890 assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
19891
19892 app.handle_action(Action::NextPane);
19894 assert_eq!(app.lambda_application_state.table.expanded_item, Some(0));
19895
19896 app.handle_action(Action::CollapseRow);
19898 assert_eq!(app.lambda_application_state.table.expanded_item, None);
19899 }
19900
19901 #[test]
19902 fn test_lambda_application_enter_on_deployments_tab_does_not_switch_to_overview() {
19903 use crate::lambda::Application;
19904 let mut app = test_app();
19905 app.mode = Mode::Normal;
19906 app.current_service = Service::LambdaApplications;
19907 app.service_selected = true;
19908 app.lambda_application_state.table.items = vec![Application {
19909 name: "my-app".to_string(),
19910 arn: String::new(),
19911 description: String::new(),
19912 status: "ACTIVE".to_string(),
19913 last_modified: String::new(),
19914 }];
19915
19916 app.handle_action(Action::Select);
19918 assert!(app.lambda_application_state.current_application.is_some());
19919
19920 use crate::ui::lambda::ApplicationDetailTab as LambdaApplicationDetailTab;
19922 app.lambda_application_state.detail_tab = LambdaApplicationDetailTab::Deployments;
19923 assert_eq!(
19924 app.lambda_application_state.detail_tab,
19925 LambdaApplicationDetailTab::Deployments
19926 );
19927
19928 app.handle_action(Action::Select);
19930 assert_eq!(
19931 app.lambda_application_state.detail_tab,
19932 LambdaApplicationDetailTab::Deployments
19933 );
19934 }
19935
19936 #[test]
19937 fn test_lambda_applications_pagination_focus_left_right_jump_pages() {
19938 use crate::lambda::Application;
19939 let mut app = test_app();
19940 app.current_service = Service::LambdaApplications;
19941 app.service_selected = true;
19942 app.mode = Mode::FilterInput;
19943 app.lambda_application_state.input_focus = InputFocus::Pagination;
19944
19945 app.lambda_application_state.table.items = (0..100)
19947 .map(|i| Application {
19948 name: format!("app{:03}", i),
19949 arn: String::new(),
19950 description: String::new(),
19951 status: "ACTIVE".to_string(),
19952 last_modified: String::new(),
19953 })
19954 .collect();
19955 app.lambda_application_state.table.selected = 0;
19956
19957 app.handle_action(Action::PageDown);
19959 assert_eq!(
19960 app.lambda_application_state.table.selected, 50,
19961 "Right arrow with pagination focus should jump to page 2"
19962 );
19963
19964 app.handle_action(Action::PageUp);
19966 assert_eq!(
19967 app.lambda_application_state.table.selected, 0,
19968 "Left arrow with pagination focus should jump back to page 1"
19969 );
19970
19971 app.handle_action(Action::NextItem);
19973 assert_eq!(
19974 app.lambda_application_state.table.selected, 0,
19975 "Down arrow must not move table when pagination is focused"
19976 );
19977 app.lambda_application_state.table.selected = 5;
19978 app.handle_action(Action::PrevItem);
19979 assert_eq!(
19980 app.lambda_application_state.table.selected, 5,
19981 "Up arrow must not move table when pagination is focused"
19982 );
19983 }
19984
19985 #[test]
19986 fn test_cloudwatch_alarms_enter_drills_down() {
19987 let mut app = test_app();
19988 app.mode = Mode::Normal;
19989 app.current_service = Service::CloudWatchAlarms;
19990 app.service_selected = true;
19991 app.view_mode = ViewMode::List;
19992
19993 app.alarms_state.table.items = vec![Alarm {
19995 name: "test-alarm".to_string(),
19996 state: "ALARM".to_string(),
19997 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
19998 description: "Test alarm".to_string(),
19999 metric_name: "CPUUtilization".to_string(),
20000 namespace: "AWS/EC2".to_string(),
20001 statistic: "Average".to_string(),
20002 period: 300,
20003 comparison_operator: "GreaterThanThreshold".to_string(),
20004 threshold: 80.0,
20005 actions_enabled: true,
20006 state_reason: "Threshold crossed".to_string(),
20007 resource: "".to_string(),
20008 dimensions: "".to_string(),
20009 expression: "".to_string(),
20010 alarm_type: "MetricAlarm".to_string(),
20011 cross_account: "".to_string(),
20012 ..Default::default()
20013 }];
20014
20015 assert!(app.alarms_state.current_alarm.is_none());
20016 assert_eq!(app.view_mode, ViewMode::List);
20017 assert!(!app.alarms_state.metrics_loading);
20018
20019 app.handle_action(Action::Select);
20021
20022 assert_eq!(
20023 app.alarms_state.current_alarm,
20024 Some("test-alarm".to_string())
20025 );
20026 assert_eq!(app.view_mode, ViewMode::Detail);
20027 assert!(
20028 app.alarms_state.metrics_loading,
20029 "Should trigger metrics loading"
20030 );
20031 }
20032
20033 #[test]
20034 fn test_cloudwatch_alarms_metric_data_renders() {
20035 let mut app = test_app();
20036 app.mode = Mode::Normal;
20037 app.current_service = Service::CloudWatchAlarms;
20038 app.service_selected = true;
20039 app.view_mode = ViewMode::Detail;
20040
20041 app.alarms_state.table.items = vec![Alarm {
20043 name: "test-alarm".to_string(),
20044 state: "ALARM".to_string(),
20045 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
20046 description: "Test alarm".to_string(),
20047 metric_name: "CPUUtilization".to_string(),
20048 namespace: "AWS/EC2".to_string(),
20049 statistic: "Average".to_string(),
20050 period: 300,
20051 comparison_operator: "GreaterThanThreshold".to_string(),
20052 threshold: 80.0,
20053 actions_enabled: true,
20054 state_reason: "Threshold crossed".to_string(),
20055 resource: "".to_string(),
20056 dimensions: "".to_string(),
20057 expression: "".to_string(),
20058 alarm_type: "MetricAlarm".to_string(),
20059 cross_account: "".to_string(),
20060 ..Default::default()
20061 }];
20062
20063 app.alarms_state.current_alarm = Some("test-alarm".to_string());
20064
20065 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0), (3000, 70.0)];
20067 app.alarms_state.metrics_loading = false;
20068
20069 assert!(!app.alarms_state.metric_data.is_empty());
20071 assert_eq!(app.alarms_state.metric_data.len(), 3);
20072 assert!(!app.alarms_state.metrics_loading);
20073 }
20074
20075 #[test]
20076 fn test_cloudwatch_alarms_back_clears_metrics() {
20077 let mut app = test_app();
20078 app.mode = Mode::Normal;
20079 app.current_service = Service::CloudWatchAlarms;
20080 app.service_selected = true;
20081 app.view_mode = ViewMode::Detail;
20082
20083 app.alarms_state.current_alarm = Some("test-alarm".to_string());
20085 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
20086
20087 assert!(!app.alarms_state.metric_data.is_empty());
20088
20089 app.handle_action(Action::GoBack);
20091
20092 assert!(app.alarms_state.current_alarm.is_none());
20094 assert!(app.alarms_state.metric_data.is_empty());
20095 assert_eq!(app.view_mode, ViewMode::List);
20096 }
20097
20098 #[test]
20099 fn test_cloudwatch_alarms_refresh_reloads_metrics() {
20100 let mut app = test_app();
20101 app.mode = Mode::Normal;
20102 app.current_service = Service::CloudWatchAlarms;
20103 app.service_selected = true;
20104 app.view_mode = ViewMode::Detail;
20105
20106 app.alarms_state.current_alarm = Some("test-alarm".to_string());
20108 app.alarms_state.metric_data = vec![(1000, 50.0), (2000, 60.0)];
20109 app.alarms_state.metrics_loading = false;
20110
20111 assert!(!app.alarms_state.metric_data.is_empty());
20112 assert!(!app.alarms_state.metrics_loading);
20113
20114 app.handle_action(Action::Refresh);
20116
20117 assert!(!app.alarms_state.metric_data.is_empty());
20119 assert!(app.alarms_state.metrics_loading);
20120 }
20121
20122 #[test]
20123 fn test_cloudwatch_alarms_right_arrow_expands() {
20124 let mut app = test_app();
20125 app.mode = Mode::Normal;
20126 app.current_service = Service::CloudWatchAlarms;
20127 app.service_selected = true;
20128 app.view_mode = ViewMode::List;
20129
20130 app.alarms_state.table.items = vec![Alarm {
20132 name: "test-alarm".to_string(),
20133 state: "ALARM".to_string(),
20134 state_updated_timestamp: "2024-01-01 12:00:00".to_string(),
20135 description: "Test alarm".to_string(),
20136 metric_name: "CPUUtilization".to_string(),
20137 namespace: "AWS/EC2".to_string(),
20138 statistic: "Average".to_string(),
20139 period: 300,
20140 comparison_operator: "GreaterThanThreshold".to_string(),
20141 threshold: 80.0,
20142 actions_enabled: true,
20143 state_reason: "Threshold crossed".to_string(),
20144 resource: "".to_string(),
20145 dimensions: "".to_string(),
20146 expression: "".to_string(),
20147 alarm_type: "MetricAlarm".to_string(),
20148 cross_account: "".to_string(),
20149 ..Default::default()
20150 }];
20151
20152 assert!(!app.alarms_state.table.is_expanded());
20153
20154 app.handle_action(Action::NextPane);
20156 assert!(app.alarms_state.table.is_expanded());
20157
20158 app.handle_action(Action::PrevPane);
20160 assert!(!app.alarms_state.table.is_expanded());
20161 }
20162
20163 #[test]
20164 fn test_cloudwatch_alarms_tab_switches_tabs() {
20165 let mut app = test_app();
20166 app.mode = Mode::Normal;
20167 app.current_service = Service::CloudWatchAlarms;
20168 app.service_selected = true;
20169
20170 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
20172
20173 app.handle_action(Action::NextDetailTab);
20175 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
20176
20177 app.handle_action(Action::NextDetailTab);
20179 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::AllAlarms);
20180
20181 app.handle_action(Action::PrevDetailTab);
20183 assert_eq!(app.alarms_state.alarm_tab, AlarmTab::InAlarm);
20184 }
20185
20186 #[test]
20187 fn test_cloudwatch_alarms_pagination_changes_table_contents() {
20188 let mut app = test_app();
20192 app.current_service = Service::CloudWatchAlarms;
20193 app.service_selected = true;
20194 app.mode = Mode::FilterInput;
20195 app.alarms_state.input_focus = InputFocus::Pagination;
20196 app.alarms_state.table.page_size = crate::common::PageSize::Ten;
20197
20198 app.alarms_state.table.items = (0..25)
20200 .map(|i| crate::cw::Alarm {
20201 name: format!("alarm-{:02}", i),
20202 state: "OK".to_string(),
20203 state_updated_timestamp: String::new(),
20204 description: String::new(),
20205 metric_name: String::new(),
20206 namespace: String::new(),
20207 statistic: String::new(),
20208 period: 60,
20209 comparison_operator: String::new(),
20210 threshold: 0.0,
20211 actions_enabled: false,
20212 state_reason: String::new(),
20213 resource: String::new(),
20214 dimensions: String::new(),
20215 expression: String::new(),
20216 alarm_type: String::new(),
20217 cross_account: String::new(),
20218 ..Default::default()
20219 })
20220 .collect();
20221
20222 assert_eq!(app.alarms_state.table.selected, 0, "starts on page 1");
20223
20224 app.handle_action(Action::PageDown);
20226 assert_eq!(
20227 app.alarms_state.table.selected, 10,
20228 "Right arrow must advance selected to page 2"
20229 );
20230
20231 app.handle_action(Action::PageUp);
20233 assert_eq!(
20234 app.alarms_state.table.selected, 0,
20235 "Left arrow must go back"
20236 );
20237
20238 app.go_to_page(3);
20240 assert_eq!(
20241 app.alarms_state.table.selected, 20,
20242 "go_to_page(3) must set selected to start of page 3"
20243 );
20244 }
20245
20246 #[test]
20247 fn test_cloudwatch_alarms_num_p_page_jump() {
20248 let mut app = test_app();
20251 app.current_service = Service::CloudWatchAlarms;
20252 app.service_selected = true;
20253 app.mode = Mode::Normal;
20254 app.alarms_state.table.page_size = crate::common::PageSize::Ten;
20255 app.alarms_state.table.items = (0..25)
20256 .map(|i| crate::cw::Alarm {
20257 name: format!("alarm-{:02}", i),
20258 state: "OK".to_string(),
20259 state_updated_timestamp: String::new(),
20260 description: String::new(),
20261 metric_name: String::new(),
20262 namespace: String::new(),
20263 statistic: String::new(),
20264 period: 60,
20265 comparison_operator: String::new(),
20266 threshold: 0.0,
20267 actions_enabled: false,
20268 state_reason: String::new(),
20269 resource: String::new(),
20270 dimensions: String::new(),
20271 expression: String::new(),
20272 alarm_type: String::new(),
20273 cross_account: String::new(),
20274 ..Default::default()
20275 })
20276 .collect();
20277
20278 app.handle_action(Action::FilterInput('2'));
20280 assert_eq!(app.page_input, "2");
20281 app.handle_action(Action::OpenColumnSelector);
20282 assert_eq!(
20283 app.alarms_state.table.selected, 10,
20284 "2p must jump to page 2 (selected=10)"
20285 );
20286 assert_eq!(app.page_input, "", "page_input must be cleared after jump");
20287 assert_ne!(
20289 app.mode,
20290 Mode::ColumnSelector,
20291 "mode must not open column selector when page_input was set"
20292 );
20293 }
20294
20295 #[test]
20296 fn test_cloudwatch_alarms_console_url_list_view() {
20297 let url = crate::cw::alarms::console_url("us-east-1", None);
20298 assert!(url.contains("alarmsV2"), "must contain alarmsV2");
20299 assert!(
20300 !url.contains("table") && !url.contains("card"),
20301 "must not embed view_mode"
20302 );
20303 }
20304
20305 #[test]
20306 fn test_cloudwatch_alarms_console_url_alarm_detail() {
20307 let url = crate::cw::alarms::console_url("us-east-1", Some("My-Alarm"));
20308 assert!(
20309 url.contains("alarmsV2:alarm/My-Alarm"),
20310 "must link directly to alarm, got: {url}"
20311 );
20312 }
20313
20314 #[test]
20315 fn test_cloudwatch_alarms_console_url_from_list_selected_alarm() {
20316 let mut app = test_app();
20319 app.current_service = Service::CloudWatchAlarms;
20320 app.service_selected = true;
20321 app.alarms_state.current_alarm = None; app.alarms_state.table.items = vec![
20323 crate::cw::Alarm {
20324 name: "alarm-0".to_string(),
20325 ..Default::default()
20326 },
20327 crate::cw::Alarm {
20328 name: "alarm-1".to_string(),
20329 ..Default::default()
20330 },
20331 ];
20332 app.alarms_state.table.selected = 1; let url = app.get_console_url();
20335 assert!(
20336 url.contains("alarmsV2:alarm/alarm-1"),
20337 "^o from list view must link to selected alarm, got: {url}"
20338 );
20339 }
20340
20341 #[test]
20342 fn test_cloudwatch_alarms_metric_math_alarm_does_not_error() {
20343 let alarm = crate::cw::Alarm {
20347 name: "math-alarm".to_string(),
20348 state: "OK".to_string(),
20349 state_updated_timestamp: String::new(),
20350 description: String::new(),
20351 metric_name: String::new(), namespace: String::new(), statistic: String::new(), period: 60,
20355 comparison_operator: "GreaterThanOrEqualToThreshold".to_string(),
20356 threshold: 1.0,
20357 actions_enabled: true,
20358 state_reason: String::new(),
20359 resource: String::new(),
20360 dimensions: String::new(),
20361 expression: "SUM([m1, m2])".to_string(),
20362 alarm_type: "expression".to_string(),
20363 cross_account: String::new(),
20364 ..Default::default()
20365 };
20366 assert!(
20368 alarm.statistic.is_empty(),
20369 "Metric math alarm must have empty statistic"
20370 );
20371 assert!(
20372 alarm.metric_name.is_empty(),
20373 "Metric math alarm must have empty metric_name"
20374 );
20375 let should_skip = alarm.statistic.is_empty() || alarm.metric_name.is_empty();
20378 assert!(
20379 should_skip,
20380 "Should skip get_metric_statistics for metric math alarms"
20381 );
20382 }
20383
20384 #[test]
20385 fn test_column_toggle_prevents_hiding_last_column() {
20386 let mut app = test_app();
20387 app.mode = Mode::Normal;
20388 app.current_service = Service::CloudWatchAlarms;
20389 app.service_selected = true;
20390
20391 app.cw_alarm_visible_column_ids = vec!["column.cw.alarm.name"];
20393
20394 app.handle_action(Action::OpenColumnSelector);
20396 assert_eq!(app.mode, Mode::ColumnSelector);
20397
20398 app.column_selector_index = 1;
20400
20401 app.handle_action(Action::ToggleColumn);
20403
20404 assert_eq!(app.cw_alarm_visible_column_ids.len(), 1);
20406 assert_eq!(app.cw_alarm_visible_column_ids[0], "column.cw.alarm.name");
20407 }
20408}
20409
20410#[cfg(test)]
20411mod insights_log_group_dropdown_tests {
20412 use super::*;
20413 use test_helpers::*;
20414
20415 fn setup_insights_with_log_groups(app: &mut App) {
20416 app.current_service = Service::CloudWatchInsights;
20417 app.service_selected = true;
20418 app.mode = Mode::InsightsInput;
20419 app.insights_state.insights.insights_focus = InsightsFocus::LogGroupSearch;
20420
20421 app.log_groups_state.log_groups.items = vec![
20423 rusticity_core::LogGroup {
20424 name: "/aws/lambda/my-function".to_string(),
20425 stored_bytes: None,
20426 retention_days: None,
20427 log_class: None,
20428 arn: None,
20429 log_group_arn: None,
20430 deletion_protection_enabled: None,
20431 creation_time: None,
20432 },
20433 rusticity_core::LogGroup {
20434 name: "/aws/containerinsights/my-cluster/application".to_string(),
20435 stored_bytes: None,
20436 retention_days: None,
20437 log_class: None,
20438 arn: None,
20439 log_group_arn: None,
20440 deletion_protection_enabled: None,
20441 creation_time: None,
20442 },
20443 ];
20444 }
20445
20446 #[test]
20447 fn test_typing_in_log_group_search_shows_dropdown() {
20448 let mut app = test_app();
20449 setup_insights_with_log_groups(&mut app);
20450
20451 app.handle_action(Action::FilterInput('a'));
20453 app.handle_action(Action::FilterInput('w'));
20454
20455 assert!(
20456 app.insights_state.insights.show_dropdown,
20457 "Dropdown must show when log group search has matches"
20458 );
20459 assert!(
20460 !app.insights_state.insights.log_group_matches.is_empty(),
20461 "Must have matches for 'aw'"
20462 );
20463 }
20464
20465 #[test]
20466 fn test_space_toggles_highlighted_log_group_in_dropdown() {
20467 let mut app = test_app();
20470 setup_insights_with_log_groups(&mut app);
20471
20472 app.handle_action(Action::FilterInput('a'));
20474 app.handle_action(Action::FilterInput('w'));
20475
20476 assert!(app.insights_state.insights.show_dropdown);
20477 assert!(app.insights_state.insights.selected_log_groups.is_empty());
20478
20479 app.handle_action(Action::ToggleFilterCheckbox);
20481
20482 assert!(
20483 !app.insights_state.insights.selected_log_groups.is_empty(),
20484 "Space must add the highlighted log group to selected_log_groups"
20485 );
20486 }
20487
20488 #[test]
20489 fn test_space_key_dispatches_toggle_not_filter_input_in_insights_mode() {
20490 use crate::keymap::handle_key;
20491 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
20492
20493 let key = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE);
20494 let action = handle_key(key, Mode::InsightsInput);
20495 assert_eq!(
20496 action,
20497 Some(Action::ToggleFilterCheckbox),
20498 "Space in InsightsInput must dispatch ToggleFilterCheckbox, not FilterInput(' ')"
20499 );
20500 }
20501
20502 #[test]
20503 fn test_down_up_arrows_navigate_dropdown() {
20504 let mut app = test_app();
20505 setup_insights_with_log_groups(&mut app);
20506 app.handle_action(Action::FilterInput('a'));
20507 app.handle_action(Action::FilterInput('w'));
20508
20509 assert_eq!(app.insights_state.insights.dropdown_selected, 0);
20510
20511 app.handle_action(Action::NextItem);
20512 assert!(
20513 app.insights_state.insights.dropdown_selected >= 1
20514 || app.insights_state.insights.log_group_matches.len() == 1,
20515 "Down arrow must advance dropdown selection"
20516 );
20517 }
20518
20519 #[test]
20520 fn test_space_in_normal_mode_toggles_dropdown_entry_not_open_space_menu() {
20521 let mut app = test_app();
20524 setup_insights_with_log_groups(&mut app);
20525 app.handle_action(Action::StartFilter);
20527 assert_eq!(app.mode, Mode::InsightsInput);
20528 app.handle_action(Action::FilterInput('a'));
20529 app.handle_action(Action::FilterInput('w'));
20530 assert!(app.insights_state.insights.show_dropdown);
20531
20532 app.mode = Mode::Normal;
20534
20535 app.handle_action(Action::OpenSpaceMenu); assert_ne!(
20538 app.mode,
20539 Mode::SpaceMenu,
20540 "Space must NOT open space menu when Insights dropdown is showing"
20541 );
20542 assert!(
20543 !app.insights_state.insights.selected_log_groups.is_empty(),
20544 "Space must toggle the highlighted log group"
20545 );
20546 }
20547
20548 #[test]
20549 fn test_enter_selects_highlighted_entry_and_closes_dropdown() {
20550 let mut app = test_app();
20552 setup_insights_with_log_groups(&mut app);
20553 app.handle_action(Action::StartFilter);
20554 app.handle_action(Action::FilterInput('a'));
20555 app.handle_action(Action::FilterInput('w'));
20556 assert!(app.insights_state.insights.show_dropdown);
20557
20558 app.handle_action(Action::ApplyFilter);
20560
20561 assert!(
20562 !app.insights_state.insights.show_dropdown,
20563 "Dropdown must close after Enter"
20564 );
20565 assert!(
20566 !app.insights_state.insights.selected_log_groups.is_empty(),
20567 "Enter must select the highlighted log group before closing"
20568 );
20569 }
20570}