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