#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
use std::collections::BTreeMap;
use assert_cmd::Command;
use camino::Utf8PathBuf;
use chrono::TimeZone;
use predicates::prelude::*;
use serde_json::Value;
use tempfile::TempDir;
use doiget_core::store::{DoigetExtension, FsStore, Metadata, Store};
use doiget_core::{Doi, Safekey, SCHEMA_VERSION};
fn utf8_path(dir: &TempDir) -> Utf8PathBuf {
Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("temp dir path must be UTF-8")
}
fn doiget(root: &Utf8PathBuf) -> Command {
let mut cmd = Command::cargo_bin("doiget").expect("locate doiget binary");
cmd.env("DOIGET_STORE_ROOT", root.as_str())
.env("HOME", root.as_str())
.env("USERPROFILE", root.as_str());
cmd
}
fn journal_article_fixture() -> (Safekey, Metadata) {
let doi = "10.1234/example";
let ref_ = doiget_core::Ref::Doi(Doi::parse(doi).expect("valid DOI"));
let safekey = ref_.safekey();
let m = Metadata {
schema_version: SCHEMA_VERSION.to_string(),
title: "Quantum Stuff".to_string(),
authors: vec!["Alice Researcher".to_string(), "Bob Coauthor".to_string()],
year: Some(2026),
doi: Some(Doi::parse(doi).expect("valid DOI")),
arxiv_id: None,
abstract_: None,
venue: Some("Phys Rev X".to_string()),
publisher: Some("APS".to_string()),
issn: Some("2160-3308".to_string()),
isbn: None,
type_: Some("journal-article".to_string()),
keywords: vec![],
url: None,
pdf_path: None,
doiget: Some(DoigetExtension {
fetched_at: chrono::Utc
.with_ymd_and_hms(2026, 5, 6, 12, 0, 0)
.single()
.expect("valid timestamp"),
source: "unpaywall".to_string(),
license: "CC-BY-4.0".to_string(),
size_bytes: 1234,
mcp_call_id: None,
}),
other: BTreeMap::new(),
};
(safekey, m)
}
fn comma_form_fixture() -> (Safekey, Metadata) {
let doi = "10.5678/comma";
let ref_ = doiget_core::Ref::Doi(Doi::parse(doi).expect("valid DOI"));
let safekey = ref_.safekey();
let m = Metadata {
schema_version: SCHEMA_VERSION.to_string(),
title: "Comma Title".to_string(),
authors: vec!["Smith, John".to_string()],
year: Some(2025),
doi: Some(Doi::parse(doi).expect("valid DOI")),
arxiv_id: None,
abstract_: None,
venue: None,
publisher: None,
issn: None,
isbn: None,
type_: None,
keywords: vec![],
url: None,
pdf_path: None,
doiget: Some(DoigetExtension {
fetched_at: chrono::Utc
.with_ymd_and_hms(2025, 1, 1, 0, 0, 0)
.single()
.expect("valid timestamp"),
source: "unpaywall".to_string(),
license: "unknown".to_string(),
size_bytes: 0,
mcp_call_id: None,
}),
other: BTreeMap::new(),
};
(safekey, m)
}
fn seeded_store_journal() -> (TempDir, Utf8PathBuf) {
let dir = TempDir::new().expect("tempdir");
let root = utf8_path(&dir).join("papers");
let store = FsStore::new(root.clone()).expect("FsStore::new");
let (k, m) = journal_article_fixture();
store.write(&k, &m, None).expect("seed entry");
(dir, root)
}
fn seeded_store_comma() -> (TempDir, Utf8PathBuf) {
let dir = TempDir::new().expect("tempdir");
let root = utf8_path(&dir).join("papers");
let store = FsStore::new(root.clone()).expect("FsStore::new");
let (k, m) = comma_form_fixture();
store.write(&k, &m, None).expect("seed entry");
(dir, root)
}
#[test]
fn csl_emits_array_with_expected_fields_for_journal_article() {
let (_dir_guard, root) = seeded_store_journal();
let assert = doiget(&root)
.args(["csl", "doi:10.1234/example"])
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone())
.expect("doiget csl stdout was not UTF-8");
let value: Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("doiget csl stdout was not valid JSON: {e}\nstdout:\n{stdout}"));
let array = value
.as_array()
.unwrap_or_else(|| panic!("expected top-level JSON array, got: {value}"));
assert_eq!(array.len(), 1, "expected single-entry array, got: {value}");
let item = &array[0];
assert_eq!(item["id"], Value::String("doi_10.1234_example".into()));
assert_eq!(item["type"], Value::String("article-journal".into()));
assert_eq!(item["title"], Value::String("Quantum Stuff".into()));
assert_eq!(item["DOI"], Value::String("10.1234/example".into()));
assert_eq!(item["container-title"], Value::String("Phys Rev X".into()));
assert_eq!(item["publisher"], Value::String("APS".into()));
assert_eq!(item["ISSN"], Value::String("2160-3308".into()));
let date_parts = &item["issued"]["date-parts"];
assert_eq!(
date_parts,
&serde_json::json!([[2026]]),
"issued.date-parts shape mismatch: {date_parts}"
);
let authors = item["author"]
.as_array()
.unwrap_or_else(|| panic!("expected author array, got: {}", item["author"]));
assert_eq!(authors.len(), 2, "expected 2 authors, got: {authors:?}");
assert_eq!(
authors[0],
serde_json::json!({"family": "Researcher", "given": "Alice"}),
"author[0] mismatch"
);
assert_eq!(
authors[1],
serde_json::json!({"family": "Coauthor", "given": "Bob"}),
"author[1] mismatch"
);
}
#[test]
fn csl_parses_comma_form_author_and_falls_back_to_manuscript_type() {
let (_dir_guard, root) = seeded_store_comma();
let assert = doiget(&root)
.args(["csl", "doi:10.5678/comma"])
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone())
.expect("doiget csl stdout was not UTF-8");
let value: Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("doiget csl stdout was not valid JSON: {e}\nstdout:\n{stdout}"));
let item = &value
.as_array()
.unwrap_or_else(|| panic!("expected top-level JSON array, got: {value}"))[0];
assert_eq!(item["type"], Value::String("manuscript".into()));
assert!(
item.get("container-title").is_none(),
"container-title should be omitted when venue is None; got: {item}"
);
assert!(
item.get("publisher").is_none(),
"publisher should be omitted when publisher is None; got: {item}"
);
assert!(
item.get("ISSN").is_none(),
"ISSN should be omitted when issn is None; got: {item}"
);
let authors = item["author"]
.as_array()
.unwrap_or_else(|| panic!("expected author array, got: {}", item["author"]));
assert_eq!(authors.len(), 1);
assert_eq!(
authors[0],
serde_json::json!({"family": "Smith", "given": "John"}),
"author[0] mismatch for comma-form name"
);
}
#[test]
fn csl_fails_for_missing_entry() {
let dir = TempDir::new().expect("tempdir");
let root = utf8_path(&dir).join("papers");
FsStore::new(root.clone()).expect("FsStore::new");
doiget(&root)
.args(["csl", "doi:10.9999/missing"])
.assert()
.failure()
.stderr(predicate::str::contains("no entry for"));
}
#[test]
fn csl_fails_for_invalid_ref_string() {
let dir = TempDir::new().expect("tempdir");
let root = utf8_path(&dir).join("papers");
FsStore::new(root.clone()).expect("FsStore::new");
doiget(&root)
.args(["csl", "not-a-ref"])
.assert()
.failure()
.stderr(predicate::str::contains("invalid ref"));
}