test_that("tabular conversion handles empty and incomplete records", {
empty <- articles_to_data_frame(list())
expect_s3_class(empty, "data.frame")
expect_identical(names(empty),
c("title", "abstract", "publication_date", "authors", "doi"))
expect_equal(nrow(empty), 0L)
expect_true(all(vapply(empty, is.character, logical(1L))))
articles <- list(
Article(title = "Paper", authors = c("Alice", "Bob"), doi = "10.1000/x"),
Article(abstract = "Content")
)
result <- articles_to_data_frame(articles)
expect_equal(result$authors[[1L]], "Alice; Bob")
expect_true(is.na(result$authors[[2L]]))
expect_true(is.na(result$title[[2L]]))
expect_equal(result$abstract[[2L]], "Content")
expect_equal(articles_to_data_frame(articles, author_separator = " | ")$authors[[1]],
"Alice | Bob")
})
test_that("BibTeX citation keys are stable and unique", {
x <- list(
Article(title = "A & B", doi = "10.1000/a", authors = c("Ada", "Bob")),
Article(title = "Second", doi = "10.1000/a"),
Article(title = "Third")
)
result <- articles_to_bibtex(x)
expect_match(result, "@article\\{doi_10_1000_a,", perl = TRUE)
expect_match(result, "@article\\{doi_10_1000_a_2,", perl = TRUE)
expect_match(result, "@article\\{ref_3,", perl = TRUE)
expect_true(grepl("A \\& B", result, fixed = TRUE))
expect_match(result, "Ada and Bob", fixed = TRUE)
expect_error(articles_to_bibtex(x, keys = c("x", "x", "y")), "unique")
expect_error(articles_to_bibtex(x, keys = "one"), "keys")
expect_equal(articles_to_bibtex(list()), "")
})
test_that("RIS exports repeat authors, preserve unicode, and use CRLF", {
x <- list(Article(title = "Métodos", authors = c("Álvaro", "Betty"),
publication_date = "2026-10-10", doi = "10.1000/p"))
result <- articles_to_ris(x)
expect_true(grepl("TY - JOUR\r\n", result, fixed = TRUE))
expect_true(grepl("AU - Álvaro\r\nAU - Betty\r\n", result, fixed = TRUE))
expect_true(grepl("DA - 2026/10/10", result, fixed = TRUE))
expect_true(endsWith(result, "ER - \r\n"))
expect_equal(articles_to_ris(list()), "")
})
test_that("untrusted text cannot inject RIS lines", {
x <- list(Article(title = "Title\nER - \nTY - ELEC",
abstract = "Some\nOther"))
result <- articles_to_ris(x)
expect_true(grepl("TI - Title ER - TY - ELEC", result, fixed = TRUE))
expect_equal(sum(startsWith(strsplit(result, "\r\n", fixed = TRUE)[[1]], "TY - ")), 1L)
expect_true(grepl("AB - Some Other", result, fixed = TRUE))
})
test_that("CSV defaults to formula-safe output", {
target <- tempfile(fileext = ".csv")
on.exit(unlink(target), add = TRUE)
x <- list(Article(title = "=HYPERLINK(1)", authors = "+danger",
doi = "10.1000/csv"))
expect_identical(export_articles(x, target, format = "csv"), invisible(target))
result <- utils::read.csv(target, check.names = FALSE,
stringsAsFactors = FALSE)
expect_identical(result$title[[1]], "'=HYPERLINK(1)")
expect_identical(result$authors[[1]], "'+danger")
export_articles(x, target, format = "csv", sanitize_csv = FALSE)
raw <- utils::read.csv(target, check.names = FALSE,
stringsAsFactors = FALSE)
expect_equal(raw$title[[1]], "=HYPERLINK(1)")
})
test_that("BibTeX and RIS write paths contain exact renderings", {
for (format in c("bibtex", "ris")) {
path <- tempfile()
x <- list(Article(title = "Case", doi = "10.1000/case"))
expected <- if (format == "bibtex") articles_to_bibtex(x) else articles_to_ris(x)
export_articles(x, path, format = format)
con <- file(path, "rb")
bytes <- readBin(con, "raw", n = file.info(path)$size)
close(con)
expect_identical(rawToChar(bytes), enc2utf8(expected))
unlink(path)
}
})
test_that("export input validation is explicit", {
expect_error(articles_to_data_frame(list(list())), "Article")
expect_error(articles_to_data_frame(list(), author_separator = NA_character_),
"author_separator")
expect_error(export_articles(list(), ""), "file")
expect_error(export_articles(list(), tempfile(), sanitize_csv = NA),
"sanitize_csv")
expect_error(export_articles(list(), tempfile(), format = "json"), "arg")
})