Skip to content

Commit 5d0080f

Browse files
Merge pull request #423 from datadog-labs/test/auth-coverage
test(auth): add positive and negative unit tests for auth commands
2 parents 6a4527e + 955e9d5 commit 5d0080f

1 file changed

Lines changed: 235 additions & 0 deletions

File tree

src/commands/auth.rs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,238 @@ pub fn list(cfg: &Config) -> Result<()> {
354354
pub fn list(_cfg: &Config) -> Result<()> {
355355
bail!("Session listing is not available in WASM builds.")
356356
}
357+
358+
#[cfg(all(test, not(target_arch = "wasm32")))]
359+
mod tests {
360+
use super::*;
361+
use crate::config::{Config, OutputFormat};
362+
363+
/// Temporary directory that removes itself on drop. Mirrors the helper
364+
/// used in src/auth/storage.rs tests so we don't depend on an external
365+
/// tempdir crate.
366+
struct TempDir(std::path::PathBuf);
367+
368+
impl TempDir {
369+
fn new(label: &str) -> Self {
370+
let nanos = std::time::SystemTime::now()
371+
.duration_since(std::time::UNIX_EPOCH)
372+
.map(|d| d.subsec_nanos())
373+
.unwrap_or(0);
374+
let dir = std::env::temp_dir().join(format!("pup_auth_cmd_test_{label}_{nanos}"));
375+
std::fs::create_dir_all(&dir).unwrap();
376+
TempDir(dir)
377+
}
378+
379+
fn path(&self) -> &std::path::PathBuf {
380+
&self.0
381+
}
382+
}
383+
384+
impl Drop for TempDir {
385+
fn drop(&mut self) {
386+
let _ = std::fs::remove_dir_all(&self.0);
387+
}
388+
}
389+
390+
fn base_config() -> Config {
391+
Config {
392+
api_key: None,
393+
app_key: None,
394+
access_token: None,
395+
site: "datadoghq.com".into(),
396+
org: None,
397+
output_format: OutputFormat::Json,
398+
auto_approve: false,
399+
agent_mode: false,
400+
read_only: false,
401+
}
402+
}
403+
404+
// ------------------------------------------------------------------
405+
// token() — the one function with an access_token bypass that never
406+
// touches the global STORAGE singleton. Hermetic.
407+
// ------------------------------------------------------------------
408+
409+
#[test]
410+
fn test_token_prints_access_token_from_config() {
411+
let mut cfg = base_config();
412+
cfg.access_token = Some("oauth-access-token-from-cfg".into());
413+
// Positive path: cfg.access_token is Some → returns Ok without
414+
// touching storage. We only assert the Result; capturing stdout
415+
// would require redirecting std::io::stdout and is not necessary
416+
// to verify the bypass branch runs.
417+
assert!(token(&cfg).is_ok());
418+
}
419+
420+
#[test]
421+
fn test_token_empty_string_still_bypasses_storage() {
422+
// An empty-string access_token is still Some(_) — the guard uses
423+
// `if let Some(token)` and does not check for empty. Pin this
424+
// behaviour so future refactors are intentional.
425+
let mut cfg = base_config();
426+
cfg.access_token = Some(String::new());
427+
assert!(token(&cfg).is_ok());
428+
}
429+
430+
// ------------------------------------------------------------------
431+
// list() — empty session registry path is hermetic: when there are
432+
// no sessions on disk, the .map() closure that calls with_storage
433+
// never runs, so the global STORAGE singleton is not touched.
434+
//
435+
// All tests below use the tokio-based lock from test_support so that
436+
// both sync and async tests in this module serialize against a single
437+
// mutex and don't race each other for PUP_CONFIG_DIR / DD_TOKEN_STORAGE.
438+
// ------------------------------------------------------------------
439+
440+
#[tokio::test]
441+
async fn test_list_empty_session_registry_returns_ok() {
442+
let _lock = crate::test_support::lock_env().await;
443+
let tmp = TempDir::new("list_empty");
444+
std::env::set_var("PUP_CONFIG_DIR", tmp.path());
445+
446+
let cfg = base_config();
447+
let result = list(&cfg);
448+
449+
std::env::remove_var("PUP_CONFIG_DIR");
450+
assert!(
451+
result.is_ok(),
452+
"list with empty session registry should be Ok"
453+
);
454+
}
455+
456+
#[tokio::test]
457+
async fn test_list_with_saved_sessions_returns_ok() {
458+
// After save_session, the sessions.json file is present but we
459+
// haven't stored any tokens. The storage backend is exercised but
460+
// returns None → list() enriches each session with "no token"
461+
// status and returns Ok. This covers the populated branch of
462+
// list() without requiring real credentials.
463+
let _lock = crate::test_support::lock_env().await;
464+
let tmp = TempDir::new("list_populated");
465+
std::env::set_var("PUP_CONFIG_DIR", tmp.path());
466+
std::env::set_var("DD_TOKEN_STORAGE", "file");
467+
468+
storage::save_session("datadoghq.com", None).unwrap();
469+
storage::save_session("datadoghq.com", Some("prod-child")).unwrap();
470+
471+
let cfg = base_config();
472+
let result = list(&cfg);
473+
474+
std::env::remove_var("DD_TOKEN_STORAGE");
475+
std::env::remove_var("PUP_CONFIG_DIR");
476+
assert!(
477+
result.is_ok(),
478+
"list with saved sessions should be Ok even when no tokens stored"
479+
);
480+
}
481+
482+
// ------------------------------------------------------------------
483+
// status() — reads tokens via the global STORAGE singleton. We can
484+
// assert the return is Ok on the unauthenticated branch (no tokens
485+
// in whatever dir STORAGE was bound to), and cannot cleanly test
486+
// the authenticated branch from here without writing to STORAGE's
487+
// captured base_dir (which the singleton freezes at first init and
488+
// we cannot observe from outside auth/storage.rs).
489+
// ------------------------------------------------------------------
490+
491+
#[tokio::test]
492+
async fn test_status_returns_ok_for_unauthenticated() {
493+
let _lock = crate::test_support::lock_env().await;
494+
let tmp = TempDir::new("status_unauth");
495+
std::env::set_var("PUP_CONFIG_DIR", tmp.path());
496+
std::env::set_var("DD_TOKEN_STORAGE", "file");
497+
498+
let mut cfg = base_config();
499+
cfg.site = "unauth-site.example.invalid".into();
500+
let result = status(&cfg);
501+
502+
std::env::remove_var("DD_TOKEN_STORAGE");
503+
std::env::remove_var("PUP_CONFIG_DIR");
504+
// status() always returns Ok; it reports authentication state
505+
// via printed JSON, not via the Result.
506+
assert!(result.is_ok());
507+
}
508+
509+
#[tokio::test]
510+
async fn test_status_returns_ok_with_org_label() {
511+
// Same contract as above but with an org set. Covers the
512+
// org_label = " (org: ...)" branch in the unauthenticated arm.
513+
let _lock = crate::test_support::lock_env().await;
514+
let tmp = TempDir::new("status_org");
515+
std::env::set_var("PUP_CONFIG_DIR", tmp.path());
516+
std::env::set_var("DD_TOKEN_STORAGE", "file");
517+
518+
let mut cfg = base_config();
519+
cfg.site = "unauth-org-site.example.invalid".into();
520+
cfg.org = Some("test-org-label".into());
521+
let result = status(&cfg);
522+
523+
std::env::remove_var("DD_TOKEN_STORAGE");
524+
std::env::remove_var("PUP_CONFIG_DIR");
525+
assert!(result.is_ok());
526+
}
527+
528+
// ------------------------------------------------------------------
529+
// logout() — removes tokens / credentials / session entry. All three
530+
// are idempotent: deleting non-existent items returns Ok. That gives
531+
// us a clean negative-space test.
532+
// ------------------------------------------------------------------
533+
534+
#[tokio::test]
535+
async fn test_logout_is_idempotent_when_not_logged_in() {
536+
let _lock = crate::test_support::lock_env().await;
537+
let tmp = TempDir::new("logout_idempotent");
538+
std::env::set_var("PUP_CONFIG_DIR", tmp.path());
539+
std::env::set_var("DD_TOKEN_STORAGE", "file");
540+
541+
let mut cfg = base_config();
542+
cfg.site = "logout-clean.example.invalid".into();
543+
let result = logout(&cfg).await;
544+
545+
std::env::remove_var("DD_TOKEN_STORAGE");
546+
std::env::remove_var("PUP_CONFIG_DIR");
547+
assert!(
548+
result.is_ok(),
549+
"logout on an un-logged-in site should be a no-op"
550+
);
551+
}
552+
553+
#[tokio::test]
554+
async fn test_logout_with_org_removes_session_entry() {
555+
// save a session, then logout should remove just that org's entry
556+
// from sessions.json. PUP_CONFIG_DIR is read fresh by the session
557+
// registry helpers (unlike the frozen STORAGE singleton), so this
558+
// assertion is hermetic.
559+
let _lock = crate::test_support::lock_env().await;
560+
let tmp = TempDir::new("logout_session");
561+
std::env::set_var("PUP_CONFIG_DIR", tmp.path());
562+
std::env::set_var("DD_TOKEN_STORAGE", "file");
563+
564+
let site = "logout-session.example.invalid";
565+
storage::save_session(site, None).unwrap();
566+
storage::save_session(site, Some("keep-me")).unwrap();
567+
568+
let mut cfg = base_config();
569+
cfg.site = site.into();
570+
cfg.org = Some("keep-me".into());
571+
let result = logout(&cfg).await;
572+
573+
let remaining = storage::list_sessions().unwrap();
574+
std::env::remove_var("DD_TOKEN_STORAGE");
575+
std::env::remove_var("PUP_CONFIG_DIR");
576+
577+
assert!(result.is_ok());
578+
// The "keep-me" org entry was removed; the default-org entry for
579+
// the same site survives.
580+
assert!(
581+
remaining.iter().any(|s| s.site == site && s.org.is_none()),
582+
"default-org session for site should remain after logging out of a different org"
583+
);
584+
assert!(
585+
!remaining
586+
.iter()
587+
.any(|s| s.site == site && s.org.as_deref() == Some("keep-me")),
588+
"logged-out org session should be removed"
589+
);
590+
}
591+
}

0 commit comments

Comments
 (0)