tests/testthat/test-configure-global.R

# Skip all global config tests on CRAN - they write to user config directory
# and require proper isolation via FW_CONFIG_HOME

test_that("configure_global reads current config when no settings provided", {
  skip_on_cran()

  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Call without settings - should return defaults
  result <- configure_global()

  expect_true(is.list(result))
  expect_true(!is.null(result$author))
  expect_true(!is.null(result$defaults))
})


test_that("configure_global updates author information", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Update author
  result <- configure_global(settings = list(
    author = list(
      name = "Test User",
      email = "test@example.com",
      affiliation = "Test Org"
    )
  ))

  expect_equal(result$author$name, "Test User")
  expect_equal(result$author$email, "test@example.com")
  expect_equal(result$author$affiliation, "Test Org")

  # Verify file was written
  settings_path <- file.path(fw_config_dir(), "settings.yml")
  expect_true(file.exists(settings_path))

  # Read back and verify
  saved <- read_frameworkrc()
  expect_equal(saved$author$name, "Test User")
})


test_that("configure_global updates defaults", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Update defaults
  result <- configure_global(settings = list(
    defaults = list(
      project_type = "presentation",
      notebook_format = "rmarkdown",
      ide = "rstudio",
      use_git = FALSE,
      seed = 12345
    )
  ))

  expect_equal(result$defaults$project_type, "presentation")
  expect_equal(result$defaults$notebook_format, "rmarkdown")
  expect_equal(result$defaults$ide, "rstudio")
  expect_false(result$defaults$use_git)
  expect_equal(result$defaults$seed, 12345)
})


test_that("configure_global validates project_type choices", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Invalid project type should error
  expect_error(
    configure_global(settings = list(
      defaults = list(project_type = "invalid")
    )),
    "project_type"
  )
})


test_that("configure_global validates notebook_format choices", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Invalid format should error
  expect_error(
    configure_global(settings = list(
      defaults = list(notebook_format = "invalid")
    )),
    "notebook_format"
  )
})


test_that("configure_global validates IDE choices", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Invalid IDE should error
  expect_error(
    configure_global(settings = list(
      defaults = list(ide = "invalid")
    )),
    "ide"
  )
})


test_that("configure_global validates boolean flags", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Invalid boolean should error
  expect_error(
    configure_global(settings = list(
      defaults = list(use_git = "yes")  # should be logical
    )),
    "flag"
  )
})


test_that("configure_global accepts numeric or character seed", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Numeric seed
  result1 <- configure_global(settings = list(
    defaults = list(seed = 12345)
  ))
  expect_equal(result1$defaults$seed, 12345)

  # Character seed
  result2 <- configure_global(settings = list(
    defaults = list(seed = "20250102")
  ))
  expect_equal(result2$defaults$seed, "20250102")
})


test_that("configure_global performs deep merge", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Set author first
  configure_global(settings = list(
    author = list(
      name = "First User",
      email = "first@example.com"
    )
  ))

  # Update just email - name should remain
  result <- configure_global(settings = list(
    author = list(
      email = "updated@example.com"
    )
  ))

  expect_equal(result$author$name, "First User")  # unchanged
  expect_equal(result$author$email, "updated@example.com")  # updated
})


test_that("configure_global can skip validation", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Should not error even with invalid data when validate = FALSE
  expect_no_error(
    suppressMessages(
      configure_global(
        settings = list(
          defaults = list(project_type = "invalid")
        ),
        validate = FALSE
      )
    )
  )
})


test_that("configure_global writes YAML format", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  configure_global(settings = list(
    author = list(name = "Test User")
  ))

  settings_path <- file.path(fw_config_dir(), "settings.yml")
  expect_true(file.exists(settings_path))

  # Should be valid YAML
  yaml_content <- yaml::read_yaml(settings_path)
  expect_true(is.list(yaml_content))
  expect_equal(yaml_content$author$name, "Test User")
})


# Tests for extra_directories validation

test_that("configure_global accepts valid extra_directories", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Valid extra_directories
  result <- configure_global(settings = list(
    project_types = list(
      project = list(
        extra_directories = list(
          list(key = "inputs_archive", label = "Archive", path = "inputs/archive", type = "input"),
          list(key = "outputs_animations", label = "Animations", path = "outputs/animations", type = "output")
        )
      )
    )
  ))

  expect_equal(length(result$project_types$project$extra_directories), 2)
  expect_equal(result$project_types$project$extra_directories[[1]]$key, "inputs_archive")
  expect_equal(result$project_types$project$extra_directories[[2]]$type, "output")
})


