Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions jrnl/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,19 @@ def install() -> dict:

# Where to create the journal?
default_journal_path = get_default_journal_path()
user_given_path = print_msg(
Message(
MsgText.InstallJournalPathQuestion,
MsgStyle.PROMPT,
params={
"default_journal_path": default_journal_path,
},
),
get_input=True,
)
try:
user_given_path = print_msg(
Message(
MsgText.InstallJournalPathQuestion,
MsgStyle.PROMPT,
params={
"default_journal_path": default_journal_path,
},
),
get_input=True,
)
except EOFError:
user_given_path = ""
journal_path = absolute_path(user_given_path or default_journal_path)
default_config = get_default_config()
default_config["journals"][DEFAULT_JOURNAL_KEY]["journal"] = journal_path
Expand All @@ -142,14 +145,25 @@ def install() -> dict:
with contextlib.suppress(OSError):
os.makedirs(path)

# Encrypt it?
encrypt = yesno(Message(MsgText.EncryptJournalQuestion), default=False)
# Encrypt it? Default is No; EOF is treated as the default.
try:
encrypt = yesno(Message(MsgText.EncryptJournalQuestion), default=False)
except EOFError:
encrypt = False
if encrypt:
default_config["encrypt"] = True
print_msg(Message(MsgText.JournalEncrypted, MsgStyle.NORMAL))

# Use colors?
use_colors = yesno(Message(MsgText.UseColorsQuestion), default=True)
# Use colors? Default is Yes; EOF falls back to the default only when the
# journal is NOT encrypted. If encryption was chosen and stdin is already
# exhausted, re-raise so save_config() is never called: an encrypt:true config
# written without a password set leaves the journal permanently broken.
try:
use_colors = yesno(Message(MsgText.UseColorsQuestion), default=True)
except EOFError:
if encrypt:
raise
use_colors = True
if use_colors:
default_config["colors"] = get_default_colors()

Expand Down
88 changes: 88 additions & 0 deletions tests/unit/test_install.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Copyright © 2012-2023 jrnl contributors
# License: https://www.gnu.org/licenses/gpl-3.0.html

import os
import sys
from unittest import mock
from unittest.mock import MagicMock
from unittest.mock import patch

import pytest

Expand All @@ -15,3 +18,88 @@ def test_initialize_autocomplete_runs_without_readline():

with mock.patch.dict(sys.modules, {"readline": None}):
install._initialize_autocomplete() # should not throw exception


def test_install_uses_defaults_when_stdin_is_eof(tmp_path):
"""install() must not raise EOFError when stdin is at EOF (e.g. jrnl < /dev/null).
Instead every interactive prompt should fall back to its built-in default."""
from jrnl import install

config_path = str(tmp_path / "jrnl.yaml")
journal_path = str(tmp_path / "journal.txt")

mock_console = MagicMock()
mock_console.input.side_effect = EOFError

with (
patch("jrnl.output._get_console", return_value=mock_console),
patch("jrnl.install.get_config_path", return_value=config_path),
patch("jrnl.config.get_config_path", return_value=config_path),
patch("jrnl.install.get_default_journal_path", return_value=journal_path),
patch("jrnl.config.get_default_journal_path", return_value=journal_path),
):
config = install.install()

assert config["journals"]["default"]["journal"] == journal_path
assert config["encrypt"] is False
assert config["colors"] == {
"body": "none",
"date": "black",
"tags": "yellow",
"title": "cyan",
}, "colors must default to ON when all prompts receive EOF"


def test_install_eof_after_yes_to_encrypt_aborts_without_writing_config(tmp_path):
"""Partial-stdin scenario: path=blank, encrypt=yes, then EOF at colors prompt.

install() must abort (re-raise EOFError) and must NOT write a config file.
Saving an encrypt:true config before a password is set leaves the journal
permanently broken (ValueError: Invalid IV size for CBC on every subsequent run).

Against c7a3755 this test fails because that commit swallowed EOF at the colors
prompt unconditionally, causing install() to save the broken config and return
normally instead of raising.
"""
from jrnl import install

config_path = str(tmp_path / "jrnl.yaml")
journal_path = str(tmp_path / "journal.txt")

mock_console = MagicMock()
# journal path: blank (use default), encrypt: yes, colors: EOF
mock_console.input.side_effect = ["", "y", EOFError()]

with (
patch("jrnl.output._get_console", return_value=mock_console),
patch("jrnl.install.get_config_path", return_value=config_path),
patch("jrnl.config.get_config_path", return_value=config_path),
patch("jrnl.install.get_default_journal_path", return_value=journal_path),
patch("jrnl.config.get_default_journal_path", return_value=journal_path),
pytest.raises(EOFError),
):
install.install()

assert not os.path.exists(
config_path
), "Config must not be written when install aborts on EOF after encrypt=yes"


def test_password_prompt_propagates_eof():
"""Password prompts must NOT swallow EOFError.

With the broad print_msgs() catch (commit 55abe86), prompt_password() would
silently return "" on EOF instead of raising. The narrower fix (catching EOF
only inside install()) must let EOFError propagate from password helpers so
callers can handle it or let it terminate the process.
"""
from jrnl.prompt import prompt_password

mock_console = MagicMock()
mock_console.input.side_effect = EOFError

with (
patch("jrnl.output._get_console", return_value=mock_console),
pytest.raises(EOFError),
):
prompt_password()