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