tests/testthat/test-env-file.R

test_that(".env file parsing preserves structure", {
  # Create temp path for test
  project_dir <- tempfile("framework_test_")
  on.exit(unlink(project_dir, recursive = TRUE), add = TRUE)

  suppressMessages({
    result <- framework::project_create(
      name = "EnvTest",
      location = project_dir,
      type = "project",
      git = list(use_git = FALSE)
    )
  })

  project_path <- result$path

  # Create .env file with specific structure and comments
  original_content <- c(
    "# Important database configuration",
    "DB_HOST=localhost",
    "# Port number",
    "DB_PORT=5432",
    "",
    "# API credentials",
    "API_KEY=secret123",
    "# End of file"
  )
  writeLines(original_content, file.path(project_path, ".env"))

  # Read the file back
  read_content <- readLines(file.path(project_path, ".env"))

  # Verify structure preserved
  expect_equal(read_content, original_content)
  expect_true(any(grepl("# Important database", read_content)))
  expect_true(any(grepl("# Port number", read_content)))
  expect_true(any(grepl("# End of file", read_content)))
})

test_that(".env file with dotenv_location in parent directory", {
  # Create parent and project directories
  parent_dir <- tempfile("framework_test_parent_")
  dir.create(parent_dir, recursive = TRUE)
  project_dir <- file.path(parent_dir, "myproject")
  on.exit(unlink(parent_dir, recursive = TRUE), add = TRUE)

  suppressMessages({
    result <- framework::project_create(
      name = "EnvTest",
      location = project_dir,
      type = "project",
      git = list(use_git = FALSE)
    )
  })

  # Remove .env that project_create() may have created
  if (file.exists(file.path(project_dir, ".env"))) {
    file.remove(file.path(project_dir, ".env"))
  }

  # Create settings.yml with dotenv_location pointing to parent
  settings_content <- c(
    "default:",
    "  project_type: project",
    "  project_name: EnvTest",
    "  dotenv_location: \"../\""
  )
  writeLines(settings_content, file.path(project_dir, "settings.yml"))

  # Create .env in parent directory
  env_content <- c("PARENT_VAR=fromparent")
  writeLines(env_content, file.path(parent_dir, ".env"))

  # Verify .env exists in parent but not in project
  expect_true(file.exists(file.path(parent_dir, ".env")))
  expect_false(file.exists(file.path(project_dir, ".env")))

  # Read settings to verify dotenv_location
  settings <- yaml::read_yaml(file.path(project_dir, "settings.yml"))
  expect_equal(settings$default$dotenv_location, "../")
})

test_that(".env file handles special characters in values", {
  # Create temp path for test
  project_dir <- tempfile("framework_test_")
  on.exit(unlink(project_dir, recursive = TRUE), add = TRUE)

  suppressMessages({
    result <- framework::project_create(
      name = "EnvTest",
      location = project_dir,
      type = "project",
      git = list(use_git = FALSE)
    )
  })

  project_path <- result$path

  # Create .env with various special cases
  env_content <- c(
    'SIMPLE=value',
    'WITH_SPACES="value with spaces"',
    'WITH_EQUALS=key=value',
    'EMPTY=',
    'WITH_QUOTES="value with \\"quotes\\""'
  )
  writeLines(env_content, file.path(project_path, ".env"))

  # Read back
  read_content <- readLines(file.path(project_path, ".env"))

  # Verify content preserved
  expect_true(any(grepl('SIMPLE=value', read_content)))
  expect_true(any(grepl('WITH_SPACES=', read_content)))
  expect_true(any(grepl('WITH_EQUALS=', read_content)))
  expect_true(any(grepl('EMPTY=', read_content)))
})

