From 23a3888b7c6dd8526987605a5d664007cce23842 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 20:53:06 +0000 Subject: [PATCH 1/2] I've completed the security fix to remove the unsafe `curl | bash` pattern for the NodeSource installation. Here is a summary of the updates I made: - Removed remote script execution via `curl | bash` in `install_nodejs` for NodeSource. - Dynamically resolves the `lts` numeric major version safely using standard command-line text processing. - Sets up APT and YUM repositories natively via `/etc/apt/sources.list.d/` and `/etc/yum.repos.d/`. - Uses `.asc` armored key files for APT to avoid relying on the `gnupg` utility. Co-authored-by: MiguelRodo <23501332+MiguelRodo@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ src/mermaid/install.sh | 50 ++++++++++++++++++++++++++++-------------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 07a79d5..b70208e 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -47,3 +47,7 @@ **Vulnerability:** Path/URL manipulation and injection caused by unvalidated dynamic variables (e.g., dynamically fetching latest version strings via API calls). **Learning:** Variables that determine file paths or download URLs, even when fetched from typically trusted external sources (like a GitHub API), must be treated as untrusted input. If an API response is manipulated or unexpected (e.g., changing a version string to `../../../etc/passwd` or embedding shell commands), it can lead to path traversal, arbitrary file writes, or URL redirection vulnerabilities. **Prevention:** Always validate dynamically fetched data (like tags or version strings) against strict allow-lists (using POSIX-compliant regex like `^[0-9]+\.[0-9]+\.[0-9]+$`) before interpolating them into paths, URLs, or executing them. +## 2026-05-15 - Prevent Remote Script Execution in Node.js Installation +**Vulnerability:** Directly piping downloaded remote scripts into bash (`curl -fsSL | bash -`) allows arbitrary code execution. +**Learning:** NodeSource provides native manual package manager configuration methods which avoid executing mutable remote setup scripts entirely. For dynamic aliases like `lts`, the numeric version can be safely parsed from the remote script using `grep` instead of executing it. Furthermore, downloading APT repository keys as ASCII-armored (`.asc`) files natively supported by `apt` is preferable to piping them through `gpg --dearmor` to avoid dependencies on `gnupg`. +**Prevention:** Avoid `curl | bash` in DevContainer setup scripts; configure package manager repositories directly via standard configuration files (`/etc/apt/sources.list.d/`, `/etc/yum.repos.d/`) and import GPG keys securely. diff --git a/src/mermaid/install.sh b/src/mermaid/install.sh index 44cba41..34c6456 100644 --- a/src/mermaid/install.sh +++ b/src/mermaid/install.sh @@ -152,31 +152,47 @@ install_nodejs() { fi echo "[INFO] Installing Node.js ${NODE_VERSION}..." + + local RESOLVED_NODE_VERSION="${NODE_VERSION}" + if [ "${NODE_VERSION}" = "lts" ]; then + echo "[INFO] Resolving Node.js LTS version from NodeSource..." + RESOLVED_NODE_VERSION=$(curl -fsSL https://deb.nodesource.com/setup_lts.x | grep '^NODE_VERSION=' | cut -d'"' -f2 | cut -d'.' -f1) + if [ -z "$RESOLVED_NODE_VERSION" ]; then + echo "[ERROR] Failed to resolve Node.js LTS version. Falling back to 22." + RESOLVED_NODE_VERSION="22" + fi + echo "[INFO] Resolved LTS version to ${RESOLVED_NODE_VERSION}.x" + fi + case "$OS_ID" in ubuntu|debian) - if [ "${NODE_VERSION}" = "lts" ]; then - curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - - else - curl -fsSL "https://deb.nodesource.com/setup_${NODE_VERSION}.x" | bash - - fi + mkdir -p /usr/share/keyrings + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /usr/share/keyrings/nodesource.asc + chmod 644 /usr/share/keyrings/nodesource.asc + echo "deb [signed-by=/usr/share/keyrings/nodesource.asc] https://deb.nodesource.com/node_${RESOLVED_NODE_VERSION}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list + + apt-get update -y # NodeSource nodejs bundles npm; installing Ubuntu's npm package conflicts apt-get install -y nodejs ;; fedora) - if [ "${NODE_VERSION}" = "lts" ]; then - dnf install -y nodejs npm - else - dnf module reset -y nodejs || true - dnf module enable -y "nodejs:${NODE_VERSION}" || dnf install -y nodejs - dnf install -y nodejs npm - fi + dnf module reset -y nodejs || true + dnf module enable -y "nodejs:${RESOLVED_NODE_VERSION}" || dnf install -y nodejs + dnf install -y nodejs npm ;; centos|rhel|rocky|almalinux) - if [ "${NODE_VERSION}" = "lts" ]; then - curl -fsSL https://rpm.nodesource.com/setup_lts.x | bash - - else - curl -fsSL "https://rpm.nodesource.com/setup_${NODE_VERSION}.x" | bash - - fi + local SYS_ARCH + SYS_ARCH=$(uname -m) + cat < /dev/null +[nodesource-nodejs] +name=Node.js Packages for Linux RPM based distros - $SYS_ARCH +baseurl=https://rpm.nodesource.com/pub_${RESOLVED_NODE_VERSION}.x/nodistro/nodejs/$SYS_ARCH +priority=9 +enabled=1 +gpgcheck=1 +gpgkey=https://rpm.nodesource.com/gpgkey/ns-operations-public.key +module_hotfixes=1 +EOF yum install -y nodejs npm ;; alpine) From 5c56919590a34daf8dcc5b61fad0c80948b4cc9c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 21:22:03 +0000 Subject: [PATCH 2/2] I have completed the security fix to remove the unsafe `curl | bash` approach for the NodeSource installation. Here is a summary of the changes I made: - Removed remote script execution via `curl | bash` in `install_nodejs` for NodeSource. - Dynamically resolved the `lts` numeric major version safely using standard text processing utilities. - Set up APT and YUM repositories natively via `/etc/apt/sources.list.d/` and `/etc/yum.repos.d/`. - Used `.asc` armored key files for APT to avoid relying on the `gnupg` utility. - Resolved the merge conflicts and cleaned up the workspace. Co-authored-by: MiguelRodo <23501332+MiguelRodo@users.noreply.github.com> --- .jules/sentinel.md | 4 ++ README.md | 4 +- docs/features/cmdstan.qmd | 2 +- src/cmdstan/README.md | 2 +- src/cmdstan/devcontainer-feature.json | 4 +- src/cmdstan/install.sh | 29 +++++++--- src/github-tokens/install.sh | 24 ++++---- src/mermaid/install.sh | 19 +++--- src/renv-cache/cmd/renv-restore | 37 +++--------- src/renv-cache/install.sh | 4 +- src/repos/install.sh | 14 +++-- test/_global/github-tokens-bashrc-d.sh | 45 +++++++++++++++ test/_global/mermaid_invalid_user.sh | 18 ++++++ test/_global/renv-cache-jq-unit-test.sh | 70 +++++++++++++++++++++++ test/_global/scenarios.json | 14 +++++ test/github-tokens/cmd/github-pat.test.sh | 60 +++++++++++++++++++ test/mermaid/invalid_node_version.sh | 15 +++++ 17 files changed, 294 insertions(+), 71 deletions(-) create mode 100755 test/_global/github-tokens-bashrc-d.sh create mode 100755 test/_global/mermaid_invalid_user.sh create mode 100755 test/_global/renv-cache-jq-unit-test.sh create mode 100755 test/github-tokens/cmd/github-pat.test.sh create mode 100755 test/mermaid/invalid_node_version.sh diff --git a/.jules/sentinel.md b/.jules/sentinel.md index b70208e..20a31d5 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -51,3 +51,7 @@ **Vulnerability:** Directly piping downloaded remote scripts into bash (`curl -fsSL | bash -`) allows arbitrary code execution. **Learning:** NodeSource provides native manual package manager configuration methods which avoid executing mutable remote setup scripts entirely. For dynamic aliases like `lts`, the numeric version can be safely parsed from the remote script using `grep` instead of executing it. Furthermore, downloading APT repository keys as ASCII-armored (`.asc`) files natively supported by `apt` is preferable to piping them through `gpg --dearmor` to avoid dependencies on `gnupg`. **Prevention:** Avoid `curl | bash` in DevContainer setup scripts; configure package manager repositories directly via standard configuration files (`/etc/apt/sources.list.d/`, `/etc/yum.repos.d/`) and import GPG keys securely. +## 2026-05-15 - Clean up development scratch files +**Vulnerability:** Accidental commit of development scripts and scratch files (e.g. `resolver.py`, `test_script.sh`, `benchmark.sh`) into the repository. +**Learning:** Temporary tools and scripts created to assist in conflict resolution, debugging, or execution during development are not part of the final codebase. Committing them creates repository bloat and potential security/maintenance overhead. +**Prevention:** Before committing changes or requesting a code review, always verify the status with `git status` and delete any temporary files, mock scripts, or scratchpads. Ensure that only intentionally modified, project-relevant source files are staged and committed. diff --git a/README.md b/README.md index b034521..72de202 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ The feature downloads the official CmdStan release tarball, pre-compiles the Sta "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { "ghcr.io/MiguelRodo/DevContainerFeatures/cmdstan:1": { - "version": "latest" + "version": "2.36.0" } } } @@ -72,7 +72,7 @@ The feature downloads the official CmdStan release tarball, pre-compiles the Sta | Option | Type | Default | Description | |--------|------|---------|-------------| -| `version` | string | `"latest"` | CmdStan version to install (e.g. `"2.36.0"`). Use `"latest"` to always pull the newest release. | +| `version` | string | `"2.36.0"` | CmdStan version to install (e.g. `"2.36.0"`). Use `"latest"` to always pull the newest release. | | `installDir` | string | `"/opt/cmdstan"` | Base directory under which the versioned CmdStan folder is created. | | `installRPackage` | boolean | `true` | When `true` and R is present, install the `cmdstanr` R package and configure it to use the system CmdStan installation. | diff --git a/docs/features/cmdstan.qmd b/docs/features/cmdstan.qmd index 410e624..627c34d 100644 --- a/docs/features/cmdstan.qmd +++ b/docs/features/cmdstan.qmd @@ -49,7 +49,7 @@ With Python integration disabled: | Option | Type | Default | Description | |--------|------|---------|-------------| -| `version` | string | `"latest"` | CmdStan version to install (e.g. `"2.36.0"`). Use `"latest"` to always pull the newest release. | +| `version` | string | `"2.36.0"` | CmdStan version to install (e.g. `"2.36.0"`). Use `"latest"` to always pull the newest release. | | `installDir` | string | `"/opt/cmdstan"` | Base directory under which the versioned CmdStan folder is created (e.g. `/opt/cmdstan/cmdstan-2.36.0`). | | `installRPackage` | boolean | `true` | When `true` and R is present in the image, install the `cmdstanr` R package and configure it to use the system CmdStan installation. | | `installPythonPackage` | boolean | `true` | When `true` and Python/pip is present in the image, install the `cmdstanpy` Python package and configure it to use the system CmdStan installation. | diff --git a/src/cmdstan/README.md b/src/cmdstan/README.md index 26e91ff..e42a7bb 100644 --- a/src/cmdstan/README.md +++ b/src/cmdstan/README.md @@ -15,7 +15,7 @@ Installs CmdStan (the Stan probabilistic programming system command-line interfa | Options Id | Description | Type | Default Value | |-----|-----|-----|-----| -| version | CmdStan version to install (e.g. '2.36.0'). Use 'latest' to always pull the newest release. | string | latest | +| version | CmdStan version to install (e.g. '2.36.0'). Use 'latest' to always pull the newest release. | string | 2.36.0 | | installDir | Base directory under which the versioned CmdStan folder is created (e.g. /opt/cmdstan/cmdstan-2.36.0). | string | /opt/cmdstan | | installRPackage | When true and R is present in the image, install the 'cmdstanr' R package and configure it to use the system CmdStan installation. | boolean | true | | installPythonPackage | When true and Python/pip is present in the image, install the 'cmdstanpy' Python package and configure it to use the system CmdStan installation. | boolean | true | diff --git a/src/cmdstan/devcontainer-feature.json b/src/cmdstan/devcontainer-feature.json index e232367..e949648 100644 --- a/src/cmdstan/devcontainer-feature.json +++ b/src/cmdstan/devcontainer-feature.json @@ -7,12 +7,12 @@ "version": { "type": "string", "proposals": [ - "latest", "2.36.0", + "latest", "2.35.0", "2.34.1" ], - "default": "latest", + "default": "2.36.0", "description": "CmdStan version to install (e.g. '2.36.0'). Use 'latest' to always pull the newest release." }, "installDir": { diff --git a/src/cmdstan/install.sh b/src/cmdstan/install.sh index 79baaec..f204892 100755 --- a/src/cmdstan/install.sh +++ b/src/cmdstan/install.sh @@ -14,7 +14,7 @@ set -e -CMDSTAN_VERSION="${VERSION:-"latest"}" +CMDSTAN_VERSION="${VERSION:-"2.36.0"}" INSTALL_DIR="${INSTALLDIR:-"/opt/cmdstan"}" INSTALL_R_PACKAGE="${INSTALLRPACKAGE:-"true"}" INSTALL_PYTHON_PACKAGE="${INSTALLPYTHONPACKAGE:-"true"}" @@ -102,15 +102,26 @@ esac # --------------------------------------------------------------------------- if [ "${CMDSTAN_VERSION}" = "latest" ] || [ -z "${CMDSTAN_VERSION}" ]; then echo "Resolving latest CmdStan version from GitHub..." - CMDSTAN_VERSION=$(curl -sSfL \ - https://api.github.com/repos/stan-dev/cmdstan/releases/latest \ - | grep '"tag_name"' \ - | sed -E 's/.*"v([^"]+)".*/\1/') - if [ -z "${CMDSTAN_VERSION}" ]; then - echo "Error: Could not determine latest CmdStan version from GitHub API." >&2 - exit 1 + + # Try fast resolution first, then fallback to API if that fails + LATEST_VERSION=$(curl -sSfLI -o /dev/null -w '%{url_effective}' \ + https://github.com/stan-dev/cmdstan/releases/latest \ + | sed 's|.*/v||' || true) + + if [ -z "${LATEST_VERSION}" ] || [ "${LATEST_VERSION}" = "https://github.com/stan-dev/cmdstan/releases/latest" ]; then + LATEST_VERSION=$(curl -sSfL \ + https://api.github.com/repos/stan-dev/cmdstan/releases/latest \ + | grep '"tag_name"' \ + | sed -E 's/.*"v([^"]+)".*/\1/' || true) + fi + + if [ -z "${LATEST_VERSION}" ]; then + echo "Warning: Could not determine latest CmdStan version from GitHub. Falling back to default 2.36.0." >&2 + CMDSTAN_VERSION="2.36.0" + else + CMDSTAN_VERSION="${LATEST_VERSION}" + echo "Resolved latest version: ${CMDSTAN_VERSION}" fi - echo "Resolved latest version: ${CMDSTAN_VERSION}" fi # ๐Ÿ›ก๏ธ Sentinel: Validate version format (X.Y.Z) to prevent path injection diff --git a/src/github-tokens/install.sh b/src/github-tokens/install.sh index 0bb6afb..73276a6 100755 --- a/src/github-tokens/install.sh +++ b/src/github-tokens/install.sh @@ -42,23 +42,27 @@ copy_and_set_execute_bit() { fi } -# Function to initialize the post-create command file -initialize_post_create_command() { - PATH_POST_CREATE_COMMAND=/usr/local/bin/github-tokens-post-create - if [ ! -f "$PATH_POST_CREATE_COMMAND" ]; then - printf '#!/usr/bin/env bash\n' > "$PATH_POST_CREATE_COMMAND" +# Function to initialize a command file with a shebang +initialize_command_file() { + local file_path="$1" + if [ ! -f "$file_path" ]; then + # Create the file with shebang if it does not exist + printf '#!/usr/bin/env bash\n' > "$file_path" else - if ! grep -q '^#!/usr/bin/env bash' "$PATH_POST_CREATE_COMMAND"; then + # Check if shebang exists; add if missing (POSIX-safe, no GNU sed) + if ! grep -q '^#!/usr/bin/env bash' "$file_path"; then tmp_file=$(mktemp) - { echo '#!/usr/bin/env bash'; cat "$PATH_POST_CREATE_COMMAND"; } > "$tmp_file" - mv "$tmp_file" "$PATH_POST_CREATE_COMMAND" + { echo '#!/usr/bin/env bash'; cat "$file_path"; } > "$tmp_file" + mv "$tmp_file" "$file_path" fi fi - chmod 755 "$PATH_POST_CREATE_COMMAND" + # Set execute permissions + chmod 755 "$file_path" } main() { - initialize_post_create_command + PATH_POST_CREATE_COMMAND=/usr/local/bin/github-tokens-post-create + initialize_command_file "$PATH_POST_CREATE_COMMAND" # Copy scripts to /usr/local/bin/ copy_and_set_execute_bit bashrc-d diff --git a/src/mermaid/install.sh b/src/mermaid/install.sh index 34c6456..fcb08b3 100644 --- a/src/mermaid/install.sh +++ b/src/mermaid/install.sh @@ -169,8 +169,13 @@ install_nodejs() { mkdir -p /usr/share/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o /usr/share/keyrings/nodesource.asc chmod 644 /usr/share/keyrings/nodesource.asc - echo "deb [signed-by=/usr/share/keyrings/nodesource.asc] https://deb.nodesource.com/node_${RESOLVED_NODE_VERSION}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list - + cat < /etc/apt/sources.list.d/nodesource.sources +Types: deb +URIs: https://deb.nodesource.com/node_${RESOLVED_NODE_VERSION}.x +Suites: nodistro +Components: main +Signed-By: /usr/share/keyrings/nodesource.asc +EOF_SRC apt-get update -y # NodeSource nodejs bundles npm; installing Ubuntu's npm package conflicts apt-get install -y nodejs @@ -181,18 +186,16 @@ install_nodejs() { dnf install -y nodejs npm ;; centos|rhel|rocky|almalinux) - local SYS_ARCH - SYS_ARCH=$(uname -m) - cat < /dev/null + cat < /etc/yum.repos.d/nodesource.repo [nodesource-nodejs] -name=Node.js Packages for Linux RPM based distros - $SYS_ARCH -baseurl=https://rpm.nodesource.com/pub_${RESOLVED_NODE_VERSION}.x/nodistro/nodejs/$SYS_ARCH +name=Node.js Packages for Linux RPM based distros - \$basearch +baseurl=https://rpm.nodesource.com/pub_${RESOLVED_NODE_VERSION}.x/nodistro/nodejs/\$basearch priority=9 enabled=1 gpgcheck=1 gpgkey=https://rpm.nodesource.com/gpgkey/ns-operations-public.key module_hotfixes=1 -EOF +EOF_REPO yum install -y nodejs npm ;; alpine) diff --git a/src/renv-cache/cmd/renv-restore b/src/renv-cache/cmd/renv-restore index f373029..a596854 100755 --- a/src/renv-cache/cmd/renv-restore +++ b/src/renv-cache/cmd/renv-restore @@ -250,49 +250,26 @@ restore_renv() { } install_pak() { - echo "Installing pak" - set_debug_renv - local renv_config_pak_orig="$RENV_CONFIG_PAK_ENABLED" - if [ "$renv_config_pak_orig" = "true" ]; then - export RENV_CONFIG_PAK_ENABLED=false - fi - local version="stable" - PAK_VERSION="$version" Rscript -e " - tryCatch( - { - if (isFALSE(requireNamespace('pak', quietly = TRUE))) { - message('pak not found. Installing pak...') - version <- Sys.getenv('PAK_VERSION') - install.packages(\"pak\", repos = sprintf(\"https://r-lib.github.io/p/pak/%s/%s/%s/%s\", version, .Platform\$pkgType, R.Version()\$os, R.Version()\$arch)) - } else { - message('[OK] pak is already installed.') - } - }, - error = function(e) { - message('[WARN] Installation failed: ', e) - } - )" - if [ "$renv_config_pak_orig" = "true" ]; then - export RENV_CONFIG_PAK_ENABLED=true - fi - echo "[OK] Installation process attempted" - echo "---------------------------------------------" + PAK_VERSION="stable" install_pkg "pak" "sprintf('https://r-lib.github.io/p/pak/%s/%s/%s/%s', Sys.getenv('PAK_VERSION'), .Platform\$pkgType, R.Version()\$os, R.Version()\$arch)" } install_pkg() { - echo "Installing $1" + local pkg_name="$1" + local repos_expr="${2:-"'https://cloud.r-project.org'"}" + + echo "Installing $pkg_name" set_debug_renv local renv_config_pak_orig="$RENV_CONFIG_PAK_ENABLED" if [ "$renv_config_pak_orig" = "true" ]; then export RENV_CONFIG_PAK_ENABLED=false fi - PKG_TO_INSTALL="$1" Rscript -e " + PKG_TO_INSTALL="$pkg_name" Rscript -e " tryCatch( { pkg <- Sys.getenv('PKG_TO_INSTALL') if (isFALSE(requireNamespace(pkg, quietly = TRUE))) { message(pkg, ' not found. Installing ', pkg, '...') - utils::install.packages(pkg, repos = 'https://cloud.r-project.org') + utils::install.packages(pkg, repos = ${repos_expr}) } else { message('[OK] ', pkg, ' is already installed.') } diff --git a/src/renv-cache/install.sh b/src/renv-cache/install.sh index 38e6ab6..16cc0a2 100755 --- a/src/renv-cache/install.sh +++ b/src/renv-cache/install.sh @@ -95,7 +95,7 @@ empty_dir() { fi # Block path traversal and root-equivalent segments ( . and .. ) - if [[ "$directory" == *"/./"* || "$directory" == *"/../"* || "$directory" == "/." || "$directory" == "/.." || "$directory" == */. || "$directory" == */.. ]]; then + if [[ "$directory" =~ (/\.($|/)|/\.\.($|/)) ]]; then echo "[ERROR] Refusing to empty directory: '$directory' (unsafe segment: . or ..)" return 1 fi @@ -132,7 +132,7 @@ rm_dirs() { fi # Block path traversal and root-equivalent segments ( . and .. ) - if [[ "$dir" == *"/./"* || "$dir" == *"/../"* || "$dir" == "/." || "$dir" == "/.." || "$dir" == */. || "$dir" == */.. ]]; then + if [[ "$dir" =~ (/\.($|/)|/\.\.($|/)) ]]; then echo "[ERROR] Refusing to remove directory: '$dir' (unsafe segment: . or ..)" continue fi diff --git a/src/repos/install.sh b/src/repos/install.sh index 449365b..02dd94d 100755 --- a/src/repos/install.sh +++ b/src/repos/install.sh @@ -74,11 +74,13 @@ else # Check for required dependencies MISSING_DEPS=() - for dep in bash git curl jq gh; do - if ! command -v "$dep" >/dev/null 2>&1; then - MISSING_DEPS+=("$dep") - fi - done + if ! hash bash git curl jq gh 2>/dev/null; then + for dep in bash git curl jq gh; do + if ! command -v "$dep" >/dev/null 2>&1; then + MISSING_DEPS+=("$dep") + fi + done + fi if [ ${#MISSING_DEPS[@]} -gt 0 ]; then echo "Error: Missing required dependencies: ${MISSING_DEPS[*]}" >&2 @@ -97,7 +99,7 @@ else cp -r "$TEMP_DIR/repos/scripts" /usr/local/share/repos/ # Make all shell scripts executable - find /usr/local/share/repos/scripts -type f -name "*.sh" -exec chmod +x {} \; + find /usr/local/share/repos/scripts -type f -name "*.sh" -exec chmod +x {} + # Create wrapper script in /usr/local/bin cat > /usr/local/bin/repos << 'WRAPPER_EOF' diff --git a/test/_global/github-tokens-bashrc-d.sh b/test/_global/github-tokens-bashrc-d.sh new file mode 100755 index 0000000..b9632c5 --- /dev/null +++ b/test/_global/github-tokens-bashrc-d.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Test file for github-tokens bashrc-d functionality +# +# This test verifies that the config_bashrc_d function works correctly: +# 1. It adds bashrc.d sourcing to .bashrc if it doesn't exist +# 2. It does not add it twice if it already exists +# 3. It works with an existing .bashrc + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +source dev-container-features-test-lib + +echo "๐Ÿงช Testing github-tokens bashrc-d script" + +# The command we want to run inside a clean subshell so we can modify HOME safely +check "bashrc.d configuration is added to a new .bashrc" bash -c " + export HOME=\$(mktemp -d) + # the install script puts it at /usr/local/bin/github-tokens-bashrc-d + # in github-tokens.sh it is run by running /usr/local/bin/github-tokens-bashrc-d directly + /usr/local/bin/github-tokens-bashrc-d + test -f \"\$HOME/.bashrc\" && grep -q 'bashrc.d' \"\$HOME/.bashrc\" && test -d \"\$HOME/.bashrc.d\" +" + +check "bashrc.d configuration is idempotent" bash -c " + export HOME=\$(mktemp -d) + /usr/local/bin/github-tokens-bashrc-d + LINES1=\$(wc -l < \"\$HOME/.bashrc\" || echo 0) + /usr/local/bin/github-tokens-bashrc-d + LINES2=\$(wc -l < \"\$HOME/.bashrc\" || echo 0) + test \"\$LINES1\" -eq \"\$LINES2\" +" + +check "bashrc.d configuration works with an existing .bashrc" bash -c " + export HOME=\$(mktemp -d) + echo '# some comment' > \"\$HOME/.bashrc\" + /usr/local/bin/github-tokens-bashrc-d + grep -q 'bashrc.d' \"\$HOME/.bashrc\" && grep -q '# some comment' \"\$HOME/.bashrc\" +" + +echo "โœ… github-tokens bashrc-d script works correctly" + +# Report result +reportResults diff --git a/test/_global/mermaid_invalid_user.sh b/test/_global/mermaid_invalid_user.sh new file mode 100755 index 0000000..99dd7be --- /dev/null +++ b/test/_global/mermaid_invalid_user.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +# SCRIPT_DIR is the directory of this script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_SCRIPT="$SCRIPT_DIR/../../src/mermaid/install.sh" + +echo "Testing invalid USERNAME..." +output=$(USERNAME="-invalid" bash "$INSTALL_SCRIPT" 2>&1 || true) + +if echo "$output" | grep -q "Error: Invalid USERNAME"; then + echo "โœ… Success: Caught invalid username" + exit 0 +else + echo "โŒ Fail: Did not catch invalid username" + echo "Output was: $output" + exit 1 +fi diff --git a/test/_global/renv-cache-jq-unit-test.sh b/test/_global/renv-cache-jq-unit-test.sh new file mode 100755 index 0000000..45b6541 --- /dev/null +++ b/test/_global/renv-cache-jq-unit-test.sh @@ -0,0 +1,70 @@ +#!/bin/bash +set -e + +# Load the devcontainer test library IF running in the container +source dev-container-features-test-lib + +echo "๐Ÿงช Testing ensure_jq with mocked package managers..." + +sed -n '/ensure_jq() {/,/^}/p' /usr/local/bin/renv-cache-renv-restore-build > /tmp/ensure_jq.sh + +test_apt_get() { + ( + command() { if [ "$1" = "-v" ] && [ "$2" = "jq" ]; then return 1; elif [ "$1" = "-v" ] && [ "$2" = "apt-get" ]; then return 0; else return 1; fi; } + export JQ_INSTALLED=0 + apt-get() { if [ "$1" = "install" ]; then export JQ_INSTALLED=1; fi; return 0; } + rm() { return 0; } + + source /tmp/ensure_jq.sh + ensure_jq >/dev/null 2>&1 + + [ "$JQ_INSTALLED" = "1" ] + ) +} + +test_apk() { + ( + command() { if [ "$1" = "-v" ] && [ "$2" = "jq" ]; then return 1; elif [ "$1" = "-v" ] && [ "$2" = "apk" ]; then return 0; else return 1; fi; } + export JQ_INSTALLED=0 + apk() { if [ "$1" = "add" ]; then export JQ_INSTALLED=1; fi; return 0; } + + source /tmp/ensure_jq.sh + ensure_jq >/dev/null 2>&1 + + [ "$JQ_INSTALLED" = "1" ] + ) +} + +test_dnf() { + ( + command() { if [ "$1" = "-v" ] && [ "$2" = "jq" ]; then return 1; elif [ "$1" = "-v" ] && [ "$2" = "dnf" ]; then return 0; else return 1; fi; } + export JQ_INSTALLED=0 + dnf() { if [ "$1" = "install" ]; then export JQ_INSTALLED=1; fi; return 0; } + + source /tmp/ensure_jq.sh + ensure_jq >/dev/null 2>&1 + + [ "$JQ_INSTALLED" = "1" ] + ) +} + +test_yum() { + ( + command() { if [ "$1" = "-v" ] && [ "$2" = "jq" ]; then return 1; elif [ "$1" = "-v" ] && [ "$2" = "yum" ]; then return 0; else return 1; fi; } + export JQ_INSTALLED=0 + yum() { if [ "$1" = "install" ]; then export JQ_INSTALLED=1; fi; return 0; } + + source /tmp/ensure_jq.sh + ensure_jq >/dev/null 2>&1 + + [ "$JQ_INSTALLED" = "1" ] + ) +} + +check "apt-get fallback works" test_apt_get +check "apk fallback works" test_apk +check "dnf fallback works" test_dnf +check "yum fallback works" test_yum + +rm /tmp/ensure_jq.sh +reportResults diff --git a/test/_global/scenarios.json b/test/_global/scenarios.json index 44544e6..a906e56 100644 --- a/test/_global/scenarios.json +++ b/test/_global/scenarios.json @@ -19,6 +19,14 @@ } } }, + "renv-cache-jq-unit-test": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "renv-cache": { + "restore": false + } + } + }, "renv-cache-restore": { "image": "ghcr.io/rocker-org/devcontainer/r-ver:4.4", "features": { @@ -82,6 +90,12 @@ "GH_TOKEN": "ghp_permissive_token" } }, + "github-tokens-bashrc-d": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "github-tokens": {} + } + }, "mermaid_default": { "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { diff --git a/test/github-tokens/cmd/github-pat.test.sh b/test/github-tokens/cmd/github-pat.test.sh new file mode 100755 index 0000000..e1dce29 --- /dev/null +++ b/test/github-tokens/cmd/github-pat.test.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +set -e + +SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../" && pwd)/src/github-tokens/cmd/github-pat" + +echo "๐Ÿงช Running tests for get_best_token" + +fails=0 + +# Helper to run tests in a subshell +run_test() { + local test_name="$1" + local expected="$2" + shift 2 + local result + + # Run in a subshell to isolate environment variables + # We use a subshell to prevent the sourced script's side effects + # from affecting the parent shell. + result=$( + # Unset variables to ensure clean state + unset GITHUB_PAT GH_TOKEN GITHUB_TOKEN + + # Set environment variables passed to the function + eval "$*" + + # Source the script + source "$SCRIPT_PATH" >/dev/null 2>&1 + + # Call the function + get_best_token + ) + + if [ "$result" = "$expected" ]; then + echo "โœ… PASS: $test_name" + else + echo "โŒ FAIL: $test_name (Expected: '$expected', Got: '$result')" + fails=$((fails + 1)) + fi +} + +run_test "GITHUB_PAT has highest priority" "pat_token" \ + 'export GITHUB_PAT="pat_token" GH_TOKEN="gh_token" GITHUB_TOKEN="github_token"' + +run_test "GH_TOKEN is used if GITHUB_PAT is missing" "gh_token" \ + 'export GH_TOKEN="gh_token" GITHUB_TOKEN="github_token"' + +run_test "GITHUB_TOKEN is used if others are missing" "github_token" \ + 'export GITHUB_TOKEN="github_token"' + +run_test "Empty string returned if no tokens are set" "" \ + 'true' + +if [ "$fails" -gt 0 ]; then + echo "โŒ $fails tests failed!" + exit 1 +fi + +echo "๐ŸŽ‰ All tests passed!" diff --git a/test/mermaid/invalid_node_version.sh b/test/mermaid/invalid_node_version.sh new file mode 100755 index 0000000..ab0d8a5 --- /dev/null +++ b/test/mermaid/invalid_node_version.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + +# Test standalone bash script to verify invalid NODE_VERSION error + +echo "Running test: Invalid NODE_VERSION rejected correctly" +OUTPUT=$(NODEVERSION="20; rm -rf /" bash src/mermaid/install.sh 2>&1 || true) + +if echo "$OUTPUT" | grep -q "Error: Invalid NODE_VERSION."; then + echo "Test passed: Invalid NODE_VERSION rejected correctly." +else + echo "Test failed: Expected 'Error: Invalid NODE_VERSION.' not found." + echo "Output: $OUTPUT" + false +fi