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 {
57 server: cfg.server.clone(),
58 was_cached,
59 };
60
61 renderer.auth_logout(&outcome, &meta)?;
62 Ok(())
63}
64
65#[cfg(test)]
66mod tests {
67 use tempfile::TempDir;
68
69 use super::run_impl;
70 use crate::cli::{FormatArgs, LogoutArgs};
71 use crate::config::auth_cache::ServerEntry;
72 use crate::config::{AuthCache, Config};
73 use crate::diagnostic::Diagnostic;
74 use crate::render::auth::{
75 AuthLoginOutcome, AuthLogoutOutcome, AuthSetTokenOutcome, AuthStatusOutcome,
76 };
77 use crate::render::{Format, MetaContext, Renderer};
78
79 struct RecordingRenderer {
82 logout_outcome: Option<(String, bool)>,
83 logout_auth_state: Option<String>,
84 }
85
86 impl RecordingRenderer {
87 fn new() -> Self {
88 Self {
89 logout_outcome: None,
90 logout_auth_state: None,
91 }
92 }
93 }
94
95 impl Renderer for RecordingRenderer {
96 fn diagnostic(
97 &mut self,
98 _diag: &Diagnostic,
99 _meta: &MetaContext,
100 ) -> Result<(), Diagnostic> {
101 Ok(())
102 }
103
104 fn auth_login(
105 &mut self,
106 _outcome: &AuthLoginOutcome,
107 _meta: &MetaContext,
108 ) -> Result<(), Diagnostic> {
109 Ok(())
110 }
111
112 fn auth_status(
113 &mut self,
114 _outcome: &AuthStatusOutcome,
115 _meta: &MetaContext,
116 ) -> Result<(), Diagnostic> {
117 Ok(())
118 }
119
120 fn auth_logout(
121 &mut self,
122 outcome: &AuthLogoutOutcome,
123 meta: &MetaContext,
124 ) -> Result<(), Diagnostic> {
125 self.logout_outcome = Some((outcome.server.clone(), outcome.was_cached));
126 self.logout_auth_state = Some(meta.auth_state.clone());
127 Ok(())
128 }
129
130 fn auth_set_token(
131 &mut self,
132 _outcome: &AuthSetTokenOutcome,
133 _meta: &MetaContext,
134 ) -> Result<(), Diagnostic> {
135 Ok(())
136 }
137
138 fn project_dump(
139 &mut self,
140 _outcome: &crate::render::DumpOutcome,
141 _meta: &MetaContext,
142 ) -> Result<(), Diagnostic> {
143 Ok(())
144 }
145
146 fn project_dump_deleted(
147 &mut self,
148 _outcome: &crate::render::DumpDeleteOutcome,
149 _meta: &MetaContext,
150 ) -> Result<(), Diagnostic> {
151 Ok(())
152 }
153
154 fn projects(
155 &mut self,
156 _view: &crate::render::ProjectListView,
157 _meta: &MetaContext,
158 ) -> Result<(), Diagnostic> {
159 Ok(())
160 }
161
162 fn project_describe(
163 &mut self,
164 _project: &crate::model::ProjectDetail,
165 _meta: &MetaContext,
166 ) -> Result<(), Diagnostic> {
167 Ok(())
168 }
169
170 fn data_models(
171 &mut self,
172 _view: &crate::render::DataModelListView,
173 _meta: &MetaContext,
174 ) -> Result<(), Diagnostic> {
175 Ok(())
176 }
177
178 fn data_model_describe(
179 &mut self,
180 _detail: &crate::model::DataModelDetail,
181 _meta: &MetaContext,
182 ) -> Result<(), Diagnostic> {
183 Ok(())
184 }
185
186 fn resource_types(
187 &mut self,
188 _view: &crate::render::ResourceTypeListView,
189 _meta: &MetaContext,
190 ) -> Result<(), Diagnostic> {
191 Ok(())
192 }
193
194 fn resource_type_describe(
195 &mut self,
196 _detail: &crate::model::ResourceTypeDetail,
197 _meta: &MetaContext,
198 ) -> Result<(), Diagnostic> {
199 unimplemented!("resource_type_describe not used in logout tests")
200 }
201
202 fn data_model_structure(
203 &mut self,
204 _structure: &crate::model::DataModelStructure,
205 _meta: &MetaContext,
206 ) -> Result<(), Diagnostic> {
207 unimplemented!("data_model_structure not used in logout tests")
208 }
209
210 fn resources(
211 &mut self,
212 _view: &crate::render::ResourceListView,
213 _meta: &MetaContext,
214 ) -> Result<(), Diagnostic> {
215 Ok(())
216 }
217
218 fn resource_describe(
219 &mut self,
220 _detail: &crate::model::ResourceDetail,
221 _meta: &MetaContext,
222 ) -> Result<(), Diagnostic> {
223 Ok(())
224 }
225
226 fn vocabularies(
227 &mut self,
228 _view: &crate::render::VocabularyListView,
229 _meta: &MetaContext,
230 ) -> Result<(), Diagnostic> {
231 unimplemented!("not exercised by this file's tests")
232 }
233
234 fn vocabulary_describe(
235 &mut self,
236 _detail: &crate::model::VocabularyDetail,
237 _meta: &MetaContext,
238 ) -> Result<(), Diagnostic> {
239 unimplemented!("not exercised by this file's tests")
240 }
241 }
242
243 fn make_args(server: &str) -> (LogoutArgs, Config) {
246 let args = LogoutArgs {
247 server: Some(server.to_string()),
248 format: FormatArgs {
249 format: Format::Prose,
250 json: false,
251 lines: false,
252 columns: None,
253 no_header: false,
254 header_only: false,
255 },
256 };
257 let cfg = Config {
258 server: server.to_string(),
259 };
260 (args, cfg)
261 }
262
263 #[test]
266 fn logout_removes_entry_and_reports_was_cached() {
267 let dir = TempDir::new().unwrap();
268 let cache_path = dir.path().join("auth.toml");
269 let (args, cfg) = make_args("https://api.test.dasch.swiss");
270
271 let mut cache = AuthCache::default();
273 cache.set_entry(
274 "https://api.test.dasch.swiss",
275 ServerEntry {
276 token: "tok-abc".to_string(),
277 user: Some("u@x.test".to_string()),
278 acquired_at: None,
279 expires_at: None,
280 },
281 );
282 cache.save_to(&cache_path).unwrap();
283
284 let mut renderer = RecordingRenderer::new();
285 run_impl(&args, &cfg, &mut renderer, Some(&cache_path)).unwrap();
286
287 let (server, was_cached) = renderer.logout_outcome.unwrap();
289 assert_eq!(server, "https://api.test.dasch.swiss");
290 assert!(was_cached, "expected was_cached=true when entry existed");
291
292 let loaded = AuthCache::load_from(&cache_path).unwrap();
294 assert_eq!(loaded.token("https://api.test.dasch.swiss"), None);
295 }
296
297 #[test]
298 fn logout_on_empty_cache_reports_was_cached_false() {
299 let dir = TempDir::new().unwrap();
300 let cache_path = dir.path().join("auth.toml");
301 let (args, cfg) = make_args("https://api.test.dasch.swiss");
302
303 let mut renderer = RecordingRenderer::new();
304 run_impl(&args, &cfg, &mut renderer, Some(&cache_path)).unwrap();
305
306 let (_, was_cached) = renderer.logout_outcome.unwrap();
307 assert!(
308 !was_cached,
309 "expected was_cached=false when cache was empty"
310 );
311 }
312
313 #[test]
314 fn logout_on_empty_cache_does_not_create_cache_file() {
315 let dir = TempDir::new().unwrap();
316 let cache_path = dir.path().join("auth.toml");
317 let (args, cfg) = make_args("https://api.test.dasch.swiss");
318
319 let mut renderer = RecordingRenderer::new();
320 run_impl(&args, &cfg, &mut renderer, Some(&cache_path)).unwrap();
321
322 assert!(
323 !cache_path.exists(),
324 "logout on empty cache must not create the cache file"
325 );
326 }
327
328 #[test]
329 fn run_always_returns_ok() {
330 let dir = TempDir::new().unwrap();
331 let cache_path = dir.path().join("auth.toml");
332 let (args, cfg) = make_args("https://api.test.dasch.swiss");
333
334 let mut renderer = RecordingRenderer::new();
335 let result = run_impl(&args, &cfg, &mut renderer, Some(&cache_path));
336 assert!(
337 result.is_ok(),
338 "logout should always return Ok; got {result:?}"
339 );
340 }
341
342 #[test]
343 fn logout_meta_auth_is_anonymous() {
344 let dir = TempDir::new().unwrap();
347 let cache_path = dir.path().join("auth.toml");
348 let (args, cfg) = make_args("https://api.test.dasch.swiss");
349
350 let mut renderer = RecordingRenderer::new();
351 run_impl(&args, &cfg, &mut renderer, Some(&cache_path)).unwrap();
352
353 assert_eq!(
354 renderer.logout_auth_state.as_deref(),
355 Some("anonymous"),
356 "_meta.auth after logout must be 'anonymous' (ADR-0007 vocabulary)"
357 );
358 }
359}