.env becomes optional. A credential it does not provide (an absent file, an absent or empty key) is asked, without echo: GITEA_TOKEN at the start, GITHUB_PAT and GITHUB_USER once GitHub is chosen. An invalid value is asked again and never shown; when input ends the run stops before any request. Asked tokens are registered for redaction at once. After the local project exists the script asks (default no) whether to create a .env in it. On a yes it holds only the needed keys, is created private (mode 600) from the start, is excluded from git through .git/info/exclude (no tracked file changes), is never replaced without a second yes and is never written when git tracks it. The summary names the keys, never the values. New library files credentials.sh and envfile.sh; README, .env.example and the security decisions updated; tests cover every case. Task: MIL-005#1 Task: MIL-005#2 Task: MIL-005#3 Task: MIL-005#4 Task: MIL-005#5 Closes #35 Closes #36 Closes #37 Closes #38 Closes #39 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
178 lines
7.1 KiB
Bash
178 lines
7.1 KiB
Bash
#!/usr/bin/env bash
|
|
# test-config.sh - tests for the config.env and .env parsing and validation
|
|
# (MIL-001 task: safe parser). Sourced by run-tests.sh.
|
|
|
|
# parse_ok FILE-CONTENT: parse a config file and print the CONFIG entries.
|
|
# shellcheck disable=SC2016 # snippet and fixture text is literal on purpose
|
|
parse_snippet() {
|
|
printf 'parse_env_file "%s" CONFIG_KEYS CONFIG\nfor k in "${!CONFIG[@]}"; do printf "%%s=%%s\\n" "$k" "${CONFIG[$k]}"; done | sort\n' "$WORK/c.env"
|
|
}
|
|
|
|
test_parser_accepts_valid_file() {
|
|
printf '# comment\n\nGITEA_URL="https://a.test/"\r\nGITHUB_WEB_URL='"'"'https://b.test'"'"'\nGITEA_API_URL=https://a.test/api/v1 # inline\n' >"$WORK/c.env"
|
|
run_lib "" "$(parse_snippet)"
|
|
assert_status "valid file" 0 "$STATUS"
|
|
assert_contains "quotes stripped" "$OUT" "GITEA_URL=https://a.test/"
|
|
assert_contains "single quotes" "$OUT" "GITHUB_WEB_URL=https://b.test"
|
|
assert_contains "inline comment dropped" "$OUT" "GITEA_API_URL=https://a.test/api/v1"
|
|
}
|
|
|
|
test_parser_rejects_bad_input() {
|
|
local case_name content expected
|
|
while IFS='|' read -r case_name content expected; do
|
|
printf '%b' "$content" >"$WORK/c.env"
|
|
run_lib "" "$(parse_snippet)"
|
|
assert_status "$case_name" 1 "$STATUS"
|
|
assert_contains "$case_name message" "$ERR" "$expected"
|
|
done <<'EOF'
|
|
unknown key|OTHER=1\n|unknown key 'OTHER'
|
|
credential in config|GITEA_TOKEN=abcdefgh12345\n|is a credential
|
|
not KEY=VALUE|just some text\n|line 1: expected KEY=VALUE
|
|
lowercase key|gitea_url=https://a.test\n|unknown key 'gitea_url'
|
|
duplicate key|GITEA_URL=https://a.test\nGITEA_URL=https://b.test\n|set twice
|
|
unbalanced quote|GITEA_URL="https://a.test\n|unbalanced
|
|
quote inside|GITEA_URL="https://a"b.test"\n|unbalanced
|
|
control character|GITEA_URL=https://a\x01b.test\n|control character
|
|
EOF
|
|
}
|
|
|
|
test_parser_missing_file() {
|
|
run_lib "" "parse_env_file \"$WORK/none.env\" CONFIG_KEYS CONFIG"
|
|
assert_status "missing file" 1 "$STATUS"
|
|
assert_contains "missing file message" "$ERR" "cannot read"
|
|
}
|
|
|
|
test_parser_never_executes_values() {
|
|
local marker="$WORK/pwned"
|
|
printf 'GITEA_URL=$(touch %s)\nGITHUB_WEB_URL=`touch %s`\n' "$marker" "$marker" >"$WORK/c.env"
|
|
run_lib "" "parse_env_file \"$WORK/c.env\" CONFIG_KEYS CONFIG
|
|
validate_config"
|
|
assert_file_missing "command substitution not run" "$marker"
|
|
assert_status "bad value rejected" 1 "$STATUS"
|
|
printf 'GITEA_TOKEN=$(touch %s)\n' "$marker" >"$WORK/e.env"
|
|
run_lib "" "parse_env_file \"$WORK/e.env\" CREDENTIAL_KEYS CREDENTIALS
|
|
validate_credentials"
|
|
assert_file_missing "token value not run" "$marker"
|
|
assert_status "bad token rejected" 1 "$STATUS"
|
|
}
|
|
|
|
test_config_validation() {
|
|
local case_name url expected
|
|
while IFS='|' read -r case_name url expected; do
|
|
printf 'GITEA_URL=%s\n' "$url" >"$WORK/c.env"
|
|
run_lib "" "parse_env_file \"$WORK/c.env\" CONFIG_KEYS CONFIG
|
|
validate_config"
|
|
assert_status "$case_name" 1 "$STATUS"
|
|
assert_contains "$case_name message" "$ERR" "$expected"
|
|
done <<'EOF'
|
|
plain http|http://git.example.test|https URL
|
|
user info|https://user:pw@git.example.test|https URL
|
|
query string|https://git.example.test/?token=abc|https URL
|
|
fragment|https://git.example.test/#x|https URL
|
|
spaces|https://git.example.test/a b|https URL
|
|
EOF
|
|
printf '# nothing\n' >"$WORK/c.env"
|
|
run_lib "" "parse_env_file \"$WORK/c.env\" CONFIG_KEYS CONFIG
|
|
validate_config"
|
|
assert_status "GITEA_URL missing" 1 "$STATUS"
|
|
assert_contains "GITEA_URL missing message" "$ERR" "GITEA_URL is missing"
|
|
}
|
|
|
|
test_config_defaults_and_normalizing() {
|
|
printf 'GITEA_URL=https://git.example.test///\n' >"$WORK/c.env"
|
|
run_lib "" "parse_env_file \"$WORK/c.env\" CONFIG_KEYS CONFIG
|
|
validate_config
|
|
printf '%s\n' \"\${CONFIG[GITEA_URL]}\" \"\${CONFIG[GITEA_API_URL]}\" \"\${CONFIG[GITHUB_API_URL]}\" \"\${CONFIG[GITHUB_WEB_URL]}\""
|
|
assert_status "defaults" 0 "$STATUS"
|
|
assert_eq "trailing slashes removed" $'https://git.example.test\nhttps://git.example.test/api/v1\nhttps://api.github.com\nhttps://github.com' "$OUT"
|
|
}
|
|
|
|
test_credentials_validation() {
|
|
local case_name content expected
|
|
while IFS='|' read -r case_name content expected; do
|
|
printf '%b' "$content" >"$WORK/e.env"
|
|
run_lib "" "parse_env_file \"$WORK/e.env\" CREDENTIAL_KEYS CREDENTIALS
|
|
validate_credentials"
|
|
assert_status "$case_name" 1 "$STATUS"
|
|
assert_contains "$case_name message" "$ERR" "$expected"
|
|
done <<'EOF'
|
|
token too short|GITEA_TOKEN=short\n|not a valid token
|
|
token with a backslash|GITEA_TOKEN=abc\\defgh12345\n|not a valid token
|
|
bad GitHub token|GITEA_TOKEN=abcdefgh12345\nGITHUB_PAT=bad token\n|GITHUB_PAT
|
|
bad GitHub user|GITEA_TOKEN=abcdefgh12345\nGITHUB_USER=-bad-\n|GITHUB_USER
|
|
EOF
|
|
}
|
|
|
|
test_validators() {
|
|
local fn value expected
|
|
while IFS='|' read -r fn value expected; do
|
|
run_lib "" "if $fn '$value'; then echo yes; else echo no; fi"
|
|
assert_eq "$fn '$value'" "$expected" "$OUT"
|
|
done <<'EOF'
|
|
is_valid_repo_name|RepoFoundry|yes
|
|
is_valid_repo_name|my.repo_1-x|yes
|
|
is_valid_repo_name|bad name|no
|
|
is_valid_repo_name|..|no
|
|
is_valid_repo_name|x.git|no
|
|
is_valid_repo_name||no
|
|
is_valid_gitea_owner|Tirsvad|yes
|
|
is_valid_gitea_owner|-lead|no
|
|
is_valid_github_owner|octo-user|yes
|
|
is_valid_github_owner|octo_user|no
|
|
is_valid_github_owner|-octo|no
|
|
is_valid_github_owner|octo-|no
|
|
is_valid_directory|./my-project|yes
|
|
is_valid_directory|-rf|no
|
|
is_valid_directory||no
|
|
is_valid_token|abcdefgh12345|yes
|
|
is_valid_token|abc|no
|
|
is_valid_base_url|https://git.example.test/api/v1|yes
|
|
is_valid_base_url|http://git.example.test|no
|
|
is_valid_request_url|https://api.github.com/user?per_page=5|yes
|
|
is_valid_request_url|https://u:p@api.github.com/user|no
|
|
EOF
|
|
}
|
|
|
|
test_example_files_hold_placeholders_only() {
|
|
local file line value
|
|
for file in "$REPO_ROOT/.env.example" "$REPO_ROOT/config.env.example"; do
|
|
assert_file_exists "example file" "$file"
|
|
done
|
|
while IFS= read -r line; do
|
|
value="${line#*=}"
|
|
check
|
|
if [[ -n $value ]]; then
|
|
fail ".env.example has a value for ${line%%=*}; it must be empty"
|
|
fi
|
|
done < <(grep -E '^[A-Z_]+=' "$REPO_ROOT/.env.example")
|
|
# The example holds a placeholder for the Gitea address, so it must be
|
|
# edited before use: as it is, it is refused with a clear message...
|
|
run_lib "" "parse_env_file \"$REPO_ROOT/config.env.example\" CONFIG_KEYS CONFIG
|
|
validate_config
|
|
echo parsed"
|
|
assert_status "placeholder address is refused" 1 "$STATUS"
|
|
assert_contains "names the key" "$ERR" "GITEA_URL"
|
|
# ...and with a real address in its place the rest of the file is valid.
|
|
sed -e 's|^GITEA_URL=.*|GITEA_URL=https://git.example.test/|' \
|
|
-e 's|^GITEA_API_URL=.*|GITEA_API_URL=https://git.example.test/api/v1|' \
|
|
"$REPO_ROOT/config.env.example" >"$WORK/example.env"
|
|
run_lib "" "parse_env_file \"$WORK/example.env\" CONFIG_KEYS CONFIG
|
|
validate_config
|
|
echo parsed"
|
|
assert_contains "config.env.example is valid once the address is set" "$OUT" "parsed"
|
|
run_lib "" "parse_env_file \"$REPO_ROOT/.env.example\" CREDENTIAL_KEYS CREDENTIALS
|
|
echo parsed"
|
|
assert_contains ".env.example parses" "$OUT" "parsed"
|
|
}
|
|
|
|
test_env_is_ignored_by_git() {
|
|
check
|
|
if ! git -C "$REPO_ROOT" check-ignore -q .env; then
|
|
fail ".env is not ignored by git"
|
|
fi
|
|
check
|
|
if git -C "$REPO_ROOT" check-ignore -q .env.example; then
|
|
fail ".env.example must not be ignored"
|
|
fi
|
|
}
|