@@ -442,3 +442,109 @@ def boom(*_a: object, **_k: object) -> None:
442442 err = captured .err
443443 assert 'Example file generated' not in out
444444 assert 'ERROR: unable to write' in err
445+
446+
447+ def test_atomic_write_cleanup_failure (
448+ monkeypatch : pytest .MonkeyPatch ,
449+ tmp_path : Path ,
450+ env_file : Path ,
451+ ) -> None :
452+ """Test rare case where os.remove fails during cleanup after os.replace
453+ failure.
454+ """
455+ def failing_remove (_path : str ) -> None :
456+ # Simulate os.remove failure during cleanup
457+ raise OSError ('remove-fail' )
458+
459+ def failing_replace (* _a : object , ** _k : object ) -> None :
460+ # First fail os.replace to trigger cleanup path
461+ raise OSError ('replace-fail' )
462+
463+ monkeypatch .setattr (
464+ 'pre_commit_hooks.catch_dotenv.os.replace' , failing_replace ,
465+ )
466+ monkeypatch .setattr (
467+ 'pre_commit_hooks.catch_dotenv.os.remove' , failing_remove ,
468+ )
469+
470+ # This should not raise an exception even if both replace and remove fail
471+ modified = ensure_env_in_gitignore (
472+ DEFAULT_ENV_FILE ,
473+ str (tmp_path / DEFAULT_GITIGNORE_FILE ),
474+ GITIGNORE_BANNER ,
475+ )
476+ assert modified is False
477+
478+
479+ def test_create_example_read_error (
480+ monkeypatch : pytest .MonkeyPatch ,
481+ tmp_path : Path ,
482+ env_file : Path ,
483+ capsys : pytest .CaptureFixture [str ],
484+ ) -> None :
485+ """Test OSError when reading source env file for create_example."""
486+ def failing_open (* _args : object , ** _kwargs : object ) -> None :
487+ raise OSError ('Permission denied' )
488+
489+ # Mock open to fail when trying to read the env file
490+ monkeypatch .setattr ('builtins.open' , failing_open )
491+
492+ from pre_commit_hooks .catch_dotenv import create_example_env
493+
494+ result = create_example_env (str (env_file ), str (tmp_path / 'test.example' ))
495+ assert result is False
496+
497+ captured = capsys .readouterr ()
498+ assert 'ERROR: unable to read' in captured .err
499+
500+
501+ def test_malformed_env_lines_ignored (tmp_path : Path , env_file : Path ) -> None :
502+ """Test that malformed env lines that don't match regex are ignored."""
503+ # Create env file with malformed lines
504+ malformed_env = tmp_path / 'malformed.env'
505+ malformed_content = [
506+ 'VALID_KEY=value' ,
507+ 'invalid-line-no-equals' ,
508+ '# comment line' ,
509+ '' , # empty line
510+ '=INVALID_EQUALS_FIRST' ,
511+ 'ANOTHER_VALID=value2' ,
512+ 'spaces in key=invalid' ,
513+ '123_INVALID_START=value' , # starts with number
514+ ]
515+ malformed_env .write_text ('\n ' .join (malformed_content ))
516+
517+ # Copy to .env location
518+ shutil .copyfile (malformed_env , tmp_path / DEFAULT_ENV_FILE )
519+
520+ # Run create-example - should only extract valid keys
521+ run_hook (tmp_path , [DEFAULT_ENV_FILE ], create_example = True )
522+
523+ example_lines = (
524+ (tmp_path / DEFAULT_EXAMPLE_ENV_FILE ).read_text ().splitlines ()
525+ )
526+ key_lines = [ln for ln in example_lines if ln and not ln .startswith ('#' )]
527+
528+ # Should only have the valid keys
529+ assert 'VALID_KEY=' in key_lines
530+ assert 'ANOTHER_VALID=' in key_lines
531+ assert len ([k for k in key_lines if '=' in k ]) == 2 # Only 2 valid keys
532+
533+
534+ def test_create_example_when_source_missing (
535+ tmp_path : Path , env_file : Path ,
536+ ) -> None :
537+ """Test --create-example when source .env doesn't exist but .env is
538+ staged.
539+ """
540+ # Remove the source .env file but keep it in the staged files list
541+ env_file .unlink () # Remove the .env file
542+
543+ # Stage .env even though it doesn't exist on disk
544+ ret = run_hook (tmp_path , [DEFAULT_ENV_FILE ], create_example = True )
545+
546+ # Hook should still block commit
547+ assert ret == 1
548+
549+ # But no example file should be created since source doesn't exist
550+ assert not (tmp_path / DEFAULT_EXAMPLE_ENV_FILE ).exists ()
0 commit comments