1use std::path::Path;
7
8use chrono::Utc;
9
10use crate::actions::auth_state::read_auth_state;
11use crate::cli::StatusArgs;
12use crate::config::{AuthCache, Config, TokenOrigin, resolve_token};
13use crate::diagnostic::Diagnostic;
14use crate::render::auth::AuthStatusOutcome;
15use crate::render::{MetaContext, Renderer};
16
17pub fn run(args: &StatusArgs, cfg: &Config, renderer: &mut dyn Renderer) -> Result<(), Diagnostic> {
22 let env_token = std::env::var("DSP_TOKEN").ok();
23 run_impl(args, cfg, renderer, None, env_token)
24}
25
26fn run_impl(
27 _args: &StatusArgs,
28 cfg: &Config,
29 renderer: &mut dyn Renderer,
30 cache_path: Option<&Path>,
31 env_token: Option<String>,
32) -> Result<(), Diagnostic> {
33 let env_token_would_win = env_token.as_deref().map(str::trim).map(|s| !s.is_empty()).unwrap_or(false);
38
39 let cache_result = match cache_path {
40 Some(p) => AuthCache::load_from(p),
41 None => AuthCache::load(),
42 };
43 let cache = match cache_result {
44 Ok(c) => c,
45 Err(e) if env_token_would_win => {
46 crate::util::warn_auth_cache_load_failed(&e, "DSP_TOKEN is set, falling through to env token");
47 AuthCache::default()
48 }
49 Err(e) => return Err(e),
50 };
51
52 let (outcome, resolved_opt) = match resolve_token(env_token, &cache, &cfg.server) {
53 Some(resolved) if resolved.origin == TokenOrigin::Env => {
54 let token = resolved.token.clone();
56 let now = Utc::now();
57 let expires_at = crate::client::jwt::extract_exp(&token);
61 let expired = expires_at.map(|t| t < now).unwrap_or(false);
62 (
63 AuthStatusOutcome::AuthenticatedViaEnv { server: cfg.server.clone(), expires_at, expired },
64 Some(resolved),
65 )
66 }
67 Some(resolved) => {
68 let user = cache.user(&cfg.server).map(str::to_owned);
71 let expires_at = cache.expires_at(&cfg.server);
72 let expired = expires_at.map(|t| t < Utc::now()).unwrap_or(false);
73 (
74 AuthStatusOutcome::LoggedIn { server: cfg.server.clone(), user, expires_at, expired },
75 Some(resolved),
76 )
77 }
78 None => (AuthStatusOutcome::NotLoggedIn { server: cfg.server.clone() }, None),
79 };
80
81 let auth_state = read_auth_state(resolved_opt.as_ref(), &cache, &cfg.server);
86
87 let meta = MetaContext {
88 server_label: cfg.server.clone(),
89 auth_state,
90 filter_warning: None,
91 count_caveat: None,
92 count_cost: None,
93 };
94
95 renderer.auth_status(&outcome, &meta)?;
96 Ok(())
97}
98
99#[cfg(test)]
100mod tests {
101 use std::sync::{Arc, Mutex};
102
103 use chrono::{TimeZone, Utc};
104 use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
105 use tempfile::TempDir;
106
107 use super::run_impl;
108 use crate::cli::{FormatArgs, StatusArgs};
109 use crate::config::auth_cache::ServerEntry;
110 use crate::config::{AuthCache, Config};
111 use crate::diagnostic::Diagnostic;
112 use crate::render::auth::{AuthLoginOutcome, AuthLogoutOutcome, AuthSetTokenOutcome, AuthStatusOutcome};
113 use crate::render::{Format, MetaContext, Renderer};
114
115 struct LoggedInRecord {
118 server: String,
119 user: Option<String>,
120 expires_at: Option<chrono::DateTime<Utc>>,
121 expired: bool,
122 }
123
124 struct EnvRecord {
125 server: String,
126 expires_at: Option<chrono::DateTime<Utc>>,
127 expired: bool,
128 }
129
130 struct RecordingRenderer {
131 status_outcome: Option<LoggedInRecord>,
132 env_outcome: Option<EnvRecord>,
133 not_logged_in: Option<String>,
134 last_auth_state: Option<String>,
135 }
136
137 impl RecordingRenderer {
138 fn new() -> Self {
139 Self {
140 status_outcome: None,
141 env_outcome: None,
142 not_logged_in: None,
143 last_auth_state: None,
144 }
145 }
146 }
147
148 impl Renderer for RecordingRenderer {
149 fn diagnostic(&mut self, _diag: &Diagnostic, _meta: &MetaContext) -> Result<(), Diagnostic> {
150 Ok(())
151 }
152
153 fn auth_login(&mut self, _outcome: &AuthLoginOutcome, _meta: &MetaContext) -> Result<(), Diagnostic> {
154 Ok(())
155 }
156
157 fn auth_status(&mut self, outcome: &AuthStatusOutcome, meta: &MetaContext) -> Result<(), Diagnostic> {
158 self.last_auth_state = Some(meta.auth_state.clone());
159 match outcome {
160 AuthStatusOutcome::LoggedIn { server, user, expires_at, expired } => {
161 self.status_outcome = Some(LoggedInRecord {
162 server: server.clone(),
163 user: user.clone(),
164 expires_at: *expires_at,
165 expired: *expired,
166 });
167 }
168 AuthStatusOutcome::AuthenticatedViaEnv { server, expires_at, expired } => {
169 self.env_outcome = Some(EnvRecord {
170 server: server.clone(),
171 expires_at: *expires_at,
172 expired: *expired,
173 });
174 }
175 AuthStatusOutcome::NotLoggedIn { server } => {
176 self.not_logged_in = Some(server.clone());
177 }
178 }
179 Ok(())
180 }
181
182 fn auth_logout(&mut self, _outcome: &AuthLogoutOutcome, _meta: &MetaContext) -> Result<(), Diagnostic> {
183 Ok(())
184 }
185
186 fn auth_set_token(&mut self, _outcome: &AuthSetTokenOutcome, _meta: &MetaContext) -> Result<(), Diagnostic> {
187 Ok(())
188 }
189
190 fn project_dump(
191 &mut self,
192 _outcome: &crate::render::DumpOutcome,
193 _meta: &MetaContext,
194 ) -> Result<(), Diagnostic> {
195 Ok(())
196 }
197
198 fn project_dump_deleted(
199 &mut self,
200 _outcome: &crate::render::DumpDeleteOutcome,
201 _meta: &MetaContext,
202 ) -> Result<(), Diagnostic> {
203 Ok(())
204 }
205
206 fn projects(&mut self, _view: &crate::render::ProjectListView, _meta: &MetaContext) -> Result<(), Diagnostic> {
207 Ok(())
208 }
209
210 fn project_describe(
211 &mut self,
212 _project: &crate::model::ProjectDetail,
213 _meta: &MetaContext,
214 ) -> Result<(), Diagnostic> {
215 Ok(())
216 }
217
218 fn data_models(
219 &mut self,
220 _view: &crate::render::DataModelListView,
221 _meta: &MetaContext,
222 ) -> Result<(), Diagnostic> {
223 Ok(())
224 }
225
226 fn data_model_describe(
227 &mut self,
228 _detail: &crate::model::DataModelDetail,
229 _meta: &MetaContext,
230 ) -> Result<(), Diagnostic> {
231 Ok(())
232 }
233
234 fn resource_types(
235 &mut self,
236 _view: &crate::render::ResourceTypeListView,
237 _meta: &MetaContext,
238 ) -> Result<(), Diagnostic> {
239 Ok(())
240 }
241
242 fn resource_type_describe(
243 &mut self,
244 _detail: &crate::model::ResourceTypeDetail,
245 _meta: &MetaContext,
246 ) -> Result<(), Diagnostic> {
247 unimplemented!("resource_type_describe not used in status tests")
248 }
249
250 fn data_model_structure(
251 &mut self,
252 _structure: &crate::model::DataModelStructure,
253 _meta: &MetaContext,
254 ) -> Result<(), Diagnostic> {
255 unimplemented!("data_model_structure not used in status tests")
256 }
257
258 fn resources(
259 &mut self,
260 _view: &crate::render::ResourceListView,
261 _meta: &MetaContext,
262 ) -> Result<(), Diagnostic> {
263 Ok(())
264 }
265
266 fn resource_describe(
267 &mut self,
268 _detail: &crate::model::ResourceDetail,
269 _meta: &MetaContext,
270 ) -> Result<(), Diagnostic> {
271 Ok(())
272 }
273
274 fn vocabularies(
275 &mut self,
276 _view: &crate::render::VocabularyListView,
277 _meta: &MetaContext,
278 ) -> Result<(), Diagnostic> {
279 unimplemented!("not exercised by this file's tests")
280 }
281
282 fn vocabulary_describe(
283 &mut self,
284 _detail: &crate::model::VocabularyDetail,
285 _meta: &MetaContext,
286 ) -> Result<(), Diagnostic> {
287 unimplemented!("not exercised by this file's tests")
288 }
289 }
290
291 fn make_args(server: &str) -> (StatusArgs, Config) {
294 let args = StatusArgs {
295 server: Some(server.to_string()),
296 format: FormatArgs {
297 format: Format::Prose,
298 json: false,
299 lines: false,
300 columns: None,
301 no_header: false,
302 header_only: false,
303 },
304 };
305 let cfg = Config { server: server.to_string() };
306 (args, cfg)
307 }
308
309 fn fixed_future() -> chrono::DateTime<Utc> {
310 Utc.with_ymd_and_hms(2099, 1, 1, 0, 0, 0).unwrap()
311 }
312
313 fn fixed_past() -> chrono::DateTime<Utc> {
314 Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap()
315 }
316
317 fn make_jwt(payload: &serde_json::Value) -> String {
320 encode(&Header::new(Algorithm::HS256), payload, &EncodingKey::from_secret(b"unused"))
321 .expect("test JWT encoding should not fail")
322 }
323
324 fn make_jwt_with_exp(exp_ts: i64) -> String {
325 make_jwt(&serde_json::json!({ "exp": exp_ts }))
326 }
327
328 #[test]
331 fn logged_in_entry_produces_logged_in_outcome() {
332 let dir = TempDir::new().unwrap();
333 let cache_path = dir.path().join("auth.toml");
334 let (args, cfg) = make_args("https://api.test.dasch.swiss");
335
336 let mut cache = AuthCache::default();
337 cache.set_entry(
338 "https://api.test.dasch.swiss",
339 ServerEntry {
340 token: "tok-123".to_string(),
341 user: Some("u@x.test".to_string()),
342 acquired_at: None,
343 expires_at: Some(fixed_future()),
344 },
345 );
346 cache.save_to(&cache_path).unwrap();
347
348 let mut renderer = RecordingRenderer::new();
349 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), None).unwrap();
350
351 let rec = renderer.status_outcome.unwrap();
352 assert_eq!(rec.server, "https://api.test.dasch.swiss");
353 assert_eq!(rec.user.as_deref(), Some("u@x.test"));
354 assert_eq!(rec.expires_at, Some(fixed_future()));
355 assert!(!rec.expired, "token with future expiry should not be expired");
356 }
357
358 #[test]
359 fn empty_cache_produces_not_logged_in_outcome() {
360 let dir = TempDir::new().unwrap();
361 let cache_path = dir.path().join("auth.toml");
362 let (args, cfg) = make_args("https://api.test.dasch.swiss");
363
364 let mut renderer = RecordingRenderer::new();
365 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), None).unwrap();
366
367 assert!(renderer.status_outcome.is_none(), "expected no LoggedIn outcome");
368 assert_eq!(renderer.not_logged_in.as_deref(), Some("https://api.test.dasch.swiss"));
369 }
370
371 #[test]
372 fn expired_entry_produces_logged_in_with_expired_true() {
373 let dir = TempDir::new().unwrap();
374 let cache_path = dir.path().join("auth.toml");
375 let (args, cfg) = make_args("https://api.test.dasch.swiss");
376
377 let mut cache = AuthCache::default();
378 cache.set_entry(
379 "https://api.test.dasch.swiss",
380 ServerEntry {
381 token: "old-tok".to_string(),
382 user: Some("u@x.test".to_string()),
383 acquired_at: None,
384 expires_at: Some(fixed_past()),
385 },
386 );
387 cache.save_to(&cache_path).unwrap();
388
389 let mut renderer = RecordingRenderer::new();
390 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), None).unwrap();
391
392 let rec = renderer.status_outcome.unwrap();
393 assert!(rec.expired, "token with past expiry should be expired");
394 }
395
396 #[test]
397 fn run_always_returns_ok_even_for_not_logged_in() {
398 let dir = TempDir::new().unwrap();
399 let cache_path = dir.path().join("auth.toml");
400 let (args, cfg) = make_args("https://api.test.dasch.swiss");
401
402 let mut renderer = RecordingRenderer::new();
403 let result = run_impl(&args, &cfg, &mut renderer, Some(&cache_path), None);
404 assert!(result.is_ok(), "status should always return Ok; got {result:?}");
405 }
406
407 #[test]
410 fn env_jwt_future_exp_produces_authenticated_via_env_not_expired() {
411 let dir = TempDir::new().unwrap();
412 let cache_path = dir.path().join("auth.toml");
413 let (args, cfg) = make_args("https://api.test.dasch.swiss");
414
415 let exp_ts = fixed_future().timestamp();
417 let token = make_jwt_with_exp(exp_ts);
418
419 let mut renderer = RecordingRenderer::new();
420 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), Some(token)).unwrap();
421
422 let rec = renderer.env_outcome.expect("expected AuthenticatedViaEnv outcome");
423 assert_eq!(rec.server, "https://api.test.dasch.swiss");
424 assert!(rec.expires_at.is_some(), "expected Some(expires_at)");
425 assert!(!rec.expired, "future exp should not be expired");
426 assert_eq!(
427 renderer.last_auth_state.as_deref(),
428 Some("authenticated via DSP_TOKEN"),
429 "_meta.auth must use dsp-cli/ADR-0007 presence/origin vocabulary (not expiry-aware)"
430 );
431 }
432
433 #[test]
434 fn env_jwt_past_exp_produces_authenticated_via_env_expired() {
435 let dir = TempDir::new().unwrap();
436 let cache_path = dir.path().join("auth.toml");
437 let (args, cfg) = make_args("https://api.test.dasch.swiss");
438
439 let exp_ts = fixed_past().timestamp();
440 let token = make_jwt_with_exp(exp_ts);
441
442 let mut renderer = RecordingRenderer::new();
443 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), Some(token)).unwrap();
444
445 let rec = renderer.env_outcome.expect("expected AuthenticatedViaEnv outcome");
446 assert!(rec.expired, "past exp should be expired");
447 assert_eq!(
450 renderer.last_auth_state.as_deref(),
451 Some("authenticated via DSP_TOKEN"),
452 "_meta.auth for expired env token must be 'authenticated via DSP_TOKEN'"
453 );
454 assert!(rec.expired, "data output must still report expired==true");
456 }
457
458 #[test]
459 fn env_non_jwt_produces_authenticated_via_env_expiry_unknown() {
460 let dir = TempDir::new().unwrap();
461 let cache_path = dir.path().join("auth.toml");
462 let (args, cfg) = make_args("https://api.test.dasch.swiss");
463
464 let mut renderer = RecordingRenderer::new();
465 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), Some("not-a-jwt".to_string())).unwrap();
466
467 let rec = renderer.env_outcome.expect("expected AuthenticatedViaEnv outcome");
468 assert!(rec.expires_at.is_none(), "non-JWT env token should have expires_at == None");
469 assert!(!rec.expired, "non-JWT env token should not be expired");
470 assert_eq!(
471 renderer.last_auth_state.as_deref(),
472 Some("authenticated via DSP_TOKEN"),
473 "_meta.auth for non-JWT env token must be 'authenticated via DSP_TOKEN'"
474 );
475 }
476
477 #[test]
478 fn env_token_wins_over_valid_cache_entry() {
479 let dir = TempDir::new().unwrap();
480 let cache_path = dir.path().join("auth.toml");
481 let (args, cfg) = make_args("https://api.test.dasch.swiss");
482
483 let mut cache = AuthCache::default();
485 cache.set_entry(
486 "https://api.test.dasch.swiss",
487 ServerEntry {
488 token: "cache-tok".to_string(),
489 user: Some("u@cache.test".to_string()),
490 acquired_at: None,
491 expires_at: Some(fixed_future()),
492 },
493 );
494 cache.save_to(&cache_path).unwrap();
495
496 let exp_ts = fixed_future().timestamp();
497 let env_token = make_jwt_with_exp(exp_ts);
498
499 let mut renderer = RecordingRenderer::new();
500 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), Some(env_token)).unwrap();
501
502 assert!(renderer.env_outcome.is_some(), "env token should win over valid cache entry");
504 assert!(
505 renderer.status_outcome.is_none(),
506 "LoggedIn should not be produced when env token is present"
507 );
508 assert_eq!(
509 renderer.last_auth_state.as_deref(),
510 Some("authenticated via DSP_TOKEN"),
511 "env win over valid cache should report 'authenticated via DSP_TOKEN'"
512 );
513 }
514
515 #[test]
516 fn env_token_wins_over_expired_cache_entry() {
517 let dir = TempDir::new().unwrap();
518 let cache_path = dir.path().join("auth.toml");
519 let (args, cfg) = make_args("https://api.test.dasch.swiss");
520
521 let mut cache = AuthCache::default();
523 cache.set_entry(
524 "https://api.test.dasch.swiss",
525 ServerEntry {
526 token: "old-cache-tok".to_string(),
527 user: Some("u@cache.test".to_string()),
528 acquired_at: None,
529 expires_at: Some(fixed_past()),
530 },
531 );
532 cache.save_to(&cache_path).unwrap();
533
534 let exp_ts = fixed_future().timestamp();
535 let env_token = make_jwt_with_exp(exp_ts);
536
537 let mut renderer = RecordingRenderer::new();
538 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), Some(env_token)).unwrap();
539
540 assert!(renderer.env_outcome.is_some(), "env token should win over expired cache entry");
541 assert!(
542 renderer.status_outcome.is_none(),
543 "LoggedIn should not be produced when env token is present"
544 );
545 assert_eq!(
546 renderer.last_auth_state.as_deref(),
547 Some("authenticated via DSP_TOKEN"),
548 "env win over expired cache should report 'authenticated via DSP_TOKEN'"
549 );
550 }
551
552 #[test]
553 fn whitespace_only_env_with_expired_cache_falls_through_to_cache() {
554 let dir = TempDir::new().unwrap();
555 let cache_path = dir.path().join("auth.toml");
556 let (args, cfg) = make_args("https://api.test.dasch.swiss");
557
558 let mut cache = AuthCache::default();
559 cache.set_entry(
560 "https://api.test.dasch.swiss",
561 ServerEntry {
562 token: "old-tok".to_string(),
563 user: Some("u@x.test".to_string()),
564 acquired_at: None,
565 expires_at: Some(fixed_past()),
566 },
567 );
568 cache.save_to(&cache_path).unwrap();
569
570 let mut renderer = RecordingRenderer::new();
571 run_impl(
572 &args,
573 &cfg,
574 &mut renderer,
575 Some(&cache_path),
576 Some(" ".to_string()), )
578 .unwrap();
579
580 let rec = renderer
582 .status_outcome
583 .expect("expected LoggedIn outcome from cache fall-through");
584 assert!(rec.expired, "cache entry has past expiry; expired should be true");
585 assert!(
586 renderer.env_outcome.is_none(),
587 "whitespace env should not produce AuthenticatedViaEnv"
588 );
589 assert_eq!(
593 renderer.last_auth_state.as_deref(),
594 Some("authenticated as u@x.test"),
595 "_meta.auth for expired cache token must be 'authenticated as <user>'"
596 );
597 }
598
599 #[test]
600 fn env_token_wins_when_cache_is_corrupt() {
601 let dir = TempDir::new().unwrap();
604 let cache_path = dir.path().join("auth.toml");
605 std::fs::write(&cache_path, b"not valid toml [[[").unwrap();
606
607 let (args, cfg) = make_args("https://api.test.dasch.swiss");
608 let exp_ts = fixed_future().timestamp();
609 let env_token = make_jwt_with_exp(exp_ts);
610
611 let mut renderer = RecordingRenderer::new();
612 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), Some(env_token)).unwrap();
613
614 assert!(renderer.env_outcome.is_some(), "env token should win over corrupt cache");
615 assert!(
616 renderer.status_outcome.is_none(),
617 "LoggedIn should not be produced when env token is present"
618 );
619 assert_eq!(renderer.last_auth_state.as_deref(), Some("authenticated via DSP_TOKEN"),);
620 }
621
622 #[derive(Clone)]
626 struct CapturingWriter(Arc<Mutex<Vec<u8>>>);
627
628 impl std::io::Write for CapturingWriter {
629 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
630 self.0.lock().unwrap().extend_from_slice(buf);
631 Ok(buf.len())
632 }
633
634 fn flush(&mut self) -> std::io::Result<()> {
635 Ok(())
636 }
637 }
638
639 #[test]
640 fn corrupt_cache_does_not_leak_token_bytes_at_default_verbosity() {
641 const FAKE_TOKEN: &str = "eyFAKE.TOKEN.LEAK-DO-NOT-SHOW-1234567890";
648
649 let dir = TempDir::new().unwrap();
650 let cache_path = dir.path().join("auth.toml");
651 std::fs::write(
654 &cache_path,
655 format!("[\"https://api.test.dasch.swiss\"]\ntoken = \"{FAKE_TOKEN}\n"),
656 )
657 .unwrap();
658
659 let (args, cfg) = make_args("https://api.test.dasch.swiss");
660 let exp_ts = fixed_future().timestamp();
661 let env_token = make_jwt_with_exp(exp_ts);
662
663 let captured = Arc::new(Mutex::new(Vec::<u8>::new()));
664 let writer = CapturingWriter(captured.clone());
665 let subscriber = tracing_subscriber::fmt()
668 .with_max_level(tracing::Level::WARN)
669 .with_writer(move || writer.clone())
670 .finish();
671
672 let mut renderer = RecordingRenderer::new();
673 tracing::subscriber::with_default(subscriber, || {
674 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), Some(env_token)).unwrap();
675 });
676
677 let logged = String::from_utf8(captured.lock().unwrap().clone()).unwrap();
678 assert!(
679 !logged.contains(FAKE_TOKEN),
680 "warn-level log leaked token bytes from the toml parse error: {logged}"
681 );
682 assert!(
683 logged.contains("auth cache"),
684 "expected a body-free warning that the auth cache could not be read; got: {logged}"
685 );
686 }
687
688 #[test]
689 fn corrupt_cache_without_env_token_still_errors() {
690 let dir = TempDir::new().unwrap();
695 let cache_path = dir.path().join("auth.toml");
696 std::fs::write(&cache_path, b"not valid toml [[[").unwrap();
697
698 let (args, cfg) = make_args("https://api.test.dasch.swiss");
699 let mut renderer = RecordingRenderer::new();
700 let result = run_impl(&args, &cfg, &mut renderer, Some(&cache_path), None);
701 assert!(
702 result.is_err(),
703 "without env token, a corrupt cache should propagate; got {result:?}"
704 );
705 }
706
707 #[test]
708 fn whitespace_only_env_with_corrupt_cache_still_errors() {
709 let dir = TempDir::new().unwrap();
713 let cache_path = dir.path().join("auth.toml");
714 std::fs::write(&cache_path, b"not valid toml [[[").unwrap();
715
716 let (args, cfg) = make_args("https://api.test.dasch.swiss");
717 let mut renderer = RecordingRenderer::new();
718 let result = run_impl(&args, &cfg, &mut renderer, Some(&cache_path), Some(" ".to_string()));
719 assert!(
720 result.is_err(),
721 "whitespace env token should not swallow corrupt-cache error; got {result:?}"
722 );
723 }
724
725 #[test]
726 fn absent_env_with_cache_entry_produces_logged_in() {
727 let dir = TempDir::new().unwrap();
728 let cache_path = dir.path().join("auth.toml");
729 let (args, cfg) = make_args("https://api.test.dasch.swiss");
730
731 let mut cache = AuthCache::default();
732 cache.set_entry(
733 "https://api.test.dasch.swiss",
734 ServerEntry {
735 token: "tok-abc".to_string(),
736 user: Some("u@x.test".to_string()),
737 acquired_at: None,
738 expires_at: Some(fixed_future()),
739 },
740 );
741 cache.save_to(&cache_path).unwrap();
742
743 let mut renderer = RecordingRenderer::new();
744 run_impl(&args, &cfg, &mut renderer, Some(&cache_path), None).unwrap();
745
746 assert!(
747 renderer.status_outcome.is_some(),
748 "absent env + cache entry should produce LoggedIn"
749 );
750 assert!(
751 renderer.env_outcome.is_none(),
752 "absent env should not produce AuthenticatedViaEnv"
753 );
754 }
755}