test_that(".env file grouping by prefix works", {
  # Create temp path for test
  project_dir <- tempfile("framework_test_")
  on.exit(unlink(project_dir, recursive = TRUE), add = TRUE)

  suppressMessages({
    result <- framework::project_create(
      name = "EnvTest",
      location = project_dir,
      type = "project",
      git = list(use_git = FALSE)
    )
  })

  project_path <- result$path

  # Create .env with mixed prefixes
  env_content <- c(
    "API_KEY=key1",
    "DB_HOST=localhost",
    "API_SECRET=secret1",
    "DB_PORT=5432",
    "NAS_PATH=/mnt/nas",
    "OTHER_VAR=value"
  )
  writeLines(env_content, file.path(project_path, ".env"))

  # Parse and group by prefix
  lines <- readLines(file.path(project_path, ".env"))
  vars <- list()

  for (line in lines) {
    if (grepl("^\\s*#", line) || grepl("^\\s*$", line)) next
    if (grepl("=", line)) {
      parts <- strsplit(line, "=", fixed = TRUE)[[1]]
      if (length(parts) >= 2) {
        key <- trimws(parts[1])
        value <- paste(parts[-1], collapse = "=")
        value <- gsub('^"(.*)"$', '\\1', value)  # Remove quotes
        vars[[key]] <- value
      }
    }
  }

  # Group by prefix
  get_prefix <- function(key) {
    parts <- strsplit(key, "_")[[1]]
    if (length(parts) > 1) parts[1] else "OTHER"
  }

  prefixes <- sapply(names(vars), get_prefix)
  unique_prefixes <- unique(prefixes)

  # Verify grouping
  expect_true("API" %in% unique_prefixes)
  expect_true("DB" %in% unique_prefixes)
  expect_true("NAS" %in% unique_prefixes)
  expect_true("OTHER" %in% unique_prefixes)

  # Count variables in each group
  api_vars <- sum(prefixes == "API")
  db_vars <- sum(prefixes == "DB")
  nas_vars <- sum(prefixes == "NAS")
  other_vars <- sum(prefixes == "OTHER")

  expect_equal(api_vars, 2)
  expect_equal(db_vars, 2)
  expect_equal(nas_vars, 1)
  expect_equal(other_vars, 1)
})

test_that(".env scanning for env() usage in R files", {
  # Create temp path for test
  project_dir <- tempfile("framework_test_")
  on.exit(unlink(project_dir, recursive = TRUE), add = TRUE)

  suppressMessages({
    result <- framework::project_create(
      name = "EnvTest",
      location = project_dir,
      type = "project",
      directories = list(functions = "functions"),
      git = list(use_git = FALSE)
    )
  })

  project_path <- result$path

  # Create R file with env() calls
  r_code <- c(
    'db_host <- env("DB_HOST")',
    'db_port <- env("DB_PORT")',
    'api_key <- env("API_KEY")',
    '# Comment with env("FAKE_VAR")',
    "another <- env('NAS_PATH')"
  )
  writeLines(r_code, file.path(project_path, "functions", "config.R"))

  # Scan for env() usage
  content <- readLines(file.path(project_path, "functions", "config.R"))
  content_str <- paste(content, collapse = "\n")

  # Find env("VARIABLE") or env('VARIABLE') patterns
  matches <- gregexpr('env\\(["\']([^"\']+)["\']', content_str, perl = TRUE)
  match_data <- regmatches(content_str, matches)[[1]]

  # Extract variable names
  var_names <- gsub('env\\(["\']([^"\']+)["\']', '\\1', match_data, perl = TRUE)

  # Verify found variables
  expect_true("DB_HOST" %in% var_names)
  expect_true("DB_PORT" %in% var_names)
  expect_true("API_KEY" %in% var_names)
  expect_true("NAS_PATH" %in% var_names)
  expect_true("FAKE_VAR" %in% var_names)  # Even in comments

  expect_equal(length(var_names), 5)
})

