1use std::path::Path;
8
9use crate::cli::{DataModelDescribeArgs, DataModelListArgs, DataModelStructureArgs};
10use crate::client::DspClient;
11use crate::config::{AuthCache, Config, resolve_token};
12use crate::diagnostic::Diagnostic;
13use crate::render::{DataModelListView, MetaContext, Renderer};
14
15use crate::actions::auth_state::read_auth_state;
16
17pub fn list(
23 args: &DataModelListArgs,
24 cfg: &Config,
25 client: &dyn DspClient,
26 renderer: &mut dyn Renderer,
27) -> Result<(), Diagnostic> {
28 let env_token = std::env::var("DSP_TOKEN").ok();
29 run_list_impl(args, cfg, client, renderer, env_token, None)
30}
31
32fn run_list_impl(
44 args: &DataModelListArgs,
45 cfg: &Config,
46 client: &dyn DspClient,
47 renderer: &mut dyn Renderer,
48 env_token: Option<String>,
49 cache_path: Option<&Path>,
50) -> Result<(), Diagnostic> {
51 let project = args.project.as_deref().ok_or_else(|| {
53 Diagnostic::Usage("--project <shortcode|shortname|IRI> is required".to_string())
54 })?;
55
56 let cache_result = match cache_path {
58 Some(p) => AuthCache::load_from(p),
59 None => AuthCache::load(),
60 };
61 let cache = match cache_result {
62 Ok(c) => c,
63 Err(e) => {
64 tracing::warn!(
65 error = %e,
66 "auth cache load failed; falling back to anonymous for data-model list"
67 );
68 AuthCache::default()
69 }
70 };
71
72 let resolved = resolve_token(env_token, &cache, &cfg.server);
74 let token = resolved.as_ref().map(|r| r.token.as_str());
75
76 let auth_state = read_auth_state(resolved.as_ref(), &cache, &cfg.server);
78
79 let pref = client.resolve_project(&cfg.server, project)?;
81
82 let mut items = client.list_data_models(&cfg.server, &pref.iri, token)?;
84
85 if args.include_builtins {
87 items.extend(crate::client::builtin_data_models());
88 }
89
90 let total = items.len();
92
93 if let Some(ref f) = args.filter {
95 let lower = f.to_lowercase();
96 items.retain(|dm| {
97 dm.name.to_lowercase().contains(&lower)
98 || dm
99 .label
100 .as_deref()
101 .unwrap_or("")
102 .to_lowercase()
103 .contains(&lower)
104 });
105 }
106
107 items.sort_by(|a, b| a.name.cmp(&b.name));
109
110 let view = DataModelListView {
112 items,
113 total,
114 filter: args.filter.clone(),
115 };
116 let meta = MetaContext {
117 server_label: cfg.server.clone(),
118 auth_state,
119 filter_warning: None,
120 };
121 renderer.data_models(&view, &meta)
122}
123
124pub fn describe(
130 args: &DataModelDescribeArgs,
131 cfg: &Config,
132 client: &dyn DspClient,
133 renderer: &mut dyn Renderer,
134) -> Result<(), Diagnostic> {
135 let env_token = std::env::var("DSP_TOKEN").ok();
136 run_describe_impl(args, cfg, client, renderer, env_token, None)
137}
138
139fn run_describe_impl(
151 args: &DataModelDescribeArgs,
152 cfg: &Config,
153 client: &dyn DspClient,
154 renderer: &mut dyn Renderer,
155 env_token: Option<String>,
156 cache_path: Option<&Path>,
157) -> Result<(), Diagnostic> {
158 let project = args.project.as_deref().ok_or_else(|| {
160 Diagnostic::Usage("--project <shortcode|shortname|IRI> is required".to_string())
161 })?;
162
163 let data_model = args
165 .data_model
166 .as_deref()
167 .ok_or_else(|| Diagnostic::Usage("--data-model <name-or-IRI> is required".to_string()))?;
168
169 let cache_result = match cache_path {
171 Some(p) => AuthCache::load_from(p),
172 None => AuthCache::load(),
173 };
174 let cache = match cache_result {
175 Ok(c) => c,
176 Err(e) => {
177 tracing::warn!(
178 error = %e,
179 "auth cache load failed; falling back to anonymous for data-model describe"
180 );
181 AuthCache::default()
182 }
183 };
184
185 let resolved = resolve_token(env_token, &cache, &cfg.server);
187 let token = resolved.as_ref().map(|r| r.token.as_str());
188
189 let auth_state = read_auth_state(resolved.as_ref(), &cache, &cfg.server);
191
192 let pref = client.resolve_project(&cfg.server, project)?;
194
195 let data_models = client.list_data_models(&cfg.server, &pref.iri, token)?;
197
198 let dm_iri = data_models
201 .iter()
202 .find(|dm| {
203 dm.iri == data_model || dm.name.eq_ignore_ascii_case(data_model)
204 })
205 .map(|dm| dm.iri.clone())
206 .ok_or_else(|| {
207 let dm_disp: String = data_model.chars().take(80).collect();
208 let dm_suffix = if data_model.chars().count() > 80 { "…" } else { "" };
209 let proj_disp: String = project.chars().take(80).collect();
210 let proj_suffix = if project.chars().count() > 80 { "…" } else { "" };
211 Diagnostic::NotFound(format!(
212 "data-model '{dm_disp}{dm_suffix}' not found in project '{proj_disp}{proj_suffix}' on {server}. \
213 Run `dsp vre data-model list --project {proj_disp}{proj_suffix} --server {server}` \
214 to see available data-models.",
215 server = cfg.server,
216 ))
217 })?;
218
219 let detail = client.describe_data_model(&cfg.server, &dm_iri, token)?;
221
222 let meta = MetaContext {
224 server_label: cfg.server.clone(),
225 auth_state,
226 filter_warning: None,
227 };
228 renderer.data_model_describe(&detail, &meta)
229}
230
231pub fn structure(
237 args: &DataModelStructureArgs,
238 cfg: &Config,
239 client: &dyn DspClient,
240 renderer: &mut dyn Renderer,
241) -> Result<(), Diagnostic> {
242 let env_token = std::env::var("DSP_TOKEN").ok();
243 run_structure_impl(args, cfg, client, renderer, env_token, None)
244}
245
246fn run_structure_impl(
258 args: &DataModelStructureArgs,
259 cfg: &Config,
260 client: &dyn DspClient,
261 renderer: &mut dyn Renderer,
262 env_token: Option<String>,
263 cache_path: Option<&Path>,
264) -> Result<(), Diagnostic> {
265 let project = args.project.as_deref().ok_or_else(|| {
267 Diagnostic::Usage("--project <shortcode|shortname|IRI> is required".to_string())
268 })?;
269
270 let data_model = args
272 .data_model
273 .as_deref()
274 .ok_or_else(|| Diagnostic::Usage("--data-model <name-or-IRI> is required".to_string()))?;
275
276 let cache_result = match cache_path {
278 Some(p) => AuthCache::load_from(p),
279 None => AuthCache::load(),
280 };
281 let cache = match cache_result {
282 Ok(c) => c,
283 Err(e) => {
284 tracing::warn!(
285 error = %e,
286 "auth cache load failed; falling back to anonymous for data-model structure"
287 );
288 AuthCache::default()
289 }
290 };
291
292 let resolved = resolve_token(env_token, &cache, &cfg.server);
294 let token = resolved.as_ref().map(|r| r.token.as_str());
295
296 let auth_state = read_auth_state(resolved.as_ref(), &cache, &cfg.server);
298
299 let pref = client.resolve_project(&cfg.server, project)?;
301
302 let data_models = client.list_data_models(&cfg.server, &pref.iri, token)?;
304
305 let dm_iri = data_models
307 .iter()
308 .find(|dm| {
309 dm.iri == data_model || dm.name.eq_ignore_ascii_case(data_model)
310 })
311 .map(|dm| dm.iri.clone())
312 .ok_or_else(|| {
313 let dm_disp: String = data_model.chars().take(80).collect();
314 let dm_suffix = if data_model.chars().count() > 80 { "…" } else { "" };
315 let proj_disp: String = project.chars().take(80).collect();
316 let proj_suffix = if project.chars().count() > 80 { "…" } else { "" };
317 Diagnostic::NotFound(format!(
318 "data-model '{dm_disp}{dm_suffix}' not found in project '{proj_disp}{proj_suffix}' on {server}. \
319 Run `dsp vre data-model list --project {proj_disp}{proj_suffix} --server {server}` \
320 to see available data-models.",
321 server = cfg.server,
322 ))
323 })?;
324
325 let mut structure = client.data_model_structure(&cfg.server, &dm_iri, token)?;
327
328 if !args.include_builtins {
330 structure.relations.retain(|r| !r.is_builtin);
331 }
332
333 let meta = MetaContext {
335 server_label: cfg.server.clone(),
336 auth_state,
337 filter_warning: None,
338 };
339 renderer.data_model_structure(&structure, &meta)
340}
341
342#[cfg(test)]
347mod tests {
348 use std::cell::RefCell;
349
350 use tempfile::TempDir;
351
352 use super::{run_describe_impl, run_list_impl, run_structure_impl};
353 use crate::cli::{
354 DataModelDescribeArgs, DataModelListArgs, DataModelStructureArgs, FormatArgs,
355 };
356 use crate::client::DspClient;
357 use crate::config::auth_cache::ServerEntry;
358 use crate::config::{AuthCache, Config};
359 use crate::diagnostic::Diagnostic;
360 use crate::model::ProjectDetail;
361 use crate::model::{
362 DataModel, DataModelDetail, DataModelStructure, ProjectRef, ResourceTypeSummary,
363 };
364 use crate::render::Format;
365 use crate::render::auth::{
366 AuthLoginOutcome, AuthLogoutOutcome, AuthSetTokenOutcome, AuthStatusOutcome,
367 };
368 use crate::render::{
369 DataModelListView, DumpDeleteOutcome, DumpOutcome, MetaContext, ProjectListView, Renderer,
370 };
371
372 struct MockDspClient {
375 resolve_result: Option<Result<ProjectRef, Diagnostic>>,
376 resolve_calls: RefCell<u32>,
377 list_data_models_result: Option<Result<Vec<DataModel>, Diagnostic>>,
379 list_data_models_calls: RefCell<u32>,
380 list_data_models_iri: RefCell<Option<String>>,
381 list_data_models_token: RefCell<Option<Option<String>>>,
382 describe_data_model_result: Option<Result<DataModelDetail, Diagnostic>>,
384 describe_data_model_iri: RefCell<Option<String>>,
386 describe_data_model_calls: RefCell<u32>,
387 data_model_structure_result: Option<Result<DataModelStructure, Diagnostic>>,
389 data_model_structure_iri: RefCell<Option<String>>,
391 data_model_structure_token: RefCell<Option<Option<String>>>,
393 data_model_structure_calls: RefCell<u32>,
394 }
395
396 impl MockDspClient {
397 fn new() -> Self {
398 Self {
399 resolve_result: None,
400 resolve_calls: RefCell::new(0),
401 list_data_models_result: None,
402 list_data_models_calls: RefCell::new(0),
403 list_data_models_iri: RefCell::new(None),
404 list_data_models_token: RefCell::new(None),
405 describe_data_model_result: None,
406 describe_data_model_iri: RefCell::new(None),
407 describe_data_model_calls: RefCell::new(0),
408 data_model_structure_result: None,
409 data_model_structure_iri: RefCell::new(None),
410 data_model_structure_token: RefCell::new(None),
411 data_model_structure_calls: RefCell::new(0),
412 }
413 }
414
415 fn with_resolve_project(mut self, result: Result<ProjectRef, Diagnostic>) -> Self {
416 self.resolve_result = Some(result);
417 self
418 }
419
420 fn with_list_data_models(mut self, result: Result<Vec<DataModel>, Diagnostic>) -> Self {
421 self.list_data_models_result = Some(result);
422 self
423 }
424
425 fn with_describe_data_model(mut self, result: Result<DataModelDetail, Diagnostic>) -> Self {
426 self.describe_data_model_result = Some(result);
427 self
428 }
429
430 fn with_data_model_structure(
431 mut self,
432 result: Result<DataModelStructure, Diagnostic>,
433 ) -> Self {
434 self.data_model_structure_result = Some(result);
435 self
436 }
437
438 fn resolve_calls(&self) -> u32 {
439 *self.resolve_calls.borrow()
440 }
441
442 fn list_data_models_calls(&self) -> u32 {
443 *self.list_data_models_calls.borrow()
444 }
445
446 fn list_data_models_iri(&self) -> Option<String> {
447 self.list_data_models_iri.borrow().clone()
448 }
449
450 fn list_data_models_token(&self) -> Option<Option<String>> {
451 self.list_data_models_token.borrow().clone()
452 }
453
454 fn describe_data_model_calls(&self) -> u32 {
455 *self.describe_data_model_calls.borrow()
456 }
457
458 fn describe_data_model_iri(&self) -> Option<String> {
459 self.describe_data_model_iri.borrow().clone()
460 }
461
462 fn data_model_structure_calls(&self) -> u32 {
463 *self.data_model_structure_calls.borrow()
464 }
465
466 fn data_model_structure_iri(&self) -> Option<String> {
467 self.data_model_structure_iri.borrow().clone()
468 }
469
470 fn data_model_structure_token(&self) -> Option<Option<String>> {
471 self.data_model_structure_token.borrow().clone()
472 }
473 }
474
475 impl DspClient for MockDspClient {
476 fn login(
477 &self,
478 _server: &str,
479 _user: &str,
480 _password: &str,
481 ) -> Result<crate::model::LoginResponse, Diagnostic> {
482 unimplemented!("login not used in data-model action tests")
483 }
484
485 fn resolve_project(&self, _server: &str, _project: &str) -> Result<ProjectRef, Diagnostic> {
486 *self.resolve_calls.borrow_mut() += 1;
487 self.resolve_result
488 .clone()
489 .expect("resolve_result must be set when resolve_project is called")
490 }
491
492 fn create_project_dump(
493 &self,
494 _server: &str,
495 _project_iri: &str,
496 _skip_assets: bool,
497 _token: &str,
498 ) -> Result<crate::model::CreateDumpOutcome, Diagnostic> {
499 unimplemented!("create_project_dump not used in data-model action tests")
500 }
501
502 fn get_project_dump_status(
503 &self,
504 _server: &str,
505 _project_iri: &str,
506 _dump_id: &str,
507 _token: &str,
508 ) -> Result<crate::model::DumpTask, Diagnostic> {
509 unimplemented!("get_project_dump_status not used in data-model action tests")
510 }
511
512 fn download_project_dump(
513 &self,
514 _server: &str,
515 _project_iri: &str,
516 _dump_id: &str,
517 _token: &str,
518 _dest: &mut dyn std::io::Write,
519 ) -> Result<u64, Diagnostic> {
520 unimplemented!("download_project_dump not used in data-model action tests")
521 }
522
523 fn delete_project_dump(
524 &self,
525 _server: &str,
526 _project_iri: &str,
527 _dump_id: &str,
528 _token: &str,
529 ) -> Result<(), Diagnostic> {
530 unimplemented!("delete_project_dump not used in data-model action tests")
531 }
532
533 fn list_projects(
534 &self,
535 _server: &str,
536 _token: Option<&str>,
537 ) -> Result<Vec<crate::model::Project>, Diagnostic> {
538 unimplemented!("list_projects not used in data-model action tests")
539 }
540
541 fn describe_project(
542 &self,
543 _server: &str,
544 _project: &str,
545 _token: Option<&str>,
546 ) -> Result<crate::model::ProjectDetail, Diagnostic> {
547 unimplemented!("describe_project not used in data-model action tests")
548 }
549
550 fn list_data_models(
551 &self,
552 _server: &str,
553 project_iri: &str,
554 token: Option<&str>,
555 ) -> Result<Vec<DataModel>, Diagnostic> {
556 *self.list_data_models_calls.borrow_mut() += 1;
557 *self.list_data_models_iri.borrow_mut() = Some(project_iri.to_string());
558 *self.list_data_models_token.borrow_mut() = Some(token.map(str::to_owned));
559 self.list_data_models_result
560 .clone()
561 .expect("list_data_models_result must be set when list_data_models is called")
562 }
563
564 fn describe_data_model(
565 &self,
566 _server: &str,
567 data_model_iri: &str,
568 _token: Option<&str>,
569 ) -> Result<DataModelDetail, Diagnostic> {
570 *self.describe_data_model_calls.borrow_mut() += 1;
571 *self.describe_data_model_iri.borrow_mut() = Some(data_model_iri.to_string());
572 self.describe_data_model_result
573 .clone()
574 .expect("describe_data_model_result must be set when describe_data_model is called")
575 }
576
577 fn describe_resource_type(
578 &self,
579 _server: &str,
580 _data_model_iri: &str,
581 _resource_type: &str,
582 _token: Option<&str>,
583 ) -> Result<crate::model::ResourceTypeDetail, Diagnostic> {
584 unimplemented!("describe_resource_type not used in data-model action tests")
585 }
586
587 fn data_model_structure(
588 &self,
589 _server: &str,
590 data_model_iri: &str,
591 token: Option<&str>,
592 ) -> Result<DataModelStructure, Diagnostic> {
593 *self.data_model_structure_calls.borrow_mut() += 1;
594 *self.data_model_structure_iri.borrow_mut() = Some(data_model_iri.to_string());
595 *self.data_model_structure_token.borrow_mut() = Some(token.map(str::to_owned));
596 self.data_model_structure_result.clone().unwrap_or_else(|| {
597 unimplemented!("data_model_structure not configured in data-model action tests")
598 })
599 }
600
601 fn list_resources(
602 &self,
603 _server: &str,
604 _project_iri: &str,
605 _resource_type_iri: &str,
606 _order_by: Option<&str>,
607 _page: u32,
608 _token: Option<&str>,
609 ) -> Result<crate::model::ResourcePage, Diagnostic> {
610 unimplemented!("list_resources not used in data-model action tests")
611 }
612
613 fn describe_resource(
614 &self,
615 _server: &str,
616 _resource_iri: &str,
617 _token: Option<&str>,
618 _with_values: bool,
619 ) -> Result<crate::model::ResourceDetail, Diagnostic> {
620 unimplemented!("describe_resource not used in data-model action tests")
621 }
622
623 fn verify_token(&self, _server: &str, _token: &str) -> Result<(), Diagnostic> {
624 unimplemented!("verify_token not used in data-model action tests")
625 }
626 }
627
628 struct RecordingRenderer {
631 data_models_view: Option<DataModelListView>,
632 data_models_meta: Option<MetaContext>,
633 data_model_describe_detail: Option<DataModelDetail>,
635 data_model_describe_meta: Option<MetaContext>,
637 data_model_structure_val: Option<crate::model::DataModelStructure>,
639 data_model_structure_meta: Option<MetaContext>,
641 }
642
643 impl RecordingRenderer {
644 fn new() -> Self {
645 Self {
646 data_models_view: None,
647 data_models_meta: None,
648 data_model_describe_detail: None,
649 data_model_describe_meta: None,
650 data_model_structure_val: None,
651 data_model_structure_meta: None,
652 }
653 }
654 }
655
656 impl Renderer for RecordingRenderer {
657 fn diagnostic(
658 &mut self,
659 _diag: &Diagnostic,
660 _meta: &MetaContext,
661 ) -> Result<(), Diagnostic> {
662 Ok(())
663 }
664
665 fn auth_login(
666 &mut self,
667 _outcome: &AuthLoginOutcome,
668 _meta: &MetaContext,
669 ) -> Result<(), Diagnostic> {
670 Ok(())
671 }
672
673 fn auth_status(
674 &mut self,
675 _outcome: &AuthStatusOutcome,
676 _meta: &MetaContext,
677 ) -> Result<(), Diagnostic> {
678 Ok(())
679 }
680
681 fn auth_logout(
682 &mut self,
683 _outcome: &AuthLogoutOutcome,
684 _meta: &MetaContext,
685 ) -> Result<(), Diagnostic> {
686 Ok(())
687 }
688
689 fn auth_set_token(
690 &mut self,
691 _outcome: &AuthSetTokenOutcome,
692 _meta: &MetaContext,
693 ) -> Result<(), Diagnostic> {
694 Ok(())
695 }
696
697 fn project_dump(
698 &mut self,
699 _outcome: &DumpOutcome,
700 _meta: &MetaContext,
701 ) -> Result<(), Diagnostic> {
702 Ok(())
703 }
704
705 fn project_dump_deleted(
706 &mut self,
707 _outcome: &DumpDeleteOutcome,
708 _meta: &MetaContext,
709 ) -> Result<(), Diagnostic> {
710 Ok(())
711 }
712
713 fn projects(
714 &mut self,
715 _view: &ProjectListView,
716 _meta: &MetaContext,
717 ) -> Result<(), Diagnostic> {
718 Ok(())
719 }
720
721 fn project_describe(
722 &mut self,
723 _project: &ProjectDetail,
724 _meta: &MetaContext,
725 ) -> Result<(), Diagnostic> {
726 Ok(())
727 }
728
729 fn data_models(
730 &mut self,
731 view: &DataModelListView,
732 meta: &MetaContext,
733 ) -> Result<(), Diagnostic> {
734 self.data_models_view = Some(view.clone());
735 self.data_models_meta = Some(meta.clone());
736 Ok(())
737 }
738
739 fn data_model_describe(
740 &mut self,
741 detail: &crate::model::DataModelDetail,
742 meta: &MetaContext,
743 ) -> Result<(), Diagnostic> {
744 self.data_model_describe_detail = Some(detail.clone());
745 self.data_model_describe_meta = Some(meta.clone());
746 Ok(())
747 }
748
749 fn resource_types(
750 &mut self,
751 _view: &crate::render::ResourceTypeListView,
752 _meta: &MetaContext,
753 ) -> Result<(), Diagnostic> {
754 Ok(())
755 }
756
757 fn resource_type_describe(
758 &mut self,
759 _detail: &crate::model::ResourceTypeDetail,
760 _meta: &MetaContext,
761 ) -> Result<(), Diagnostic> {
762 unimplemented!("resource_type_describe not used in data-model action tests")
763 }
764
765 fn data_model_structure(
766 &mut self,
767 structure: &crate::model::DataModelStructure,
768 meta: &MetaContext,
769 ) -> Result<(), Diagnostic> {
770 self.data_model_structure_val = Some(structure.clone());
771 self.data_model_structure_meta = Some(meta.clone());
772 Ok(())
773 }
774
775 fn resources(
776 &mut self,
777 _view: &crate::render::ResourceListView,
778 _meta: &MetaContext,
779 ) -> Result<(), Diagnostic> {
780 Ok(())
781 }
782
783 fn resource_describe(
784 &mut self,
785 _detail: &crate::model::ResourceDetail,
786 _meta: &MetaContext,
787 ) -> Result<(), Diagnostic> {
788 Ok(())
789 }
790 }
791
792 const SERVER: &str = "https://api.test.dasch.swiss";
795
796 fn make_cfg() -> Config {
797 Config {
798 server: SERVER.to_string(),
799 }
800 }
801
802 fn make_project_ref() -> ProjectRef {
803 ProjectRef {
804 iri: "http://rdfh.ch/projects/0801".to_string(),
805 shortcode: "0801".to_string(),
806 shortname: "beol".to_string(),
807 }
808 }
809
810 fn make_args(project: Option<&str>) -> DataModelListArgs {
811 DataModelListArgs {
812 server: Some(SERVER.to_string()),
813 project: project.map(str::to_owned),
814 filter: None,
815 include_builtins: false,
816 format: FormatArgs {
817 format: Format::Prose,
818 json: false,
819 lines: false,
820 columns: None,
821 no_header: false,
822 header_only: false,
823 },
824 }
825 }
826
827 fn make_data_model(name: &str, label: Option<&str>) -> DataModel {
828 DataModel {
829 name: name.to_string(),
830 iri: format!("http://api.test.dasch.swiss/ontology/0801/{name}/v2"),
831 label: label.map(str::to_owned),
832 last_modified: None,
833 is_builtin: false,
834 }
835 }
836
837 fn cache_with_entry(server: &str, token: &str, user: &str) -> AuthCache {
838 let mut cache = AuthCache::default();
839 cache.set_entry(
840 server,
841 ServerEntry {
842 token: token.to_string(),
843 user: Some(user.to_string()),
844 acquired_at: None,
845 expires_at: None,
846 },
847 );
848 cache
849 }
850
851 fn write_cache(dir: &TempDir, cache: &AuthCache) -> std::path::PathBuf {
852 let path = dir.path().join("auth.toml");
853 cache.save_to(&path).expect("failed to write test cache");
854 path
855 }
856
857 #[test]
860 fn test_list_data_models_success_sorted_view() {
861 let project_ref = make_project_ref();
862 let items = vec![
863 make_data_model("zebra", None),
864 make_data_model("apple", Some("Apple data-model")),
865 ];
866
867 let client = MockDspClient::new()
868 .with_resolve_project(Ok(project_ref.clone()))
869 .with_list_data_models(Ok(items));
870
871 let mut renderer = RecordingRenderer::new();
872 let args = make_args(Some("0801"));
873 let result = run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
874
875 assert!(result.is_ok(), "expected Ok, got {:?}", result);
876
877 assert_eq!(client.list_data_models_calls(), 1);
879 assert_eq!(
880 client.list_data_models_iri().as_deref(),
881 Some(project_ref.iri.as_str())
882 );
883 assert_eq!(
885 client.list_data_models_token(),
886 Some(None),
887 "token should be None for anonymous call"
888 );
889
890 let view = renderer
891 .data_models_view
892 .expect("data_models must have been called");
893 assert_eq!(view.items.len(), 2);
895 assert_eq!(view.items[0].name, "apple");
896 assert_eq!(view.items[1].name, "zebra");
897 assert_eq!(view.total, 2);
898 assert!(view.filter.is_none());
899 }
900
901 #[test]
905 fn test_list_data_models_excludes_builtins() {
906 let items = vec![
907 make_data_model("beol", Some("The BEOL data-model")),
908 make_data_model("images", None),
909 ];
910
911 let client = MockDspClient::new()
912 .with_resolve_project(Ok(make_project_ref()))
913 .with_list_data_models(Ok(items));
914
915 let mut renderer = RecordingRenderer::new();
916 let mut args = make_args(Some("0801"));
917 args.include_builtins = false;
918 run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None).expect("expected Ok");
919
920 let view = renderer
921 .data_models_view
922 .expect("data_models must have been called");
923
924 assert_eq!(
927 view.total, 2,
928 "total must equal the number of project data-models (2); \
929 a regression capturing total after filtering would break this"
930 );
931
932 for item in &view.items {
934 assert!(
935 !item.is_builtin,
936 "item '{}' has is_builtin == true but should be false",
937 item.name
938 );
939 }
940
941 let names: Vec<&str> = view.items.iter().map(|i| i.name.as_str()).collect();
943 assert!(
944 !names.contains(&"knora-api"),
945 "knora-api must not appear without --include-builtins"
946 );
947 assert!(
948 !names.contains(&"standoff"),
949 "standoff must not appear without --include-builtins"
950 );
951 assert!(
952 !names.contains(&"salsah-gui"),
953 "salsah-gui must not appear without --include-builtins"
954 );
955 }
956
957 #[test]
958 fn test_list_data_models_include_builtins_appended_and_sorted() {
959 let items = vec![
960 make_data_model("limc", Some("LIMC")),
961 make_data_model("rosetta", None),
962 ];
963
964 let client = MockDspClient::new()
965 .with_resolve_project(Ok(make_project_ref()))
966 .with_list_data_models(Ok(items));
967
968 let mut renderer = RecordingRenderer::new();
969 let mut args = make_args(Some("0801"));
970 args.include_builtins = true;
971 run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None).expect("expected Ok");
972
973 let view = renderer
974 .data_models_view
975 .expect("data_models must have been called");
976
977 assert_eq!(view.total, 5, "total must include builtins before filter");
979 assert_eq!(view.items.len(), 5);
980
981 let names: Vec<&str> = view.items.iter().map(|i| i.name.as_str()).collect();
983 assert_eq!(
984 names,
985 vec!["knora-api", "limc", "rosetta", "salsah-gui", "standoff"]
986 );
987
988 for item in &view.items {
990 if matches!(item.name.as_str(), "knora-api" | "standoff" | "salsah-gui") {
991 assert!(
992 item.is_builtin,
993 "'{}' should have is_builtin == true",
994 item.name
995 );
996 } else {
997 assert!(
998 !item.is_builtin,
999 "'{}' should have is_builtin == false",
1000 item.name
1001 );
1002 }
1003 }
1004 }
1005
1006 #[test]
1007 fn test_list_data_models_missing_project_is_usage_error() {
1008 let client = MockDspClient::new();
1009 let mut renderer = RecordingRenderer::new();
1010 let args = make_args(None); let result = run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1012
1013 assert!(
1014 matches!(result, Err(Diagnostic::Usage(_))),
1015 "missing --project must yield Diagnostic::Usage, got {:?}",
1016 result
1017 );
1018 assert_eq!(
1020 client.resolve_calls(),
1021 0,
1022 "resolve_project must not be called when --project is missing"
1023 );
1024 assert_eq!(
1025 client.list_data_models_calls(),
1026 0,
1027 "list_data_models must not be called when --project is missing"
1028 );
1029 assert!(renderer.data_models_view.is_none());
1031 }
1032
1033 #[test]
1034 fn test_list_data_models_resolve_not_found_propagates() {
1035 let client = MockDspClient::new()
1036 .with_resolve_project(Err(Diagnostic::NotFound("project '9999' not found".into())));
1037
1038 let mut renderer = RecordingRenderer::new();
1039 let args = make_args(Some("9999"));
1040 let result = run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1041
1042 assert!(
1043 matches!(result, Err(Diagnostic::NotFound(_))),
1044 "resolve_project NotFound must propagate, got {:?}",
1045 result
1046 );
1047 assert_eq!(
1048 client.list_data_models_calls(),
1049 0,
1050 "list_data_models must not be called when resolve_project fails"
1051 );
1052 }
1053
1054 #[test]
1055 fn test_list_data_models_list_error_propagates() {
1056 let client = MockDspClient::new()
1057 .with_resolve_project(Ok(make_project_ref()))
1058 .with_list_data_models(Err(Diagnostic::ServerError("server is broken".into())));
1059
1060 let mut renderer = RecordingRenderer::new();
1061 let args = make_args(Some("0801"));
1062 let result = run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1063
1064 assert!(
1065 matches!(result, Err(Diagnostic::ServerError(_))),
1066 "list_data_models error must propagate, got {:?}",
1067 result
1068 );
1069 }
1070
1071 #[test]
1072 fn test_list_data_models_filter_case_insensitive() {
1073 let items = vec![
1074 make_data_model("beol", Some("The BEOL data-model")),
1075 make_data_model("images", Some("Image collection")),
1076 make_data_model("admin", None),
1077 ];
1078
1079 let client = MockDspClient::new()
1080 .with_resolve_project(Ok(make_project_ref()))
1081 .with_list_data_models(Ok(items));
1082
1083 let mut renderer = RecordingRenderer::new();
1084 let mut args = make_args(Some("0801"));
1085 args.filter = Some("beol".to_string());
1086 run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None).expect("expected Ok");
1087
1088 let view = renderer
1089 .data_models_view
1090 .expect("data_models must have been called");
1091
1092 assert_eq!(view.total, 3, "total must be pre-filter count");
1094 assert_eq!(view.items.len(), 1, "only the matching item should remain");
1095 assert_eq!(view.items[0].name, "beol");
1096 assert_eq!(view.filter.as_deref(), Some("beol"));
1097 }
1098
1099 #[test]
1100 fn test_list_data_models_filter_matches_label() {
1101 let items = vec![
1102 make_data_model("ont1", Some("Awesome Label")),
1103 make_data_model("ont2", Some("Other label")),
1104 make_data_model("ont3", None),
1105 ];
1106
1107 let client = MockDspClient::new()
1108 .with_resolve_project(Ok(make_project_ref()))
1109 .with_list_data_models(Ok(items));
1110
1111 let mut renderer = RecordingRenderer::new();
1112 let mut args = make_args(Some("0801"));
1113 args.filter = Some("AWESOME".to_string());
1115 run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None).expect("expected Ok");
1116
1117 let view = renderer
1118 .data_models_view
1119 .expect("data_models must have been called");
1120 assert_eq!(view.total, 3);
1121 assert_eq!(view.items.len(), 1);
1122 assert_eq!(view.items[0].name, "ont1");
1123 }
1124
1125 #[test]
1126 fn test_list_data_models_filter_no_match_yields_empty() {
1127 let items = vec![make_data_model("beol", None)];
1128
1129 let client = MockDspClient::new()
1130 .with_resolve_project(Ok(make_project_ref()))
1131 .with_list_data_models(Ok(items));
1132
1133 let mut renderer = RecordingRenderer::new();
1134 let mut args = make_args(Some("0801"));
1135 args.filter = Some("zzznomatch".to_string());
1136 run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None).expect("expected Ok");
1137
1138 let view = renderer
1139 .data_models_view
1140 .expect("data_models must have been called");
1141 assert_eq!(view.total, 1, "total is pre-filter");
1142 assert_eq!(view.items.len(), 0, "no items match the filter");
1143 }
1144
1145 #[test]
1146 fn test_list_data_models_filter_all_pass_still_renders() {
1147 let items = vec![
1148 make_data_model("beol", Some("beol label")),
1149 make_data_model("images", Some("images label")),
1150 ];
1151
1152 let client = MockDspClient::new()
1153 .with_resolve_project(Ok(make_project_ref()))
1154 .with_list_data_models(Ok(items));
1155
1156 let mut renderer = RecordingRenderer::new();
1157 let mut args = make_args(Some("0801"));
1158 args.filter = Some("label".to_string());
1160 run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None).expect("expected Ok");
1161
1162 let view = renderer
1163 .data_models_view
1164 .expect("data_models must have been called");
1165 assert_eq!(view.total, 2);
1167 assert_eq!(view.items.len(), 2);
1168 assert!(view.filter.is_some());
1169 }
1170
1171 #[test]
1172 fn test_list_data_models_filter_matches_only_builtins() {
1173 let items = vec![
1174 make_data_model("beol", None),
1175 make_data_model("images", None),
1176 ];
1177
1178 let client = MockDspClient::new()
1179 .with_resolve_project(Ok(make_project_ref()))
1180 .with_list_data_models(Ok(items));
1181
1182 let mut renderer = RecordingRenderer::new();
1183 let mut args = make_args(Some("0801"));
1184 args.include_builtins = true;
1185 args.filter = Some("knora".to_string());
1187 run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None).expect("expected Ok");
1188
1189 let view = renderer
1190 .data_models_view
1191 .expect("data_models must have been called");
1192 assert_eq!(view.total, 5, "total must include builtins before filter");
1194 assert_eq!(view.items.len(), 1);
1196 assert_eq!(view.items[0].name, "knora-api");
1197 assert!(view.items[0].is_builtin);
1198 }
1199
1200 #[test]
1201 fn test_list_data_models_filter_excludes_builtins_total_still_counts_them() {
1202 let items = vec![make_data_model("beol", None)];
1203
1204 let client = MockDspClient::new()
1205 .with_resolve_project(Ok(make_project_ref()))
1206 .with_list_data_models(Ok(items));
1207
1208 let mut renderer = RecordingRenderer::new();
1209 let mut args = make_args(Some("0801"));
1210 args.include_builtins = true;
1211 args.filter = Some("beol".to_string());
1213 run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None).expect("expected Ok");
1214
1215 let view = renderer
1216 .data_models_view
1217 .expect("data_models must have been called");
1218 assert_eq!(
1220 view.total, 4,
1221 "total must count builtins even when filtered out"
1222 );
1223 assert_eq!(view.items.len(), 1);
1224 assert_eq!(view.items[0].name, "beol");
1225 }
1226
1227 #[test]
1230 fn test_list_data_models_auth_anonymous_when_no_token() {
1231 let client = MockDspClient::new()
1232 .with_resolve_project(Ok(make_project_ref()))
1233 .with_list_data_models(Ok(vec![]));
1234
1235 let mut renderer = RecordingRenderer::new();
1236 let args = make_args(Some("0801"));
1237 run_list_impl(&args, &make_cfg(), &client, &mut renderer, None, None).expect("expected Ok");
1238
1239 let meta = renderer.data_models_meta.expect("meta must be present");
1240 assert_eq!(meta.auth_state, "anonymous");
1241 assert_eq!(client.list_data_models_token(), Some(None));
1243 }
1244
1245 #[test]
1246 fn test_list_data_models_auth_via_env_token() {
1247 let client = MockDspClient::new()
1248 .with_resolve_project(Ok(make_project_ref()))
1249 .with_list_data_models(Ok(vec![]));
1250
1251 let mut renderer = RecordingRenderer::new();
1252 let args = make_args(Some("0801"));
1253 run_list_impl(
1255 &args,
1256 &make_cfg(),
1257 &client,
1258 &mut renderer,
1259 Some("env-jwt-token".to_string()),
1260 None,
1261 )
1262 .expect("expected Ok");
1263
1264 let meta = renderer.data_models_meta.expect("meta must be present");
1265 assert_eq!(meta.auth_state, "authenticated via DSP_TOKEN");
1266 assert_eq!(
1268 client.list_data_models_token(),
1269 Some(Some("env-jwt-token".to_string()))
1270 );
1271 }
1272
1273 #[test]
1274 fn test_list_data_models_auth_via_cache_with_user() {
1275 let dir = TempDir::new().expect("tempdir");
1276 let cache = cache_with_entry(SERVER, "cache-token-xyz", "user@example.com");
1277 let path = write_cache(&dir, &cache);
1278
1279 let client = MockDspClient::new()
1280 .with_resolve_project(Ok(make_project_ref()))
1281 .with_list_data_models(Ok(vec![]));
1282
1283 let mut renderer = RecordingRenderer::new();
1284 let args = make_args(Some("0801"));
1285 run_list_impl(
1286 &args,
1287 &make_cfg(),
1288 &client,
1289 &mut renderer,
1290 None,
1291 Some(&path),
1292 )
1293 .expect("expected Ok");
1294
1295 let meta = renderer.data_models_meta.expect("meta must be present");
1296 assert_eq!(meta.auth_state, "authenticated as user@example.com");
1297 assert_eq!(
1298 client.list_data_models_token(),
1299 Some(Some("cache-token-xyz".to_string()))
1300 );
1301 }
1302
1303 #[test]
1304 fn test_list_data_models_corrupt_cache_falls_back_to_anonymous() {
1305 let dir = TempDir::new().expect("tempdir");
1307 let bad_path = dir.path().join("nonexistent_auth.toml");
1308
1309 let client = MockDspClient::new()
1310 .with_resolve_project(Ok(make_project_ref()))
1311 .with_list_data_models(Ok(vec![]));
1312
1313 let mut renderer = RecordingRenderer::new();
1314 let args = make_args(Some("0801"));
1315 let result = run_list_impl(
1317 &args,
1318 &make_cfg(),
1319 &client,
1320 &mut renderer,
1321 None,
1322 Some(&bad_path),
1323 );
1324 assert!(
1325 result.is_ok(),
1326 "corrupt/missing cache must not fail: {:?}",
1327 result
1328 );
1329
1330 let meta = renderer.data_models_meta.expect("meta must be present");
1331 assert_eq!(
1332 meta.auth_state, "anonymous",
1333 "corrupt cache + no env token must fall back to anonymous"
1334 );
1335 }
1336
1337 fn make_describe_args(
1340 project: Option<&str>,
1341 data_model: Option<&str>,
1342 ) -> DataModelDescribeArgs {
1343 DataModelDescribeArgs {
1344 server: Some(SERVER.to_string()),
1345 project: project.map(str::to_owned),
1346 data_model: data_model.map(str::to_owned),
1347 format: FormatArgs {
1348 format: Format::Prose,
1349 json: false,
1350 lines: false,
1351 columns: None,
1352 no_header: false,
1353 header_only: false,
1354 },
1355 }
1356 }
1357
1358 fn make_data_model_detail(name: &str) -> DataModelDetail {
1359 DataModelDetail {
1360 name: name.to_string(),
1361 iri: format!("http://api.test.dasch.swiss/ontology/0801/{name}/v2"),
1362 label: Some(format!("The {name} data-model")),
1363 last_modified: Some("2024-05-27T12:00:00Z".to_string()),
1364 resource_types: vec![ResourceTypeSummary {
1365 name: "Letter".to_string(),
1366 iri: format!("http://api.test.dasch.swiss/ontology/0801/{name}/v2#Letter"),
1367 label: Some("Letter".to_string()),
1368 }],
1369 }
1370 }
1371
1372 #[test]
1375 fn test_describe_data_model_success() {
1376 let data_model_iri = "http://api.test.dasch.swiss/ontology/0801/beol/v2";
1377 let detail = make_data_model_detail("beol");
1378 let client = MockDspClient::new()
1379 .with_resolve_project(Ok(make_project_ref()))
1380 .with_list_data_models(Ok(vec![make_data_model(
1381 "beol",
1382 Some("The BEOL data-model"),
1383 )]))
1384 .with_describe_data_model(Ok(detail.clone()));
1385
1386 let mut renderer = RecordingRenderer::new();
1387 let args = make_describe_args(Some("0801"), Some("beol"));
1388 let result = run_describe_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1389
1390 assert!(result.is_ok(), "expected Ok, got {:?}", result);
1391 assert_eq!(client.describe_data_model_calls(), 1);
1392 assert_eq!(
1393 client.describe_data_model_iri().as_deref(),
1394 Some(data_model_iri),
1395 "describe_data_model must be called with the matched IRI"
1396 );
1397 let recorded = renderer
1398 .data_model_describe_detail
1399 .expect("data_model_describe must have been called");
1400 assert_eq!(recorded, detail);
1401 }
1402
1403 #[test]
1404 fn test_describe_data_model_match_by_name_case_insensitive() {
1405 let data_model_iri = "http://api.test.dasch.swiss/ontology/0801/beol/v2";
1407 let client = MockDspClient::new()
1408 .with_resolve_project(Ok(make_project_ref()))
1409 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1410 .with_describe_data_model(Ok(make_data_model_detail("beol")));
1411
1412 let mut renderer = RecordingRenderer::new();
1413 let args = make_describe_args(Some("0801"), Some("BEOL"));
1414 let result = run_describe_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1415
1416 assert!(
1417 result.is_ok(),
1418 "expected Ok for case-insensitive match, got {:?}",
1419 result
1420 );
1421 assert_eq!(
1422 client.describe_data_model_iri().as_deref(),
1423 Some(data_model_iri)
1424 );
1425 }
1426
1427 #[test]
1428 fn test_describe_data_model_match_by_exact_iri() {
1429 let data_model_iri = "http://api.test.dasch.swiss/ontology/0801/beol/v2";
1430 let client = MockDspClient::new()
1431 .with_resolve_project(Ok(make_project_ref()))
1432 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1433 .with_describe_data_model(Ok(make_data_model_detail("beol")));
1434
1435 let mut renderer = RecordingRenderer::new();
1436 let args = make_describe_args(Some("0801"), Some(data_model_iri));
1438 let result = run_describe_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1439
1440 assert!(
1441 result.is_ok(),
1442 "expected Ok for exact IRI match, got {:?}",
1443 result
1444 );
1445 assert_eq!(
1446 client.describe_data_model_iri().as_deref(),
1447 Some(data_model_iri)
1448 );
1449 }
1450
1451 #[test]
1452 fn test_describe_data_model_not_found() {
1453 let client = MockDspClient::new()
1454 .with_resolve_project(Ok(make_project_ref()))
1455 .with_list_data_models(Ok(vec![make_data_model("beol", None)]));
1456
1457 let mut renderer = RecordingRenderer::new();
1458 let args = make_describe_args(Some("0801"), Some("nonexistent-dm"));
1459 let result = run_describe_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1460
1461 assert!(
1462 matches!(result, Err(Diagnostic::NotFound(_))),
1463 "unknown data-model must yield NotFound, got {:?}",
1464 result
1465 );
1466 assert_eq!(
1467 client.describe_data_model_calls(),
1468 0,
1469 "describe_data_model must not be called when the data-model is not found"
1470 );
1471 }
1472
1473 #[test]
1474 fn test_describe_data_model_missing_project_is_usage_error() {
1475 let client = MockDspClient::new();
1476 let mut renderer = RecordingRenderer::new();
1477 let args = make_describe_args(None, Some("beol")); let result = run_describe_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1479
1480 assert!(
1481 matches!(result, Err(Diagnostic::Usage(_))),
1482 "missing --project must yield Diagnostic::Usage, got {:?}",
1483 result
1484 );
1485 assert_eq!(
1486 client.resolve_calls(),
1487 0,
1488 "resolve_project must not be called when --project is missing"
1489 );
1490 assert_eq!(
1491 client.list_data_models_calls(),
1492 0,
1493 "list_data_models must not be called when --project is missing"
1494 );
1495 assert_eq!(
1496 client.describe_data_model_calls(),
1497 0,
1498 "describe_data_model must not be called when --project is missing"
1499 );
1500 }
1501
1502 #[test]
1503 fn test_describe_data_model_missing_data_model_is_usage_error() {
1504 let client = MockDspClient::new();
1505 let mut renderer = RecordingRenderer::new();
1506 let args = make_describe_args(Some("0801"), None); let result = run_describe_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1508
1509 assert!(
1510 matches!(result, Err(Diagnostic::Usage(_))),
1511 "missing --data-model must yield Diagnostic::Usage, got {:?}",
1512 result
1513 );
1514 assert_eq!(
1515 client.resolve_calls(),
1516 0,
1517 "resolve_project must not be called when --data-model is missing"
1518 );
1519 assert_eq!(
1520 client.list_data_models_calls(),
1521 0,
1522 "list_data_models must not be called when --data-model is missing"
1523 );
1524 assert_eq!(
1525 client.describe_data_model_calls(),
1526 0,
1527 "describe_data_model must not be called when --data-model is missing"
1528 );
1529 }
1530
1531 #[test]
1532 fn test_describe_data_model_resolve_project_not_found_propagates() {
1533 let client = MockDspClient::new()
1534 .with_resolve_project(Err(Diagnostic::NotFound("project '9999' not found".into())));
1535
1536 let mut renderer = RecordingRenderer::new();
1537 let args = make_describe_args(Some("9999"), Some("beol"));
1538 let result = run_describe_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1539
1540 assert!(
1541 matches!(result, Err(Diagnostic::NotFound(_))),
1542 "resolve_project NotFound must propagate, got {:?}",
1543 result
1544 );
1545 assert_eq!(
1546 client.list_data_models_calls(),
1547 0,
1548 "list_data_models must not be called when resolve_project fails"
1549 );
1550 assert_eq!(
1551 client.describe_data_model_calls(),
1552 0,
1553 "describe_data_model must not be called when resolve_project fails"
1554 );
1555 }
1556
1557 #[test]
1558 fn test_describe_data_model_list_error_propagates() {
1559 let client = MockDspClient::new()
1560 .with_resolve_project(Ok(make_project_ref()))
1561 .with_list_data_models(Err(Diagnostic::ServerError("server is broken".into())));
1562
1563 let mut renderer = RecordingRenderer::new();
1564 let args = make_describe_args(Some("0801"), Some("beol"));
1565 let result = run_describe_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1566
1567 assert!(
1568 matches!(result, Err(Diagnostic::ServerError(_))),
1569 "list_data_models error must propagate, got {:?}",
1570 result
1571 );
1572 assert_eq!(
1573 client.describe_data_model_calls(),
1574 0,
1575 "describe_data_model must not be called when list_data_models fails"
1576 );
1577 }
1578
1579 #[test]
1580 fn test_describe_data_model_describe_error_propagates() {
1581 let client = MockDspClient::new()
1582 .with_resolve_project(Ok(make_project_ref()))
1583 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1584 .with_describe_data_model(Err(Diagnostic::ServerError("allentities failed".into())));
1585
1586 let mut renderer = RecordingRenderer::new();
1587 let args = make_describe_args(Some("0801"), Some("beol"));
1588 let result = run_describe_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1589
1590 assert!(
1591 matches!(result, Err(Diagnostic::ServerError(_))),
1592 "describe_data_model error must propagate, got {:?}",
1593 result
1594 );
1595 }
1596
1597 #[test]
1598 fn test_describe_data_model_auth_anonymous() {
1599 let client = MockDspClient::new()
1600 .with_resolve_project(Ok(make_project_ref()))
1601 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1602 .with_describe_data_model(Ok(make_data_model_detail("beol")));
1603
1604 let mut renderer = RecordingRenderer::new();
1605 let args = make_describe_args(Some("0801"), Some("beol"));
1606 run_describe_impl(&args, &make_cfg(), &client, &mut renderer, None, None)
1607 .expect("expected Ok");
1608
1609 let meta = renderer
1610 .data_model_describe_meta
1611 .expect("meta must be present");
1612 assert_eq!(meta.auth_state, "anonymous");
1613 }
1614
1615 #[test]
1616 fn test_describe_data_model_auth_via_env_token() {
1617 let client = MockDspClient::new()
1618 .with_resolve_project(Ok(make_project_ref()))
1619 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1620 .with_describe_data_model(Ok(make_data_model_detail("beol")));
1621
1622 let mut renderer = RecordingRenderer::new();
1623 let args = make_describe_args(Some("0801"), Some("beol"));
1624 run_describe_impl(
1625 &args,
1626 &make_cfg(),
1627 &client,
1628 &mut renderer,
1629 Some("env-jwt-token".to_string()),
1630 None,
1631 )
1632 .expect("expected Ok");
1633
1634 let meta = renderer
1635 .data_model_describe_meta
1636 .expect("meta must be present");
1637 assert_eq!(meta.auth_state, "authenticated via DSP_TOKEN");
1638 }
1639
1640 #[test]
1641 fn test_describe_data_model_auth_via_cache_with_user() {
1642 let dir = TempDir::new().expect("tempdir");
1643 let cache = cache_with_entry(SERVER, "cache-token-xyz", "user@example.com");
1644 let path = write_cache(&dir, &cache);
1645
1646 let client = MockDspClient::new()
1647 .with_resolve_project(Ok(make_project_ref()))
1648 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1649 .with_describe_data_model(Ok(make_data_model_detail("beol")));
1650
1651 let mut renderer = RecordingRenderer::new();
1652 let args = make_describe_args(Some("0801"), Some("beol"));
1653 run_describe_impl(
1654 &args,
1655 &make_cfg(),
1656 &client,
1657 &mut renderer,
1658 None,
1659 Some(&path),
1660 )
1661 .expect("expected Ok");
1662
1663 let meta = renderer
1664 .data_model_describe_meta
1665 .expect("meta must be present");
1666 assert_eq!(meta.auth_state, "authenticated as user@example.com");
1667 }
1668
1669 #[test]
1670 fn test_describe_data_model_corrupt_cache_falls_back_to_anonymous() {
1671 let dir = TempDir::new().expect("tempdir");
1672 let bad_path = dir.path().join("nonexistent_auth.toml");
1673
1674 let client = MockDspClient::new()
1675 .with_resolve_project(Ok(make_project_ref()))
1676 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1677 .with_describe_data_model(Ok(make_data_model_detail("beol")));
1678
1679 let mut renderer = RecordingRenderer::new();
1680 let args = make_describe_args(Some("0801"), Some("beol"));
1681 let result = run_describe_impl(
1682 &args,
1683 &make_cfg(),
1684 &client,
1685 &mut renderer,
1686 None,
1687 Some(&bad_path),
1688 );
1689 assert!(
1690 result.is_ok(),
1691 "corrupt/missing cache must not fail: {:?}",
1692 result
1693 );
1694
1695 let meta = renderer
1696 .data_model_describe_meta
1697 .expect("meta must be present");
1698 assert_eq!(
1699 meta.auth_state, "anonymous",
1700 "corrupt cache + no env token must fall back to anonymous"
1701 );
1702 }
1703
1704 fn make_structure_args(
1707 project: Option<&str>,
1708 data_model: Option<&str>,
1709 include_builtins: bool,
1710 ) -> DataModelStructureArgs {
1711 DataModelStructureArgs {
1712 server: Some(SERVER.to_string()),
1713 project: project.map(str::to_owned),
1714 data_model: data_model.map(str::to_owned),
1715 include_builtins,
1716 format: FormatArgs {
1717 format: Format::Prose,
1718 json: false,
1719 lines: false,
1720 columns: None,
1721 no_header: false,
1722 header_only: false,
1723 },
1724 }
1725 }
1726
1727 fn make_link_relation(
1728 source: &str,
1729 field: &str,
1730 target: &str,
1731 target_data_model: Option<&str>,
1732 is_builtin: bool,
1733 ) -> crate::model::Relation {
1734 crate::model::Relation {
1735 source: source.to_string(),
1736 target: target.to_string(),
1737 kind: crate::model::RelationKind::Link,
1738 field: Some(field.to_string()),
1739 target_data_model: target_data_model.map(str::to_owned),
1740 is_builtin,
1741 }
1742 }
1743
1744 fn make_inherits_relation(
1745 source: &str,
1746 target: &str,
1747 target_data_model: Option<&str>,
1748 is_builtin: bool,
1749 ) -> crate::model::Relation {
1750 crate::model::Relation {
1751 source: source.to_string(),
1752 target: target.to_string(),
1753 kind: crate::model::RelationKind::Inherits,
1754 field: None,
1755 target_data_model: target_data_model.map(str::to_owned),
1756 is_builtin,
1757 }
1758 }
1759
1760 fn make_structure(
1761 data_model: &str,
1762 relations: Vec<crate::model::Relation>,
1763 ) -> DataModelStructure {
1764 DataModelStructure {
1765 data_model: data_model.to_string(),
1766 relations,
1767 }
1768 }
1769
1770 #[test]
1773 fn test_structure_link_only() {
1774 let structure = make_structure(
1775 "beol",
1776 vec![
1777 make_link_relation("letter", "hasSender", "person", None, false),
1778 make_link_relation("letter", "hasRecipient", "person", None, false),
1779 ],
1780 );
1781 let client = MockDspClient::new()
1782 .with_resolve_project(Ok(make_project_ref()))
1783 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1784 .with_data_model_structure(Ok(structure.clone()));
1785
1786 let mut renderer = RecordingRenderer::new();
1787 let args = make_structure_args(Some("0801"), Some("beol"), false);
1788 let result = run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1789
1790 assert!(result.is_ok(), "expected Ok, got {:?}", result);
1791 assert_eq!(client.data_model_structure_calls(), 1);
1792 let recorded = renderer
1793 .data_model_structure_val
1794 .expect("data_model_structure must have been called");
1795 assert_eq!(recorded.relations.len(), 2);
1796 assert!(
1797 recorded
1798 .relations
1799 .iter()
1800 .all(|r| r.kind == crate::model::RelationKind::Link)
1801 );
1802 }
1803
1804 #[test]
1805 fn test_structure_inherits_only() {
1806 let structure = make_structure(
1807 "beol",
1808 vec![make_inherits_relation(
1809 "letter",
1810 "writtenSource",
1811 None,
1812 false,
1813 )],
1814 );
1815 let client = MockDspClient::new()
1816 .with_resolve_project(Ok(make_project_ref()))
1817 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1818 .with_data_model_structure(Ok(structure.clone()));
1819
1820 let mut renderer = RecordingRenderer::new();
1821 let args = make_structure_args(Some("0801"), Some("beol"), false);
1822 run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None)
1823 .expect("expected Ok");
1824
1825 let recorded = renderer
1826 .data_model_structure_val
1827 .expect("data_model_structure must have been called");
1828 assert_eq!(recorded.relations.len(), 1);
1829 assert_eq!(
1830 recorded.relations[0].kind,
1831 crate::model::RelationKind::Inherits
1832 );
1833 }
1834
1835 #[test]
1836 fn test_structure_mixed_link_and_inherits() {
1837 let structure = make_structure(
1838 "beol",
1839 vec![
1840 make_link_relation("letter", "hasSender", "person", None, false),
1841 make_inherits_relation("letter", "writtenSource", None, false),
1842 ],
1843 );
1844 let client = MockDspClient::new()
1845 .with_resolve_project(Ok(make_project_ref()))
1846 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1847 .with_data_model_structure(Ok(structure.clone()));
1848
1849 let mut renderer = RecordingRenderer::new();
1850 let args = make_structure_args(Some("0801"), Some("beol"), false);
1851 run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None)
1852 .expect("expected Ok");
1853
1854 let recorded = renderer
1855 .data_model_structure_val
1856 .expect("data_model_structure must have been called");
1857 assert_eq!(recorded.relations.len(), 2);
1858 let kinds: Vec<_> = recorded.relations.iter().map(|r| r.kind).collect();
1859 assert!(kinds.contains(&crate::model::RelationKind::Link));
1860 assert!(kinds.contains(&crate::model::RelationKind::Inherits));
1861 }
1862
1863 #[test]
1864 fn test_structure_cross_model_target_tag_carried_through() {
1865 let structure = make_structure(
1868 "beol",
1869 vec![make_link_relation(
1870 "letter",
1871 "cites",
1872 "Book",
1873 Some("biblio"),
1874 false,
1875 )],
1876 );
1877 let client = MockDspClient::new()
1878 .with_resolve_project(Ok(make_project_ref()))
1879 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1880 .with_data_model_structure(Ok(structure.clone()));
1881
1882 let mut renderer = RecordingRenderer::new();
1883 let args = make_structure_args(Some("0801"), Some("beol"), false);
1884 run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None)
1885 .expect("expected Ok");
1886
1887 let recorded = renderer
1888 .data_model_structure_val
1889 .expect("data_model_structure must have been called");
1890 assert_eq!(recorded.relations.len(), 1);
1891 assert_eq!(
1892 recorded.relations[0].target_data_model.as_deref(),
1893 Some("biblio"),
1894 "cross-model target_data_model must be preserved through the action"
1895 );
1896 }
1897
1898 #[test]
1899 fn test_structure_asymmetric_builtin_project_link_to_builtin_target_shown_by_default() {
1900 let project_link_to_builtin = make_link_relation(
1903 "letter",
1904 "hasRelation",
1905 "Resource",
1906 None, false, );
1909 let system_inherit = make_inherits_relation(
1911 "letter", "Resource", None, true, );
1914 let structure = make_structure("beol", vec![project_link_to_builtin, system_inherit]);
1915 let client = MockDspClient::new()
1916 .with_resolve_project(Ok(make_project_ref()))
1917 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1918 .with_data_model_structure(Ok(structure));
1919
1920 let mut renderer = RecordingRenderer::new();
1921 let args = make_structure_args(Some("0801"), Some("beol"), false); run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None)
1923 .expect("expected Ok");
1924
1925 let recorded = renderer
1926 .data_model_structure_val
1927 .expect("data_model_structure must have been called");
1928 assert_eq!(
1930 recorded.relations.len(),
1931 1,
1932 "only the project link (is_builtin=false) should survive default filtering"
1933 );
1934 assert_eq!(recorded.relations[0].kind, crate::model::RelationKind::Link);
1935 assert_eq!(recorded.relations[0].field.as_deref(), Some("hasRelation"));
1936 }
1937
1938 #[test]
1939 fn test_structure_include_builtins_shows_all() {
1940 let project_link_to_builtin =
1942 make_link_relation("letter", "hasRelation", "Resource", None, false);
1943 let system_inherit = make_inherits_relation("letter", "Resource", None, true);
1944 let structure = make_structure("beol", vec![project_link_to_builtin, system_inherit]);
1945 let client = MockDspClient::new()
1946 .with_resolve_project(Ok(make_project_ref()))
1947 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1948 .with_data_model_structure(Ok(structure));
1949
1950 let mut renderer = RecordingRenderer::new();
1951 let args = make_structure_args(Some("0801"), Some("beol"), true); run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None)
1953 .expect("expected Ok");
1954
1955 let recorded = renderer
1956 .data_model_structure_val
1957 .expect("data_model_structure must have been called");
1958 assert_eq!(
1959 recorded.relations.len(),
1960 2,
1961 "--include-builtins must show all relations including is_builtin=true ones"
1962 );
1963 }
1964
1965 #[test]
1966 fn test_structure_zero_relations() {
1967 let structure = make_structure("beol", vec![]);
1968 let client = MockDspClient::new()
1969 .with_resolve_project(Ok(make_project_ref()))
1970 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
1971 .with_data_model_structure(Ok(structure));
1972
1973 let mut renderer = RecordingRenderer::new();
1974 let args = make_structure_args(Some("0801"), Some("beol"), false);
1975 let result = run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1976
1977 assert!(
1978 result.is_ok(),
1979 "zero relations must render OK, got {:?}",
1980 result
1981 );
1982 let recorded = renderer
1983 .data_model_structure_val
1984 .expect("data_model_structure must have been called");
1985 assert!(
1986 recorded.relations.is_empty(),
1987 "zero relations must be empty after filtering"
1988 );
1989 }
1990
1991 #[test]
1992 fn test_structure_missing_project_is_usage_error() {
1993 let client = MockDspClient::new();
1994 let mut renderer = RecordingRenderer::new();
1995 let args = make_structure_args(None, Some("beol"), false);
1996 let result = run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
1997
1998 assert!(
1999 matches!(result, Err(Diagnostic::Usage(_))),
2000 "missing --project must yield Diagnostic::Usage, got {:?}",
2001 result
2002 );
2003 assert_eq!(
2004 client.resolve_calls(),
2005 0,
2006 "resolve_project must not be called when --project is missing"
2007 );
2008 assert_eq!(
2009 client.data_model_structure_calls(),
2010 0,
2011 "data_model_structure must not be called when --project is missing"
2012 );
2013 }
2014
2015 #[test]
2016 fn test_structure_missing_data_model_is_usage_error() {
2017 let client = MockDspClient::new();
2018 let mut renderer = RecordingRenderer::new();
2019 let args = make_structure_args(Some("0801"), None, false);
2020 let result = run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
2021
2022 assert!(
2023 matches!(result, Err(Diagnostic::Usage(_))),
2024 "missing --data-model must yield Diagnostic::Usage, got {:?}",
2025 result
2026 );
2027 assert_eq!(
2028 client.resolve_calls(),
2029 0,
2030 "resolve_project must not be called when --data-model is missing"
2031 );
2032 assert_eq!(
2033 client.data_model_structure_calls(),
2034 0,
2035 "data_model_structure must not be called when --data-model is missing"
2036 );
2037 }
2038
2039 #[test]
2040 fn test_structure_project_not_found_propagates() {
2041 let client = MockDspClient::new()
2042 .with_resolve_project(Err(Diagnostic::NotFound("project '9999' not found".into())));
2043
2044 let mut renderer = RecordingRenderer::new();
2045 let args = make_structure_args(Some("9999"), Some("beol"), false);
2046 let result = run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
2047
2048 assert!(
2049 matches!(result, Err(Diagnostic::NotFound(_))),
2050 "resolve_project NotFound must propagate, got {:?}",
2051 result
2052 );
2053 assert_eq!(
2054 client.list_data_models_calls(),
2055 0,
2056 "list_data_models must not be called when resolve_project fails"
2057 );
2058 assert_eq!(
2059 client.data_model_structure_calls(),
2060 0,
2061 "data_model_structure must not be called when resolve_project fails"
2062 );
2063 }
2064
2065 #[test]
2066 fn test_structure_data_model_not_found_reframed_to_not_found_diagnostic() {
2067 let client = MockDspClient::new()
2070 .with_resolve_project(Ok(make_project_ref()))
2071 .with_list_data_models(Ok(vec![make_data_model("beol", None)]));
2072
2073 let mut renderer = RecordingRenderer::new();
2074 let args = make_structure_args(Some("0801"), Some("nonexistent-dm"), false);
2075 let result = run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None);
2076
2077 assert!(
2078 matches!(result, Err(Diagnostic::NotFound(_))),
2079 "unknown data-model must yield NotFound Diagnostic, got {:?}",
2080 result
2081 );
2082 assert_eq!(
2083 client.data_model_structure_calls(),
2084 0,
2085 "data_model_structure must not be called when the data-model is not found"
2086 );
2087 }
2088
2089 #[test]
2090 fn test_structure_data_model_iri_forwarded() {
2091 let dm_iri = "http://api.test.dasch.swiss/ontology/0801/beol/v2";
2093 let structure = make_structure("beol", vec![]);
2094 let client = MockDspClient::new()
2095 .with_resolve_project(Ok(make_project_ref()))
2096 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
2097 .with_data_model_structure(Ok(structure));
2098
2099 let mut renderer = RecordingRenderer::new();
2100 let args = make_structure_args(Some("0801"), Some("beol"), false);
2101 run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None)
2102 .expect("expected Ok");
2103
2104 assert_eq!(
2105 client.data_model_structure_iri().as_deref(),
2106 Some(dm_iri),
2107 "data_model_structure must be called with the resolved IRI"
2108 );
2109 }
2110
2111 #[test]
2112 fn test_structure_token_forwarded_to_client() {
2113 let structure = make_structure("beol", vec![]);
2115 let client = MockDspClient::new()
2116 .with_resolve_project(Ok(make_project_ref()))
2117 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
2118 .with_data_model_structure(Ok(structure));
2119
2120 let mut renderer = RecordingRenderer::new();
2121 let args = make_structure_args(Some("0801"), Some("beol"), false);
2122 run_structure_impl(
2123 &args,
2124 &make_cfg(),
2125 &client,
2126 &mut renderer,
2127 Some("env-jwt-token".to_string()),
2128 None,
2129 )
2130 .expect("expected Ok");
2131
2132 assert_eq!(
2133 client.data_model_structure_token(),
2134 Some(Some("env-jwt-token".to_string())),
2135 "env token must be forwarded to data_model_structure"
2136 );
2137 let meta = renderer
2138 .data_model_structure_meta
2139 .expect("meta must be present");
2140 assert_eq!(meta.auth_state, "authenticated via DSP_TOKEN");
2141 }
2142
2143 #[test]
2144 fn test_structure_auth_anonymous_when_no_token() {
2145 let structure = make_structure("beol", vec![]);
2146 let client = MockDspClient::new()
2147 .with_resolve_project(Ok(make_project_ref()))
2148 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
2149 .with_data_model_structure(Ok(structure));
2150
2151 let mut renderer = RecordingRenderer::new();
2152 let args = make_structure_args(Some("0801"), Some("beol"), false);
2153 run_structure_impl(&args, &make_cfg(), &client, &mut renderer, None, None)
2154 .expect("expected Ok");
2155
2156 let meta = renderer
2157 .data_model_structure_meta
2158 .expect("meta must be present");
2159 assert_eq!(meta.auth_state, "anonymous");
2160 assert_eq!(client.data_model_structure_token(), Some(None));
2161 }
2162
2163 #[test]
2164 fn test_structure_auth_via_cache_with_user() {
2165 let dir = TempDir::new().expect("tempdir");
2170 let cache = cache_with_entry(SERVER, "cache-token-xyz", "user@example.com");
2171 let path = write_cache(&dir, &cache);
2172
2173 let structure = make_structure("beol", vec![]);
2174 let client = MockDspClient::new()
2175 .with_resolve_project(Ok(make_project_ref()))
2176 .with_list_data_models(Ok(vec![make_data_model("beol", None)]))
2177 .with_data_model_structure(Ok(structure));
2178
2179 let mut renderer = RecordingRenderer::new();
2180 let args = make_structure_args(Some("0801"), Some("beol"), false);
2181 run_structure_impl(
2182 &args,
2183 &make_cfg(),
2184 &client,
2185 &mut renderer,
2186 None,
2187 Some(&path),
2188 )
2189 .expect("expected Ok");
2190
2191 let meta = renderer
2192 .data_model_structure_meta
2193 .expect("meta must be present");
2194 assert_eq!(
2195 meta.auth_state, "authenticated as user@example.com",
2196 "cached-token path must produce 'authenticated as <user>' auth state"
2197 );
2198 assert_eq!(
2199 client.data_model_structure_token(),
2200 Some(Some("cache-token-xyz".to_string())),
2201 "cached token must be forwarded to data_model_structure"
2202 );
2203 }
2204}