1use std::path::Path;
7
8use crate::cli::LogoutArgs;
9use crate::config::{AuthCache, Config};
10use crate::diagnostic::Diagnostic;
11use crate::render::auth::AuthLogoutOutcome;
12use crate::render::{MetaContext, Renderer};
13
14pub fn run(args: &LogoutArgs, cfg: &Config, renderer: &mut dyn Renderer) -> Result<(), Diagnostic> {
19 run_impl(args, cfg, renderer, None)
20}
21
22fn run_impl(
23 _args: &LogoutArgs,
24 cfg: &Config,
25 renderer: &mut dyn Renderer,
26 cache_path: Option<&Path>,
27) -> Result<(), Diagnostic> {
28 let mut cache = match cache_path {
29 Some(p) => AuthCache::load_from(p)?,
30 None => AuthCache::load()?,
31 };
32
33 let was_cached = cache.remove(&cfg.server);
34
35 if was_cached {
38 match cache_path {
39 Some(p) => cache.save_to(p)?,
40 None => cache.save()?,
41 }
42 }
43
44 let meta = MetaContext {
49 server_label: cfg.server.clone(),
50 auth_state: crate::actions::auth_state::read_auth_state(None, &cache, &cfg.server),
51 filter_warning: None,
52 count_caveat: None,
53 count_cost: None,
54 };
55
56 let outcome = AuthLogoutOutcome { server: cfg.server.clone(), was_cached };
57
58 renderer.auth_logout(&outcome, &meta)?;
59 Ok(())
60}
61
62#[cfg(test)]
63mod tests {
64 use tempfile::TempDir;
65
66 use super::run_impl;
67 use crate::cli::{FormatArgs, LogoutArgs};
68 use crate::config::auth_cache::ServerEntry;
69 use crate::config::{AuthCache, Config};
70 use crate::diagnostic::Diagnostic;
71 use crate::render::auth::{AuthLoginOutcome, AuthLogoutOutcome, AuthSetTokenOutcome, AuthStatusOutcome};
72 use crate::render::{Format, MetaContext, Renderer};
73
74 struct RecordingRenderer {
77 logout_outcome: Option<(String, bool)>,
78 logout_auth_state: Option<String>,
79 }
80
81 impl RecordingRenderer {
82 fn new() -> Self {
83 Self { logout_outcome: None, logout_auth_state: None }
84 }
85 }
86
87 impl Renderer for RecordingRenderer {
88 fn diagnostic(&mut self, _diag: &Diagnostic, _meta: &MetaContext) -> Result<(), Diagnostic> {
89 Ok(())
90 }
91
92 fn auth_login(&mut self, _outcome: &AuthLoginOutcome, _meta: &MetaContext) -> Result<(), Diagnostic> {
93 Ok(())
94 }
95
96 fn auth_status(&mut self, _outcome: &AuthStatusOutcome, _meta: &MetaContext) -> Result<(), Diagnostic> {
97 Ok(())
98 }
99
100 fn auth_logout(&mut self, outcome: &AuthLogoutOutcome, meta: &MetaContext) -> Result<(), Diagnostic> {
101 self.logout_outcome = Some((outcome.server.clone(), outcome.was_cached));
102 self.logout_auth_state = Some(meta.auth_state.clone());
103 Ok(())
104 }
105
106 fn auth_set_token(&mut self, _outcome: &AuthSetTokenOutcome, _meta: &MetaContext) -> Result<(), Diagnostic> {
107 Ok(())
108 }
109
110 fn project_dump(
111 &mut self,
112 _outcome: &crate::render::DumpOutcome,
113 _meta: &MetaContext,
114 ) -> Result<(), Diagnostic> {
115 Ok(())
116 }
117
118 fn project_dump_deleted(
119 &mut self,
120 _outcome: &crate::render::DumpDeleteOutcome,
121 _meta: &MetaContext,
122 ) -> Result<(), Diagnostic> {
123 Ok(())
124 }
125
126 fn projects(&mut self, _view: &crate::render::ProjectListView, _meta: &MetaContext) -> Result<(), Diagnostic> {
127 Ok(())
128 }
129
130 fn project_describe(
131 &mut self,
132 _project: &crate::model::ProjectDetail,
133 _meta: &MetaContext,
134 ) -> Result<(), Diagnostic> {
135 Ok(())
136 }
137
138 fn data_models(
139 &mut self,
140 _view: &crate::render::DataModelListView,
141 _meta: &MetaContext,
142 ) -> Result<(), Diagnostic> {
143 Ok(())
144 }
145
146 fn data_model_describe(
147 &mut self,
148 _detail: &crate::model::DataModelDetail,
149 _meta: &MetaContext,
150 ) -> Result<(), Diagnostic> {
151 Ok(())
152 }
153
154 fn resource_types(
155 &mut self,
156 _view: &crate::render::ResourceTypeListView,
157 _meta: &MetaContext,
158 ) -> Result<(), Diagnostic> {
159 Ok(())
160 }
161
162 fn resource_type_describe(
163 &mut self,
164 _detail: &crate::model::ResourceTypeDetail,
165 _meta: &MetaContext,
166 ) -> Result<(), Diagnostic> {
167 unimplemented!("resource_type_describe not used in logout tests")
168 }
169
170 fn data_model_structure(
171 &mut self,
172 _structure: &crate::model::DataModelStructure,
173 _meta: &MetaContext,
174 ) -> Result<(), Diagnostic> {
175 unimplemented!("data_model_structure not used in logout tests")
176 }
177
178 fn resources(
179 &mut self,
180 _view: &crate::render::ResourceListView,
181 _meta: &MetaContext,
182 ) -> Result<(), Diagnostic> {
183 Ok(())
184 }
185
186 fn resource_describe(
187 &mut self,
188 _detail: &crate::model::ResourceDetail,
189 _meta: &MetaContext,
190 ) -> Result<(), Diagnostic> {
191 Ok(())
192 }
193
194 fn vocabularies(
195 &mut self,
196 _view: &crate::render::VocabularyListView,
197 _meta: &MetaContext,
198 ) -> Result<(), Diagnostic> {
199 unimplemented!("not exercised by this file's tests")
200 }
201
202 fn vocabulary_describe(
203 &mut self,
204 _detail: &crate::model::VocabularyDetail,
205 _meta: &MetaContext,
206 ) -> Result<(), Diagnostic> {
207 unimplemented!("not exercised by this file's tests")
208 }
209 }
210
211 fn make_args(server: &str) -> (LogoutArgs, Config) {
214 let args = LogoutArgs {
215 server: Some(server.to_string()),
216 format: FormatArgs {
217 format: Format::Prose,
218 json: false,
219 lines: false,
220 columns: None,
221 no_header: false,
222 header_only: false,
223 },
224 };
225 let cfg = Config { server: server.to_string() };
226 (args, cfg)
227 }
228
229 #[test]
232 fn logout_removes_entry_and_reports_was_cached() {
233 let dir = TempDir::new().unwrap();
234 let cache_path = dir.path().join("auth.toml");
235 let (args, cfg) = make_args("https://api.test.dasch.swiss");
236
237 let mut cache = AuthCache::default();
239 cache.set_entry(
240 "https://api.test.dasch.swiss",
241 ServerEntry {
242 token: "tok-abc".to_string(),
243 user: Some("u@x.test".to_string()),
244 acquired_at: None,
245 expires_at: None,
246 },
247 );
248 cache.save_to(&cache_path).unwrap();
249
250 let mut renderer = RecordingRenderer::new();
251 run_impl(&args, &cfg, &mut renderer, Some(&cache_path)).unwrap();
252
253 let (server, was_cached) = renderer.logout_outcome.unwrap();
255 assert_eq!(server, "https://api.test.dasch.swiss");
256 assert!(was_cached, "expected was_cached=true when entry existed");
257
258 let loaded = AuthCache::load_from(&cache_path).unwrap();
260 assert_eq!(loaded.token("https://api.test.dasch.swiss"), None);
261 }
262
263 #[test]
264 fn logout_on_empty_cache_reports_was_cached_false() {
265 let dir = TempDir::new().unwrap();
266 let cache_path = dir.path().join("auth.toml");
267 let (args, cfg) = make_args("https://api.test.dasch.swiss");
268
269 let mut renderer = RecordingRenderer::new();
270 run_impl(&args, &cfg, &mut renderer, Some(&cache_path)).unwrap();
271
272 let (_, was_cached) = renderer.logout_outcome.unwrap();
273 assert!(!was_cached, "expected was_cached=false when cache was empty");
274 }
275
276 #[test]
277 fn logout_on_empty_cache_does_not_create_cache_file() {
278 let dir = TempDir::new().unwrap();
279 let cache_path = dir.path().join("auth.toml");
280 let (args, cfg) = make_args("https://api.test.dasch.swiss");
281
282 let mut renderer = RecordingRenderer::new();
283 run_impl(&args, &cfg, &mut renderer, Some(&cache_path)).unwrap();
284
285 assert!(!cache_path.exists(), "logout on empty cache must not create the cache file");
286 }
287
288 #[test]
289 fn run_always_returns_ok() {
290 let dir = TempDir::new().unwrap();
291 let cache_path = dir.path().join("auth.toml");
292 let (args, cfg) = make_args("https://api.test.dasch.swiss");
293
294 let mut renderer = RecordingRenderer::new();
295 let result = run_impl(&args, &cfg, &mut renderer, Some(&cache_path));
296 assert!(result.is_ok(), "logout should always return Ok; got {result:?}");
297 }
298
299 #[test]
300 fn logout_meta_auth_is_anonymous() {
301 let dir = TempDir::new().unwrap();
304 let cache_path = dir.path().join("auth.toml");
305 let (args, cfg) = make_args("https://api.test.dasch.swiss");
306
307 let mut renderer = RecordingRenderer::new();
308 run_impl(&args, &cfg, &mut renderer, Some(&cache_path)).unwrap();
309
310 assert_eq!(
311 renderer.logout_auth_state.as_deref(),
312 Some("anonymous"),
313 "_meta.auth after logout must be 'anonymous' (dsp-cli/ADR-0007 vocabulary)"
314 );
315 }
316}