test_that(".env file edge cases", {
  # Create temp path for test
  project_dir <- tempfile("framework_test_")
  on.exit(unlink(project_dir, recursive = TRUE), add = TRUE)

  suppressMessages({
    result <- framework::project_create(
      name = "EnvTest",
      location = project_dir,
      type = "project",
      git = list(use_git = FALSE)
    )
  })

  project_path <- result$path

  # Test empty .env file
  writeLines("", file.path(project_path, ".env"))
  expect_true(file.exists(file.path(project_path, ".env")))

  # Test .env with only comments
  writeLines(c("# Comment 1", "# Comment 2"), file.path(project_path, ".env"))
  content <- readLines(file.path(project_path, ".env"))
  expect_equal(length(content), 2)
  expect_true(all(grepl("^#", content)))

  # Test .env with blank lines
  writeLines(c("VAR1=value1", "", "VAR2=value2", ""), file.path(project_path, ".env"))
  content <- readLines(file.path(project_path, ".env"))
  expect_equal(length(content), 4)

  # Test malformed lines (missing equals)
  writeLines(c(
    "VALID=value",
    "INVALID_NO_EQUALS",
    "ALSO_VALID=foo"
  ), file.path(project_path, ".env"))
  content <- readLines(file.path(project_path, ".env"))

  # Parse only valid lines
  vars <- list()
  for (line in content) {
    if (grepl("=", line) && !grepl("^\\s*#", line)) {
      parts <- strsplit(line, "=", fixed = TRUE)[[1]]
      if (length(parts) >= 2) {
        vars[[parts[1]]] <- paste(parts[-1], collapse = "=")
      }
    }
  }

  expect_equal(length(vars), 2)
  expect_equal(vars$VALID, "value")
  expect_equal(vars$ALSO_VALID, "foo")
  expect_null(vars$INVALID_NO_EQUALS)
})

test_that(".env regrouping creates proper structure", {
  # Create temp path for test
  project_dir <- tempfile("framework_test_")
  on.exit(unlink(project_dir, recursive = TRUE), add = TRUE)

  suppressMessages({
    result <- framework::project_create(
      name = "EnvTest",
      location = project_dir,
      type = "project",
      git = list(use_git = FALSE)
    )
  })

  project_path <- result$path

  # Simulate regrouping logic
  vars <- list(
    API_KEY = "key1",
    DB_HOST = "localhost",
    API_SECRET = "secret1",
    DB_PORT = "5432",
    OTHER_VAR = "value"
  )

  # Extract prefixes
  get_prefix <- function(key) {
    parts <- strsplit(key, "_")[[1]]
    if (length(parts) > 1) parts[1] else "OTHER"
  }

  prefixes <- sapply(names(vars), get_prefix)
  unique_prefixes <- unique(prefixes)
  # Sort prefixes, with OTHER last
  unique_prefixes <- c(sort(unique_prefixes[unique_prefixes != "OTHER"]), "OTHER")
  unique_prefixes <- unique_prefixes[unique_prefixes %in% prefixes]

  # Build lines
  lines <- c("# Environment Variables", "# Grouped by prefix", "")

  for (prefix in unique_prefixes) {
    keys_in_prefix <- names(vars)[prefixes == prefix]
    if (length(keys_in_prefix) > 0) {
      # Add section header
      if (prefix == "OTHER") {
        lines <- c(lines, "# Other Variables")
      } else {
        lines <- c(lines, sprintf("# %s Variables", toupper(prefix)))
      }

      # Add variables (sorted)
      for (key in sort(keys_in_prefix)) {
        value <- vars[[key]]
        if (grepl(" ", value)) {
          lines <- c(lines, sprintf('%s="%s"', key, value))
        } else {
          lines <- c(lines, sprintf('%s=%s', key, value))
        }
      }
      lines <- c(lines, "")
    }
  }

  # Write regrouped file
  writeLines(lines, file.path(project_path, ".env"))

  # Read back and verify
  content <- readLines(file.path(project_path, ".env"))

  # Verify headers exist
  expect_true(any(grepl("# API Variables", content)))
  expect_true(any(grepl("# DB Variables", content)))
  expect_true(any(grepl("# Other Variables", content)))

  # Verify order (API before DB before OTHER)
  api_header_line <- which(grepl("# API Variables", content))
  db_header_line <- which(grepl("# DB Variables", content))
  other_header_line <- which(grepl("# Other Variables", content))

  expect_true(api_header_line < db_header_line)
  expect_true(db_header_line < other_header_line)

  # Verify variables are grouped correctly
  api_key_line <- which(grepl("^API_KEY=", content))
  api_secret_line <- which(grepl("^API_SECRET=", content))
  db_host_line <- which(grepl("^DB_HOST=", content))
  db_port_line <- which(grepl("^DB_PORT=", content))

  # All API vars should be before all DB vars
  expect_true(max(c(api_key_line, api_secret_line)) < min(c(db_host_line, db_port_line)))
})

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.