test_that("configure_global rejects extra_directories with missing fields", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Missing 'key' field
  expect_error(
    configure_global(settings = list(
      project_types = list(
        project = list(
          extra_directories = list(
            list(label = "Archive", path = "inputs/archive", type = "input")
          )
        )
      )
    )),
    "missing required field 'key'"
  )

  # Missing 'type' field
  expect_error(
    configure_global(settings = list(
      project_types = list(
        project = list(
          extra_directories = list(
            list(key = "test", label = "Test", path = "test")
          )
        )
      )
    )),
    "missing required field 'type'"
  )
})


test_that("configure_global rejects invalid extra_directories key format", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Key with hyphen (invalid)
  expect_error(
    configure_global(settings = list(
      project_types = list(
        project = list(
          extra_directories = list(
            list(key = "my-dir", label = "My Dir", path = "my-dir", type = "input")
          )
        )
      )
    )),
    "must contain only letters, numbers, and underscores"
  )

  # Key with space (invalid)
  expect_error(
    configure_global(settings = list(
      project_types = list(
        project = list(
          extra_directories = list(
            list(key = "my dir", label = "My Dir", path = "mydir", type = "input")
          )
        )
      )
    )),
    "must contain only letters, numbers, and underscores"
  )
})


test_that("configure_global rejects duplicate extra_directories keys", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Duplicate keys
  expect_error(
    configure_global(settings = list(
      project_types = list(
        project = list(
          extra_directories = list(
            list(key = "test_dir", label = "Test 1", path = "test1", type = "input"),
            list(key = "test_dir", label = "Test 2", path = "test2", type = "input")
          )
        )
      )
    )),
    "duplicate extra_directories key 'test_dir'"
  )
})


test_that("configure_global rejects invalid extra_directories type", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Invalid type
  expect_error(
    configure_global(settings = list(
      project_types = list(
        project = list(
          extra_directories = list(
            list(key = "test", label = "Test", path = "test", type = "invalid")
          )
        )
      )
    )),
    "type 'invalid' must be one of: input, workspace, output"
  )
})


test_that("configure_global rejects absolute paths in extra_directories", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Absolute path
  expect_error(
    configure_global(settings = list(
      project_types = list(
        project = list(
          extra_directories = list(
            list(key = "test", label = "Test", path = "/absolute/path", type = "input")
          )
        )
      )
    )),
    "must be relative \\(no leading slash\\)"
  )
})


test_that("configure_global rejects path traversal in extra_directories", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Path traversal
  expect_error(
    configure_global(settings = list(
      project_types = list(
        project = list(
          extra_directories = list(
            list(key = "test", label = "Test", path = "../parent", type = "input")
          )
        )
      )
    )),
    "cannot contain '\\.\\.' \\(path traversal\\)"
  )
})


test_that("configure_global preserves extra_directories through modifyList", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Set initial extra_directories
  configure_global(settings = list(
    project_types = list(
      project = list(
        extra_directories = list(
          list(key = "test1", label = "Test 1", path = "test1", type = "input")
        )
      )
    )
  ))

  # Update author (different field) - extra_directories should persist
  result <- configure_global(settings = list(
    author = list(name = "Updated User")
  ))

  # extra_directories should still exist
  expect_equal(length(result$project_types$project$extra_directories), 1)
  expect_equal(result$project_types$project$extra_directories[[1]]$key, "test1")

  # Now update extra_directories
  result2 <- configure_global(settings = list(
    project_types = list(
      project = list(
        extra_directories = list(
          list(key = "test2", label = "Test 2", path = "test2", type = "workspace")
        )
      )
    )
  ))

  # Should be replaced
  expect_equal(length(result2$project_types$project$extra_directories), 1)
  expect_equal(result2$project_types$project$extra_directories[[1]]$key, "test2")
  expect_equal(result2$project_types$project$extra_directories[[1]]$type, "workspace")
})


test_that("configure_global saves global.projects_root", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Set global.projects_root
  test_path <- "/Users/test/my-projects"
  result <- configure_global(settings = list(
    global = list(
      projects_root = test_path
    )
  ))

  # Verify return value has the path
expect_equal(result$global$projects_root, test_path)

  # Verify file was written
  settings_path <- file.path(fw_config_dir(), "settings.yml")
  expect_true(file.exists(settings_path))

  # Read the raw YAML to verify it was actually saved
  saved_yaml <- yaml::read_yaml(settings_path)
  expect_equal(saved_yaml$global$projects_root, test_path)

  # Also verify through read_frameworkrc
  saved <- read_frameworkrc()
  expect_equal(saved$global$projects_root, test_path)
})


