1use colored::Colorize;
2use std::env;
3
4use crate::auth::{ensure_drive_scope, EnvTokenProvider};
5use crate::error::Error;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum CheckStatus {
10 Pass,
11 Warning,
12 Error,
13}
14
15impl CheckStatus {
16 pub fn exit_code(&self) -> i32 {
18 match self {
19 CheckStatus::Pass => 0,
20 CheckStatus::Warning => 1,
21 CheckStatus::Error => 2,
22 }
23 }
24
25 pub fn as_marker(&self) -> String {
27 let label = match self {
28 CheckStatus::Pass => "ok",
29 CheckStatus::Warning => "warn",
30 CheckStatus::Error => "error",
31 };
32 let total_width = "error".len() + 2; format!("{:>width$}", format!("[{}]", label), width = total_width)
34 }
35
36 pub fn as_marker_colored(&self) -> String {
38 let marker = self.as_marker();
39 match self {
40 CheckStatus::Pass => marker.green(),
41 CheckStatus::Warning => marker.yellow(),
42 CheckStatus::Error => marker.red(),
43 }
44 .to_string()
45 }
46}
47
48#[derive(Debug, Clone)]
50pub struct CheckResult {
51 pub name: String,
52 pub status: CheckStatus,
53 pub message: String,
54 pub suggestion: Option<String>,
55}
56
57impl CheckResult {
58 pub fn new(name: impl Into<String>, status: CheckStatus, message: impl Into<String>) -> Self {
59 Self {
60 name: name.into(),
61 status,
62 message: message.into(),
63 suggestion: None,
64 }
65 }
66
67 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
68 self.suggestion = Some(suggestion.into());
69 self
70 }
71
72 pub fn format(&self) -> String {
74 self.format_with_marker(self.status.as_marker())
75 }
76
77 pub fn format_colored(&self) -> String {
79 self.format_with_marker(self.status.as_marker_colored())
80 }
81
82 fn format_with_marker(&self, marker: String) -> String {
83 let mut output = format!("{} {}", marker, self.message);
84 if let Some(suggestion) = &self.suggestion {
85 output.push_str(&format!("\n Suggestion: {}", suggestion));
86 }
87 output
88 }
89}
90
91#[derive(Debug)]
93pub struct DiagnosticsSummary {
94 pub checks: Vec<CheckResult>,
95}
96
97impl DiagnosticsSummary {
98 pub fn new(checks: Vec<CheckResult>) -> Self {
99 Self { checks }
100 }
101
102 pub fn exit_code(&self) -> i32 {
104 self.checks
105 .iter()
106 .map(|check| check.status.exit_code())
107 .max()
108 .unwrap_or(0)
109 }
110
111 pub fn count_by_status(&self, status: CheckStatus) -> usize {
113 self.checks
114 .iter()
115 .filter(|check| check.status == status)
116 .count()
117 }
118
119 pub fn format_summary(&self) -> String {
121 let total = self.checks.len();
122 let failed =
123 self.count_by_status(CheckStatus::Error) + self.count_by_status(CheckStatus::Warning);
124
125 if failed == 0 {
126 format!("\nSummary: All {} checks passed.", total)
127 } else {
128 format!(
129 "\nSummary: {} checks failing out of {}. See above for details.",
130 failed, total
131 )
132 }
133 }
134
135 pub fn format_summary_colored(&self) -> String {
137 let total = self.checks.len();
138 let failed =
139 self.count_by_status(CheckStatus::Error) + self.count_by_status(CheckStatus::Warning);
140
141 if failed == 0 {
142 format!(
143 "\n{}",
144 format!("Summary: All {} checks passed.", total).green()
145 )
146 } else {
147 format!(
148 "\n{}",
149 format!(
150 "Summary: {} checks failing out of {}. See above for details.",
151 failed, total
152 )
153 .yellow()
154 )
155 }
156 }
157}
158
159pub struct EnvVarCheck {
161 pub name: &'static str,
162 pub required: bool,
163 pub suggestion: &'static str,
164 pub show_value: bool,
165}
166
167const ENV_VAR_CHECKS: &[EnvVarCheck] = &[
169 EnvVarCheck {
170 name: "NBLM_PROJECT_NUMBER",
171 required: true,
172 suggestion: "export NBLM_PROJECT_NUMBER=<your-project-number>",
173 show_value: true,
174 },
175 EnvVarCheck {
176 name: "NBLM_ENDPOINT_LOCATION",
177 required: false,
178 suggestion: "export NBLM_ENDPOINT_LOCATION=us # or 'eu' or 'global'",
179 show_value: true,
180 },
181 EnvVarCheck {
182 name: "NBLM_LOCATION",
183 required: false,
184 suggestion: "export NBLM_LOCATION=global",
185 show_value: true,
186 },
187 EnvVarCheck {
188 name: "NBLM_ACCESS_TOKEN",
189 required: false,
190 suggestion: "export NBLM_ACCESS_TOKEN=$(gcloud auth print-access-token)",
191 show_value: false,
192 },
193];
194
195fn check_env_var(config: &EnvVarCheck) -> CheckResult {
197 match env::var(config.name) {
198 Ok(value) if !value.is_empty() => {
199 let message = if config.show_value {
200 format!("{}={}", config.name, value)
201 } else {
202 format!("{} set (value hidden)", config.name)
203 };
204 CheckResult::new(
205 format!("env_var_{}", config.name.to_lowercase()),
206 CheckStatus::Pass,
207 message,
208 )
209 }
210 Ok(_) | Err(env::VarError::NotPresent) => {
211 let status = if config.required {
212 CheckStatus::Error
213 } else {
214 CheckStatus::Warning
215 };
216 CheckResult::new(
217 format!("env_var_{}", config.name.to_lowercase()),
218 status,
219 format!("{} missing", config.name),
220 )
221 .with_suggestion(config.suggestion)
222 }
223 Err(env::VarError::NotUnicode(_)) => CheckResult::new(
224 format!("env_var_{}", config.name.to_lowercase()),
225 CheckStatus::Error,
226 format!("{} contains invalid UTF-8", config.name),
227 ),
228 }
229}
230
231pub fn check_environment_variables() -> Vec<CheckResult> {
233 ENV_VAR_CHECKS.iter().map(check_env_var).collect()
234}
235
236pub struct CommandCheck {
238 pub name: &'static str,
239 pub command: &'static str,
240 pub required: bool,
241 pub suggestion: &'static str,
242}
243
244const COMMAND_CHECKS: &[CommandCheck] = &[CommandCheck {
246 name: "gcloud",
247 command: "gcloud",
248 required: false,
249 suggestion: "Install Google Cloud CLI: https://cloud.google.com/sdk/docs/install",
250}];
251
252fn check_command(config: &CommandCheck) -> CheckResult {
254 let status = std::process::Command::new(config.command)
255 .arg("--version")
256 .output();
257
258 match status {
259 Ok(output) if output.status.success() => {
260 let version = String::from_utf8_lossy(&output.stdout);
261 let version_line = version.lines().next().unwrap_or("").trim();
262 CheckResult::new(
263 format!("command_{}", config.name),
264 CheckStatus::Pass,
265 format!("{} is installed ({})", config.name, version_line),
266 )
267 }
268 _ => {
269 let status = if config.required {
270 CheckStatus::Error
271 } else {
272 CheckStatus::Warning
273 };
274 CheckResult::new(
275 format!("command_{}", config.name),
276 status,
277 format!("{} command not found", config.name),
278 )
279 .with_suggestion(config.suggestion)
280 }
281 }
282}
283
284pub fn check_commands() -> Vec<CheckResult> {
286 COMMAND_CHECKS.iter().map(check_command).collect()
287}
288
289pub async fn check_drive_access_token() -> Vec<CheckResult> {
291 match env::var("NBLM_ACCESS_TOKEN") {
292 Ok(value) if !value.trim().is_empty() => {
293 let provider = EnvTokenProvider::new("NBLM_ACCESS_TOKEN");
294 match ensure_drive_scope(&provider).await {
295 Ok(_) => vec![CheckResult::new(
296 "drive_scope_nblm_access_token",
297 CheckStatus::Pass,
298 "NBLM_ACCESS_TOKEN grants Google Drive access",
299 )],
300 Err(Error::TokenProvider(message)) => {
301 if message.contains("missing the required drive.file scope") {
302 vec![CheckResult::new(
303 "drive_scope_nblm_access_token",
304 CheckStatus::Warning,
305 "NBLM_ACCESS_TOKEN lacks Google Drive scope",
306 )
307 .with_suggestion(
308 "Run `gcloud auth login --enable-gdrive-access` and refresh NBLM_ACCESS_TOKEN",
309 )]
310 } else {
311 vec![CheckResult::new(
312 "drive_scope_nblm_access_token",
313 CheckStatus::Warning,
314 format!(
315 "Could not confirm Google Drive scope for NBLM_ACCESS_TOKEN: {}",
316 message
317 ),
318 )]
319 }
320 }
321 Err(err) => vec![CheckResult::new(
322 "drive_scope_nblm_access_token",
323 CheckStatus::Warning,
324 format!(
325 "Could not confirm Google Drive scope for NBLM_ACCESS_TOKEN: {}",
326 err
327 ),
328 )],
329 }
330 }
331 _ => Vec::new(),
332 }
333}
334
335pub async fn check_api_connectivity() -> Vec<CheckResult> {
337 use crate::auth::GcloudTokenProvider;
338 use crate::client::NblmClient;
339 use crate::env::EnvironmentConfig;
340 use std::sync::Arc;
341
342 let project_number = match env::var("NBLM_PROJECT_NUMBER") {
344 Ok(val) if !val.is_empty() => val,
345 _ => {
346 return Vec::new();
348 }
349 };
350
351 let location = env::var("NBLM_LOCATION").unwrap_or_else(|_| "global".to_string());
352 let endpoint_location =
353 env::var("NBLM_ENDPOINT_LOCATION").unwrap_or_else(|_| "global".to_string());
354
355 let env_config =
357 match EnvironmentConfig::enterprise(project_number, location, endpoint_location) {
358 Ok(config) => config,
359 Err(err) => {
360 return vec![CheckResult::new(
361 "api_connectivity",
362 CheckStatus::Error,
363 format!("Cannot construct environment config: {}", err),
364 )
365 .with_suggestion(
366 "Ensure NBLM_PROJECT_NUMBER, NBLM_LOCATION, and NBLM_ENDPOINT_LOCATION are valid",
367 )];
368 }
369 };
370
371 let token_provider: Arc<dyn crate::auth::TokenProvider> =
373 match env::var("NBLM_ACCESS_TOKEN").ok().filter(|s| !s.is_empty()) {
374 Some(_) => Arc::new(crate::auth::EnvTokenProvider::new("NBLM_ACCESS_TOKEN")),
375 None => {
376 if !is_gcloud_available() {
378 return Vec::new();
379 }
380 Arc::new(GcloudTokenProvider::new("gcloud"))
381 }
382 };
383
384 let client = match NblmClient::new(token_provider, env_config) {
386 Ok(client) => client,
387 Err(err) => {
388 return vec![CheckResult::new(
389 "api_connectivity",
390 CheckStatus::Error,
391 format!("Failed to create API client: {}", err),
392 )
393 .with_suggestion("Check your environment configuration and credentials")];
394 }
395 };
396
397 match client.list_recently_viewed(Some(1)).await {
399 Ok(_) => vec![CheckResult::new(
400 "api_connectivity",
401 CheckStatus::Pass,
402 "Successfully connected to NotebookLM API",
403 )],
404 Err(err) => {
405 let err_string = err.to_string();
406 let (status, message, suggestion) = categorize_api_error(&err_string);
407
408 vec![CheckResult::new("api_connectivity", status, message).with_suggestion(suggestion)]
409 }
410 }
411}
412
413fn is_gcloud_available() -> bool {
415 std::process::Command::new("gcloud")
416 .arg("--version")
417 .output()
418 .map(|output| output.status.success())
419 .unwrap_or(false)
420}
421
422fn categorize_api_error(error: &str) -> (CheckStatus, String, &'static str) {
424 let error_lower = error.to_lowercase();
425
426 match () {
427 _ if error_lower.contains("401") || error_lower.contains("unauthorized") => (
428 CheckStatus::Error,
429 "Authentication failed (401 Unauthorized)".to_string(),
430 "Run `gcloud auth login` or `gcloud auth application-default login`",
431 ),
432 _ if error_lower.contains("403") || error_lower.contains("permission denied") => (
433 CheckStatus::Error,
434 "Permission denied (403 Forbidden)".to_string(),
435 "Ensure your account has NotebookLM API access and required IAM roles (e.g., aiplatform.user)",
436 ),
437 _ if error_lower.contains("404") || error_lower.contains("not found") => (
438 CheckStatus::Error,
439 "Resource not found (404)".to_string(),
440 "Verify NBLM_PROJECT_NUMBER is correct and the project has NotebookLM enabled",
441 ),
442 _
443 if error_lower.contains("timeout")
444 || error_lower.contains("connection")
445 || error_lower.contains("network") =>
446 {
447 (
448 CheckStatus::Error,
449 format!("Network error: {}", error),
450 "Check your internet connection and firewall settings",
451 )
452 }
453 _ => (
454 CheckStatus::Error,
455 format!("API error: {}", error),
456 "Check the error message above and your configuration",
457 ),
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use serial_test::serial;
465 use wiremock::matchers::{method, path, query_param};
466 use wiremock::{Mock, MockServer, ResponseTemplate};
467
468 struct EnvGuard {
469 key: &'static str,
470 original: Option<String>,
471 }
472
473 impl EnvGuard {
474 fn new(key: &'static str) -> Self {
475 let original = env::var(key).ok();
476 Self { key, original }
477 }
478 }
479
480 impl Drop for EnvGuard {
481 fn drop(&mut self) {
482 if let Some(value) = &self.original {
483 env::set_var(self.key, value);
484 } else {
485 env::remove_var(self.key);
486 }
487 }
488 }
489
490 #[test]
491 fn test_check_status_markers() {
492 assert_eq!(CheckStatus::Pass.as_marker(), " [ok]");
493 assert_eq!(CheckStatus::Warning.as_marker(), " [warn]");
494 assert_eq!(CheckStatus::Error.as_marker(), "[error]");
495 }
496
497 #[test]
498 fn test_check_status_colored_markers() {
499 colored::control::set_override(true);
501
502 let ok = CheckStatus::Pass.as_marker_colored();
504 assert!(ok.contains("[ok]"));
505
506 let warn = CheckStatus::Warning.as_marker_colored();
507 assert!(warn.contains("[warn]"));
508
509 let err = CheckStatus::Error.as_marker_colored();
510 assert!(err.contains("[error]"));
511
512 colored::control::unset_override();
514 }
515
516 #[test]
517 fn test_check_status_exit_codes() {
518 assert_eq!(CheckStatus::Pass.exit_code(), 0);
519 assert_eq!(CheckStatus::Warning.exit_code(), 1);
520 assert_eq!(CheckStatus::Error.exit_code(), 2);
521 }
522
523 #[test]
524 fn test_check_result_format() {
525 let result = CheckResult::new("test", CheckStatus::Pass, "Test passed");
526 assert_eq!(result.format(), " [ok] Test passed");
527
528 let result_with_suggestion = CheckResult::new("test", CheckStatus::Warning, "Test warning")
529 .with_suggestion("Try this fix");
530 assert!(result_with_suggestion.format().contains("Suggestion:"));
531 }
532
533 #[test]
534 fn test_check_result_format_colored() {
535 colored::control::set_override(true);
537
538 let result = CheckResult::new("test", CheckStatus::Pass, "Test passed");
539 let colored = result.format_colored();
540 assert!(colored.contains("\x1b["));
541 assert!(colored.contains("Test passed"));
542 assert!(colored.ends_with("Test passed"));
543
544 colored::control::unset_override();
546 }
547
548 #[test]
549 fn test_diagnostics_summary_exit_code() {
550 let summary = DiagnosticsSummary::new(vec![
551 CheckResult::new("test1", CheckStatus::Pass, "Pass"),
552 CheckResult::new("test2", CheckStatus::Pass, "Pass"),
553 ]);
554 assert_eq!(summary.exit_code(), 0);
555
556 let summary = DiagnosticsSummary::new(vec![
557 CheckResult::new("test1", CheckStatus::Pass, "Pass"),
558 CheckResult::new("test2", CheckStatus::Warning, "Warning"),
559 ]);
560 assert_eq!(summary.exit_code(), 1);
561
562 let summary = DiagnosticsSummary::new(vec![
563 CheckResult::new("test1", CheckStatus::Pass, "Pass"),
564 CheckResult::new("test2", CheckStatus::Error, "Error"),
565 ]);
566 assert_eq!(summary.exit_code(), 2);
567 }
568
569 #[test]
570 fn test_check_env_var_present() {
571 env::set_var("TEST_VAR", "test_value");
572 let config = EnvVarCheck {
573 name: "TEST_VAR",
574 required: true,
575 suggestion: "export TEST_VAR=value",
576 show_value: true,
577 };
578 let result = check_env_var(&config);
579 assert_eq!(result.status, CheckStatus::Pass);
580 assert!(result.message.contains("test_value"));
581 env::remove_var("TEST_VAR");
582 }
583
584 #[test]
585 fn test_check_env_var_missing_required() {
586 env::remove_var("MISSING_VAR");
587 let config = EnvVarCheck {
588 name: "MISSING_VAR",
589 required: true,
590 suggestion: "export MISSING_VAR=value",
591 show_value: true,
592 };
593 let result = check_env_var(&config);
594 assert_eq!(result.status, CheckStatus::Error);
595 assert!(result.message.contains("missing"));
596 assert!(result.suggestion.is_some());
597 }
598
599 #[test]
600 fn test_check_env_var_missing_optional() {
601 env::remove_var("OPTIONAL_VAR");
602 let config = EnvVarCheck {
603 name: "OPTIONAL_VAR",
604 required: false,
605 suggestion: "export OPTIONAL_VAR=value",
606 show_value: true,
607 };
608 let result = check_env_var(&config);
609 assert_eq!(result.status, CheckStatus::Warning);
610 assert!(result.message.contains("missing"));
611 }
612
613 #[test]
614 fn test_check_command_not_found() {
615 let config = CommandCheck {
616 name: "nonexistent_command_xyz",
617 command: "nonexistent_command_xyz",
618 required: false,
619 suggestion: "Install the command",
620 };
621 let result = check_command(&config);
622 assert_eq!(result.status, CheckStatus::Warning);
623 assert!(result.message.contains("not found"));
624 assert!(result.suggestion.is_some());
625 }
626
627 #[test]
628 fn test_check_command_required_not_found() {
629 let config = CommandCheck {
630 name: "nonexistent_required",
631 command: "nonexistent_required",
632 required: true,
633 suggestion: "Install the command",
634 };
635 let result = check_command(&config);
636 assert_eq!(result.status, CheckStatus::Error);
637 assert!(result.message.contains("not found"));
638 }
639
640 #[tokio::test]
641 #[serial]
642 async fn test_drive_access_check_passes_with_valid_scope() {
643 let token_guard = EnvGuard::new("NBLM_ACCESS_TOKEN");
644 let endpoint_guard = EnvGuard::new("NBLM_TOKENINFO_ENDPOINT");
645
646 env::set_var("NBLM_ACCESS_TOKEN", "test-token");
647
648 let server = MockServer::start().await;
649 let tokeninfo_url = format!("{}/tokeninfo", server.uri());
650 env::set_var("NBLM_TOKENINFO_ENDPOINT", &tokeninfo_url);
651
652 Mock::given(method("GET"))
653 .and(path("/tokeninfo"))
654 .and(query_param("access_token", "test-token"))
655 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
656 "scope": "https://www.googleapis.com/auth/drive.file"
657 })))
658 .expect(1)
659 .mount(&server)
660 .await;
661
662 let results = check_drive_access_token().await;
663 assert_eq!(results.len(), 1);
664 assert_eq!(results[0].status, CheckStatus::Pass);
665 assert!(results[0].message.contains("grants Google Drive access"));
666
667 drop(token_guard);
668 drop(endpoint_guard);
669 }
670
671 #[tokio::test]
672 #[serial]
673 async fn test_drive_access_check_reports_missing_scope() {
674 let token_guard = EnvGuard::new("NBLM_ACCESS_TOKEN");
675 let endpoint_guard = EnvGuard::new("NBLM_TOKENINFO_ENDPOINT");
676
677 env::set_var("NBLM_ACCESS_TOKEN", "test-token");
678
679 let server = MockServer::start().await;
680 let tokeninfo_url = format!("{}/tokeninfo", server.uri());
681 env::set_var("NBLM_TOKENINFO_ENDPOINT", &tokeninfo_url);
682
683 Mock::given(method("GET"))
684 .and(path("/tokeninfo"))
685 .and(query_param("access_token", "test-token"))
686 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
687 "scope": "https://www.googleapis.com/auth/cloud-platform"
688 })))
689 .expect(1)
690 .mount(&server)
691 .await;
692
693 let results = check_drive_access_token().await;
694 assert_eq!(results.len(), 1);
695 assert_eq!(results[0].status, CheckStatus::Warning);
696 assert!(results[0].message.contains("lacks Google Drive scope"));
697
698 drop(token_guard);
699 drop(endpoint_guard);
700 }
701
702 #[test]
703 fn test_categorize_api_error_401() {
704 let (status, message, suggestion) = categorize_api_error("401 Unauthorized");
705 assert_eq!(status, CheckStatus::Error);
706 assert!(message.contains("Authentication failed"));
707 assert!(suggestion.contains("gcloud auth login"));
708 }
709
710 #[test]
711 fn test_categorize_api_error_403() {
712 let (status, message, suggestion) = categorize_api_error("403 Permission denied");
713 assert_eq!(status, CheckStatus::Error);
714 assert!(message.contains("Permission denied"));
715 assert!(suggestion.contains("IAM roles"));
716 }
717
718 #[test]
719 fn test_categorize_api_error_404() {
720 let (status, message, suggestion) = categorize_api_error("404 Not found");
721 assert_eq!(status, CheckStatus::Error);
722 assert!(message.contains("Resource not found"));
723 assert!(suggestion.contains("NBLM_PROJECT_NUMBER"));
724 }
725
726 #[test]
727 fn test_categorize_api_error_timeout() {
728 let (status, message, suggestion) = categorize_api_error("Connection timeout");
729 assert_eq!(status, CheckStatus::Error);
730 assert!(message.contains("Network error"));
731 assert!(suggestion.contains("internet connection"));
732 }
733
734 #[test]
735 fn test_categorize_api_error_generic() {
736 let (status, message, suggestion) = categorize_api_error("Some random error");
737 assert_eq!(status, CheckStatus::Error);
738 assert!(message.contains("API error"));
739 assert!(suggestion.contains("configuration"));
740 }
741
742 #[tokio::test]
743 #[serial]
744 async fn test_check_api_connectivity_missing_project_number() {
745 let _guard = EnvGuard::new("NBLM_PROJECT_NUMBER");
746 env::remove_var("NBLM_PROJECT_NUMBER");
747
748 let results = check_api_connectivity().await;
749 assert_eq!(results.len(), 0);
750 }
751
752 #[test]
753 fn test_is_gcloud_available() {
754 let _ = is_gcloud_available();
757 }
758
759 #[test]
760 fn test_diagnostics_summary_count_by_status() {
761 let summary = DiagnosticsSummary::new(vec![
762 CheckResult::new("test1", CheckStatus::Pass, "Pass"),
763 CheckResult::new("test2", CheckStatus::Warning, "Warning"),
764 CheckResult::new("test3", CheckStatus::Error, "Error"),
765 CheckResult::new("test4", CheckStatus::Pass, "Pass"),
766 ]);
767
768 assert_eq!(summary.count_by_status(CheckStatus::Pass), 2);
769 assert_eq!(summary.count_by_status(CheckStatus::Warning), 1);
770 assert_eq!(summary.count_by_status(CheckStatus::Error), 1);
771 }
772
773 #[test]
774 fn test_diagnostics_summary_format() {
775 let summary = DiagnosticsSummary::new(vec![
776 CheckResult::new("test1", CheckStatus::Pass, "Pass"),
777 CheckResult::new("test2", CheckStatus::Pass, "Pass"),
778 ]);
779 let formatted = summary.format_summary();
780 assert!(formatted.contains("All 2 checks passed"));
781
782 let summary_with_failures = DiagnosticsSummary::new(vec![
783 CheckResult::new("test1", CheckStatus::Pass, "Pass"),
784 CheckResult::new("test2", CheckStatus::Warning, "Warning"),
785 ]);
786 let formatted_fail = summary_with_failures.format_summary();
787 assert!(formatted_fail.contains("1 checks failing out of 2"));
788 }
789
790 #[test]
791 fn test_check_result_with_suggestion() {
792 let result = CheckResult::new("test", CheckStatus::Warning, "Something wrong")
793 .with_suggestion("Fix it this way");
794
795 assert_eq!(result.suggestion, Some("Fix it this way".to_string()));
796 assert!(result.format().contains("Suggestion: Fix it this way"));
797 }
798
799 #[test]
800 fn test_check_env_var_hidden_value() {
801 env::set_var("SECRET_VAR", "secret_value");
802 let config = EnvVarCheck {
803 name: "SECRET_VAR",
804 required: true,
805 suggestion: "export SECRET_VAR=value",
806 show_value: false,
807 };
808 let result = check_env_var(&config);
809 assert_eq!(result.status, CheckStatus::Pass);
810 assert!(result.message.contains("value hidden"));
811 assert!(!result.message.contains("secret_value"));
812 env::remove_var("SECRET_VAR");
813 }
814
815 #[test]
816 fn test_check_environment_variables_integration() {
817 let results = check_environment_variables();
818 assert_eq!(results.len(), ENV_VAR_CHECKS.len());
820 }
821
822 #[test]
823 fn test_check_commands_integration() {
824 let results = check_commands();
825 assert_eq!(results.len(), COMMAND_CHECKS.len());
827 }
828}