mirror of
https://github.com/pre-commit/pre-commit-hooks.git
synced 2026-03-30 02:16:52 +00:00
550 lines
19 KiB
Python
550 lines
19 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import shutil
|
|
import threading
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from pre_commit_hooks.catch_dotenv import DEFAULT_ENV_FILE
|
|
from pre_commit_hooks.catch_dotenv import DEFAULT_EXAMPLE_ENV_FILE
|
|
from pre_commit_hooks.catch_dotenv import DEFAULT_GITIGNORE_FILE
|
|
from pre_commit_hooks.catch_dotenv import ensure_env_in_gitignore
|
|
from pre_commit_hooks.catch_dotenv import GITIGNORE_BANNER
|
|
from pre_commit_hooks.catch_dotenv import main
|
|
|
|
# Tests cover hook behavior: detection gating, .gitignore normalization,
|
|
# example file generation parsing edge cases, idempotency, and preservation of
|
|
# existing content. Each test isolates a single behavioral contract.
|
|
|
|
|
|
@pytest.fixture()
|
|
def env_file(tmp_path: Path) -> Path:
|
|
"""Copy shared resource .env into tmp workspace as the canonical .env.
|
|
|
|
All tests rely on this baseline content (optionally appending extra lines
|
|
for edge cases) to ensure consistent parsing behavior.
|
|
"""
|
|
# Find repository root by looking for .git directory
|
|
test_file_path = Path(__file__).resolve()
|
|
repo_root = test_file_path
|
|
while repo_root.parent != repo_root: # Stop at filesystem root
|
|
if (repo_root / '.git').exists():
|
|
break
|
|
repo_root = repo_root.parent
|
|
else:
|
|
raise RuntimeError('Could not find repository root (.git directory)')
|
|
|
|
# Source file stored as test.env in repo (cannot commit a real .env in CI)
|
|
resource_env = repo_root / 'testing' / 'resources' / 'test.env'
|
|
dest = tmp_path / DEFAULT_ENV_FILE
|
|
shutil.copyfile(resource_env, dest)
|
|
return dest
|
|
|
|
|
|
def run_hook(
|
|
tmp_path: Path, staged: list[str], create_example: bool = False,
|
|
) -> int:
|
|
cwd = os.getcwd()
|
|
os.chdir(tmp_path)
|
|
try:
|
|
args = staged[:]
|
|
if create_example:
|
|
args.append('--create-example')
|
|
return main(args)
|
|
finally:
|
|
os.chdir(cwd)
|
|
|
|
|
|
def test_no_env_file(tmp_path: Path, env_file: Path) -> None:
|
|
"""Hook should no-op (return 0) if .env not staged even if it exists."""
|
|
(tmp_path / 'foo.txt').write_text('x')
|
|
assert run_hook(tmp_path, ['foo.txt']) == 0
|
|
|
|
|
|
def test_blocks_env_and_updates_gitignore(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Staging .env triggers block (exit 1) and appends banner + env entry."""
|
|
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
|
assert ret == 1
|
|
gi = (tmp_path / DEFAULT_GITIGNORE_FILE).read_text().splitlines()
|
|
assert gi[-2] == GITIGNORE_BANNER
|
|
assert gi[-1] == DEFAULT_ENV_FILE
|
|
|
|
|
|
def test_env_present_but_not_staged(tmp_path: Path, env_file: Path) -> None:
|
|
"""Existing .env on disk but not staged should not block commit."""
|
|
assert run_hook(tmp_path, ['unrelated.txt']) == 0
|
|
|
|
|
|
def test_idempotent_gitignore(tmp_path: Path, env_file: Path) -> None:
|
|
"""Re-running after initial normalization leaves .gitignore unchanged."""
|
|
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
|
g.write_text(f"{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n")
|
|
first = run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
|
assert first == 1
|
|
content1 = g.read_text()
|
|
second = run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
|
assert second == 1
|
|
assert g.read_text() == content1 # unchanged
|
|
|
|
|
|
def test_gitignore_with_existing_content_preserved(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Existing entries stay intact; banner/env appended at end cleanly."""
|
|
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
|
g.write_text(
|
|
'node_modules/\n# comment line\n',
|
|
) # existing content with trailing newline
|
|
run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
|
lines = g.read_text().splitlines()
|
|
# original content should still be at top
|
|
assert lines[0] == 'node_modules/'
|
|
assert '# comment line' in lines[1]
|
|
# Last two lines should be banner + env file
|
|
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
|
|
|
|
|
def test_gitignore_duplicates_are_collapsed(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Multiple prior duplicate banner/env lines collapse to single pair."""
|
|
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
|
g.write_text(
|
|
f"other\n{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n"
|
|
f"{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n\n\n",
|
|
)
|
|
run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
|
lines = g.read_text().splitlines()
|
|
assert lines.count(GITIGNORE_BANNER) == 1
|
|
assert lines.count(DEFAULT_ENV_FILE) == 1
|
|
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
|
|
|
|
|
def test_create_example(tmp_path: Path, env_file: Path) -> None:
|
|
"""Example file includes discovered keys; values stripped to KEY=."""
|
|
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
|
assert ret == 1
|
|
example = (tmp_path / DEFAULT_EXAMPLE_ENV_FILE).read_text().splitlines()
|
|
key_lines = [ln for ln in example if ln and not ln.startswith('#')]
|
|
# All key lines should be KEY=
|
|
assert all(re.match(r'^[A-Za-z_][A-Za-z0-9_]*=$', ln) for ln in key_lines)
|
|
# Spot check a few known keys from resource file
|
|
for k in [
|
|
'OPENAI_API_KEY=',
|
|
'ACCESS_TOKEN_SECRET=',
|
|
'SUPABASE_SERVICE_KEY=',
|
|
]:
|
|
assert k in key_lines
|
|
|
|
|
|
def test_create_example_duplicate_key_variant_ignored(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Appending whitespace duplicate of existing key should not duplicate
|
|
in example.
|
|
"""
|
|
# Create a copy of the env_file to avoid contaminating the fixture
|
|
modified_env = tmp_path / 'modified.env'
|
|
shutil.copyfile(env_file, modified_env)
|
|
with open(modified_env, 'a', encoding='utf-8') as f:
|
|
f.write('BACKEND_CONTAINER_PORT =999 # duplicate variant\n')
|
|
|
|
# Override the env file path for this test
|
|
original_env = tmp_path / DEFAULT_ENV_FILE
|
|
shutil.copyfile(modified_env, original_env)
|
|
run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
|
lines = (tmp_path / DEFAULT_EXAMPLE_ENV_FILE).read_text().splitlines()
|
|
key_lines = [ln for ln in lines if ln and not ln.startswith('#')]
|
|
assert key_lines.count('BACKEND_CONTAINER_PORT=') == 1
|
|
|
|
|
|
def test_gitignore_without_trailing_newline(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Normalization works when original .gitignore lacks trailing newline."""
|
|
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
|
g.write_text('existing_line') # no newline at EOF
|
|
run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
|
lines = g.read_text().splitlines()
|
|
assert lines[0] == 'existing_line'
|
|
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
|
|
|
|
|
def test_ensure_env_in_gitignore_normalizes(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Direct API call collapses duplicates and produces canonical tail
|
|
layout.
|
|
"""
|
|
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
|
g.write_text(
|
|
f"{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n"
|
|
f"{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n\n",
|
|
)
|
|
modified = ensure_env_in_gitignore(
|
|
DEFAULT_ENV_FILE, str(g), GITIGNORE_BANNER,
|
|
)
|
|
assert modified is True
|
|
lines = g.read_text().splitlines()
|
|
# final two lines should be banner + env
|
|
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
|
# only one occurrence each
|
|
assert lines.count(GITIGNORE_BANNER) == 1
|
|
assert lines.count(DEFAULT_ENV_FILE) == 1
|
|
|
|
|
|
def test_source_env_file_not_modified(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Hook must not alter original .env (comments and formatting stay)."""
|
|
original = env_file.read_text()
|
|
run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
|
assert env_file.read_text() == original
|
|
|
|
|
|
def test_large_resource_env_parsing(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Generate example from resource env; assert broad key coverage &
|
|
format.
|
|
"""
|
|
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
|
assert ret == 1
|
|
example_lines = (
|
|
(tmp_path / DEFAULT_EXAMPLE_ENV_FILE).read_text().splitlines()
|
|
)
|
|
key_lines = [ln for ln in example_lines if ln and not ln.startswith('#')]
|
|
assert len(key_lines) > 20
|
|
assert all(re.match(r'^[A-Za-z_][A-Za-z0-9_]*=$', ln) for ln in key_lines)
|
|
for k in [
|
|
'BACKEND_CONTAINER_PORT=',
|
|
'SUPABASE_SERVICE_KEY=',
|
|
'ACCESS_TOKEN_SECRET=',
|
|
]:
|
|
assert k in key_lines
|
|
|
|
|
|
def test_failure_message_content(
|
|
tmp_path: Path,
|
|
env_file: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
"""Hook stdout message should contain key phrases when blocking commit."""
|
|
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
|
assert ret == 1
|
|
out = capsys.readouterr().out.strip()
|
|
assert 'Blocked committing' in out
|
|
assert DEFAULT_GITIGNORE_FILE in out # updated path appears
|
|
assert 'Generated .env.example.' in out
|
|
assert 'Remove .env' in out
|
|
|
|
|
|
def test_create_example_when_env_missing(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""--create-example with no .env staged or present should no-op (exit 0).
|
|
|
|
Uses env_file fixture (requirement: all tests use fixture) then removes the
|
|
copied .env to simulate absence.
|
|
"""
|
|
env_file.unlink()
|
|
ret = run_hook(tmp_path, ['unrelated.txt'], create_example=True)
|
|
assert ret == 0
|
|
assert not (tmp_path / DEFAULT_EXAMPLE_ENV_FILE).exists()
|
|
|
|
|
|
def test_gitignore_is_directory_error(
|
|
tmp_path: Path,
|
|
env_file: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
"""If .gitignore path is a directory, hook should print error and still
|
|
block.
|
|
"""
|
|
gitignore_dir = tmp_path / DEFAULT_GITIGNORE_FILE
|
|
gitignore_dir.mkdir()
|
|
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
|
assert ret == 1 # still blocks commit
|
|
captured = capsys.readouterr()
|
|
assert 'ERROR:' in captured.err # error now printed to stderr
|
|
|
|
|
|
def test_env_example_overwrites_existing(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Pre-existing example file with junk should be overwritten with header
|
|
& keys.
|
|
"""
|
|
example = tmp_path / DEFAULT_EXAMPLE_ENV_FILE
|
|
example.write_text('junk=1\nSHOULD_NOT_REMAIN=2\n')
|
|
run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
|
content = example.read_text().splitlines()
|
|
assert content[0].startswith('# Generated by catch-dotenv')
|
|
assert any(ln.startswith('BACKEND_CONTAINER_PORT=') for ln in content)
|
|
assert 'junk=1' not in content
|
|
assert 'SHOULD_NOT_REMAIN=2' not in content
|
|
|
|
|
|
def test_large_gitignore_normalization_performance(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Very large .gitignore remains normalized quickly (functional smoke)."""
|
|
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
|
# Generate many lines with scattered duplicates of banner/env
|
|
lines = (
|
|
[f"file_{i}" for i in range(3000)] +
|
|
[GITIGNORE_BANNER, DEFAULT_ENV_FILE] * 3
|
|
)
|
|
g.write_text('\n'.join(lines) + '\n')
|
|
start = time.time()
|
|
run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
|
elapsed = time.time() - start
|
|
result_lines = g.read_text().splitlines()
|
|
assert result_lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
|
assert result_lines.count(GITIGNORE_BANNER) == 1
|
|
assert result_lines.count(DEFAULT_ENV_FILE) == 1
|
|
# Soft performance expectation: should finish fast
|
|
# (< 0.5s on typical dev machine)
|
|
assert elapsed < 0.5
|
|
|
|
|
|
def test_concurrent_gitignore_writes(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Concurrent ensure_env_in_gitignore calls result in canonical final
|
|
state.
|
|
"""
|
|
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
|
# Seed with messy duplicates
|
|
g.write_text(f"other\n{GITIGNORE_BANNER}\n{DEFAULT_ENV_FILE}\n\n")
|
|
|
|
def worker():
|
|
ensure_env_in_gitignore(DEFAULT_ENV_FILE, str(g), GITIGNORE_BANNER)
|
|
|
|
threads = [threading.Thread(target=worker) for _ in range(5)]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
lines = g.read_text().splitlines()
|
|
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
|
assert lines.count(GITIGNORE_BANNER) == 1
|
|
assert lines.count(DEFAULT_ENV_FILE) == 1
|
|
|
|
|
|
def test_mixed_staged_files(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Staging .env with other files still blocks and only normalizes
|
|
gitignore once.
|
|
"""
|
|
other = tmp_path / 'README.md'
|
|
other.write_text('hi')
|
|
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE, 'README.md'])
|
|
assert ret == 1
|
|
lines = (tmp_path / DEFAULT_GITIGNORE_FILE).read_text().splitlines()
|
|
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
|
|
|
|
|
def test_already_ignored_env_with_variations(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Pre-existing ignore lines with spacing normalize to single
|
|
canonical pair.
|
|
"""
|
|
g = tmp_path / DEFAULT_GITIGNORE_FILE
|
|
g.write_text(
|
|
f" {DEFAULT_ENV_FILE} \n{GITIGNORE_BANNER}\n"
|
|
f" {DEFAULT_ENV_FILE}\n",
|
|
)
|
|
run_hook(tmp_path, [DEFAULT_ENV_FILE])
|
|
lines = g.read_text().splitlines()
|
|
assert lines[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
|
assert lines.count(DEFAULT_ENV_FILE) == 1
|
|
|
|
|
|
def test_subdirectory_invocation(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Running from a subdirectory now writes .gitignore relative to CWD
|
|
(simplified behavior).
|
|
"""
|
|
sub = tmp_path / 'subdir'
|
|
sub.mkdir()
|
|
# simulate repository root marker
|
|
(tmp_path / '.git').mkdir()
|
|
# simulate running hook from subdir while staged path relative to repo root
|
|
cwd = os.getcwd()
|
|
os.chdir(sub)
|
|
try:
|
|
ret = main(
|
|
[str(Path('..') / DEFAULT_ENV_FILE)],
|
|
) # staged path relative to subdir
|
|
gi = (sub / DEFAULT_GITIGNORE_FILE).read_text().splitlines()
|
|
finally:
|
|
os.chdir(cwd)
|
|
assert ret == 1
|
|
assert gi[-2:] == [GITIGNORE_BANNER, DEFAULT_ENV_FILE]
|
|
|
|
|
|
def test_atomic_write_failure_gitignore(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
env_file: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
"""Simulate os.replace failure during gitignore write to exercise error
|
|
path.
|
|
"""
|
|
def boom(*_a: object, **_k: object) -> None:
|
|
raise OSError('replace-fail')
|
|
monkeypatch.setattr('pre_commit_hooks.catch_dotenv.os.replace', boom)
|
|
modified = ensure_env_in_gitignore(
|
|
DEFAULT_ENV_FILE,
|
|
str(tmp_path / DEFAULT_GITIGNORE_FILE),
|
|
GITIGNORE_BANNER,
|
|
)
|
|
assert modified is False
|
|
captured = capsys.readouterr()
|
|
assert 'ERROR: unable to write' in captured.err
|
|
|
|
|
|
def test_atomic_write_failure_example(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
env_file: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
"""Simulate os.replace failure when writing example env file."""
|
|
def boom(*_a: object, **_k: object) -> None:
|
|
raise OSError('replace-fail')
|
|
monkeypatch.setattr('pre_commit_hooks.catch_dotenv.os.replace', boom)
|
|
ok = False
|
|
# create_example_env requires source .env to exist; env_file fixture
|
|
# provides it in tmp_path root
|
|
cwd = os.getcwd()
|
|
os.chdir(tmp_path)
|
|
try:
|
|
ok = main([DEFAULT_ENV_FILE, '--create-example']) == 1
|
|
finally:
|
|
os.chdir(cwd)
|
|
# hook still blocks; but example creation failed -> message should
|
|
# not claim Example file generated
|
|
assert ok is True
|
|
captured = capsys.readouterr()
|
|
out = captured.out
|
|
err = captured.err
|
|
assert 'Example file generated' not in out
|
|
assert 'ERROR: unable to write' in err
|
|
|
|
|
|
def test_atomic_write_cleanup_failure(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
env_file: Path,
|
|
) -> None:
|
|
"""Test rare case where os.remove fails during cleanup after os.replace
|
|
failure.
|
|
"""
|
|
def failing_remove(_path: str) -> None:
|
|
# Simulate os.remove failure during cleanup
|
|
raise OSError('remove-fail')
|
|
|
|
def failing_replace(*_a: object, **_k: object) -> None:
|
|
# First fail os.replace to trigger cleanup path
|
|
raise OSError('replace-fail')
|
|
|
|
monkeypatch.setattr(
|
|
'pre_commit_hooks.catch_dotenv.os.replace', failing_replace,
|
|
)
|
|
monkeypatch.setattr(
|
|
'pre_commit_hooks.catch_dotenv.os.remove', failing_remove,
|
|
)
|
|
|
|
# This should not raise an exception even if both replace and remove fail
|
|
modified = ensure_env_in_gitignore(
|
|
DEFAULT_ENV_FILE,
|
|
str(tmp_path / DEFAULT_GITIGNORE_FILE),
|
|
GITIGNORE_BANNER,
|
|
)
|
|
assert modified is False
|
|
|
|
|
|
def test_create_example_read_error(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
env_file: Path,
|
|
capsys: pytest.CaptureFixture[str],
|
|
) -> None:
|
|
"""Test OSError when reading source env file for create_example."""
|
|
def failing_open(*_args: object, **_kwargs: object) -> None:
|
|
raise OSError('Permission denied')
|
|
|
|
# Mock open to fail when trying to read the env file
|
|
monkeypatch.setattr('builtins.open', failing_open)
|
|
|
|
from pre_commit_hooks.catch_dotenv import create_example_env
|
|
|
|
result = create_example_env(str(env_file), str(tmp_path / 'test.example'))
|
|
assert result is False
|
|
|
|
captured = capsys.readouterr()
|
|
assert 'ERROR: unable to read' in captured.err
|
|
|
|
|
|
def test_malformed_env_lines_ignored(tmp_path: Path, env_file: Path) -> None:
|
|
"""Test that malformed env lines that don't match regex are ignored."""
|
|
# Create env file with malformed lines
|
|
malformed_env = tmp_path / 'malformed.env'
|
|
malformed_content = [
|
|
'VALID_KEY=value',
|
|
'invalid-line-no-equals',
|
|
'# comment line',
|
|
'', # empty line
|
|
'=INVALID_EQUALS_FIRST',
|
|
'ANOTHER_VALID=value2',
|
|
'spaces in key=invalid',
|
|
'123_INVALID_START=value', # starts with number
|
|
]
|
|
malformed_env.write_text('\n'.join(malformed_content))
|
|
|
|
# Copy to .env location
|
|
shutil.copyfile(malformed_env, tmp_path / DEFAULT_ENV_FILE)
|
|
|
|
# Run create-example - should only extract valid keys
|
|
run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
|
|
|
example_lines = (
|
|
(tmp_path / DEFAULT_EXAMPLE_ENV_FILE).read_text().splitlines()
|
|
)
|
|
key_lines = [ln for ln in example_lines if ln and not ln.startswith('#')]
|
|
|
|
# Should only have the valid keys
|
|
assert 'VALID_KEY=' in key_lines
|
|
assert 'ANOTHER_VALID=' in key_lines
|
|
assert len([k for k in key_lines if '=' in k]) == 2 # Only 2 valid keys
|
|
|
|
|
|
def test_create_example_when_source_missing(
|
|
tmp_path: Path, env_file: Path,
|
|
) -> None:
|
|
"""Test --create-example when source .env doesn't exist but .env is
|
|
staged.
|
|
"""
|
|
# Remove the source .env file but keep it in the staged files list
|
|
env_file.unlink() # Remove the .env file
|
|
|
|
# Stage .env even though it doesn't exist on disk
|
|
ret = run_hook(tmp_path, [DEFAULT_ENV_FILE], create_example=True)
|
|
|
|
# Hook should still block commit
|
|
assert ret == 1
|
|
|
|
# But no example file should be created since source doesn't exist
|
|
assert not (tmp_path / DEFAULT_EXAMPLE_ENV_FILE).exists()
|