test_that("configure_global persists global.projects_root through multiple saves", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # First save: set projects_root
  configure_global(settings = list(
    global = list(
      projects_root = "/first/path"
    )
  ))

  # Second save: change projects_root
  result <- configure_global(settings = list(
    global = list(
      projects_root = "/second/path"
    )
  ))

  expect_equal(result$global$projects_root, "/second/path")

  # Verify in file
  saved <- read_frameworkrc()
  expect_equal(saved$global$projects_root, "/second/path")
})


test_that("configure_global preserves global.projects_root when saving other settings", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # First: set projects_root
  configure_global(settings = list(
    global = list(
      projects_root = "/my/projects"
    )
  ))

  # Second: save author (without sending global)
  configure_global(settings = list(
    author = list(
      name = "New Author"
    )
  ))

  # projects_root should still be there
  saved <- read_frameworkrc()
  expect_equal(saved$global$projects_root, "/my/projects")
  expect_equal(saved$author$name, "New Author")
})


# ============================================================================
# Cache Directory Configuration Tests
# ============================================================================

test_that("configure_global can set cache directory per project type", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Set cache directory for standard project type
  result <- configure_global(settings = list(
    project_types = list(
      project = list(
        directories = list(
          cache = "custom/cache/path"
        )
      )
    )
  ))

  expect_equal(result$project_types$project$directories$cache, "custom/cache/path")
})


test_that("configure_global supports different cache paths per project type", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Set different cache directories for different project types
  result <- configure_global(settings = list(
    project_types = list(
      project = list(
        directories = list(
          cache = "outputs/cache"
        )
      ),
      project_sensitive = list(
        directories = list(
          cache = "outputs/private/cache"
        )
      ),
      presentation = list(
        directories = list(
          cache = "cache"
        )
      ),
      course = list(
        directories = list(
          cache = "cache"
        )
      )
    )
  ))

  expect_equal(result$project_types$project$directories$cache, "outputs/cache")
  expect_equal(result$project_types$project_sensitive$directories$cache, "outputs/private/cache")
  expect_equal(result$project_types$presentation$directories$cache, "cache")
  expect_equal(result$project_types$course$directories$cache, "cache")
})


test_that("configure_global preserves cache directory when updating other settings", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # First: set cache directory
  configure_global(settings = list(
    project_types = list(
      project = list(
        directories = list(
          cache = "my-custom-cache"
        )
      )
    )
  ))

  # Second: update author (without sending project_types)
  configure_global(settings = list(
    author = list(
      name = "Updated Author"
    )
  ))

  # cache directory should still be there
  saved <- read_frameworkrc()
  expect_equal(saved$project_types$project$directories$cache, "my-custom-cache")
  expect_equal(saved$author$name, "Updated Author")
})


test_that("configure_global saves and loads directories_enabled", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # Save directories_enabled for project type
  result <- configure_global(settings = list(
    project_types = list(
      project = list(
        directories_enabled = list(
          scratch = TRUE,
          outputs_tables = FALSE,
          outputs_figures = TRUE
        )
      )
    )
  ))

  # Verify it was saved
  expect_equal(result$project_types$project$directories_enabled$scratch, TRUE)
  expect_equal(result$project_types$project$directories_enabled$outputs_tables, FALSE)
  expect_equal(result$project_types$project$directories_enabled$outputs_figures, TRUE)

  # Verify it persists in file
  saved <- read_frameworkrc()
  expect_equal(saved$project_types$project$directories_enabled$scratch, TRUE)
  expect_equal(saved$project_types$project$directories_enabled$outputs_tables, FALSE)
  expect_equal(saved$project_types$project$directories_enabled$outputs_figures, TRUE)
})


test_that("configure_global preserves directories_enabled when updating other fields", {
  skip_on_cran()
  
  cleanup <- setup_isolated_config()
  on.exit(cleanup())

  # First: save directories_enabled
  configure_global(settings = list(
    project_types = list(
      project = list(
        directories_enabled = list(
          scratch = TRUE,
          outputs_tables = FALSE
        )
      )
    )
  ))

  # Second: update author (without sending directories_enabled)
  configure_global(settings = list(
    author = list(name = "New Author")
  ))

  # directories_enabled should still be there
  saved <- read_frameworkrc()
  expect_equal(saved$project_types$project$directories_enabled$scratch, TRUE)
  expect_equal(saved$project_types$project$directories_enabled$outputs_tables, FALSE)
  expect_equal(saved$author$name, "New Author")
})

Try the framework package in your browser

Any scripts or data that you put into this service are public.

framework documentation built on Feb. 18, 2026, 1:07 a.m.