mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-21 10:29:06 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95bd8a1a67 |
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
Executable → Regular
@@ -58,7 +58,7 @@ def repos(tmp_path):
|
|||||||
def test_branch_with_upstream_uses_a_plain_pull(repos):
|
def test_branch_with_upstream_uses_a_plain_pull(repos):
|
||||||
args, note, error = resolve_pull_command(str(repos))
|
args, note, error = resolve_pull_command(str(repos))
|
||||||
assert error is None
|
assert error is None
|
||||||
assert args == ['git', 'pull', '--rebase', '--autostash']
|
assert args == ['git', 'pull', '--rebase']
|
||||||
assert note == ''
|
assert note == ''
|
||||||
|
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ def test_branch_without_upstream_falls_back_to_origin_branch(repos):
|
|||||||
|
|
||||||
args, note, error = resolve_pull_command(str(repos))
|
args, note, error = resolve_pull_command(str(repos))
|
||||||
assert error is None
|
assert error is None
|
||||||
assert args == ['git', 'pull', '--rebase', '--autostash', 'origin', 'audit']
|
assert args == ['git', 'pull', '--rebase', 'origin', 'audit']
|
||||||
assert 'audit' in note
|
assert 'audit' in note
|
||||||
|
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ def test_switching_attaches_tracking_so_pull_needs_no_fallback(repos):
|
|||||||
|
|
||||||
args, note, error = resolve_pull_command(str(repos))
|
args, note, error = resolve_pull_command(str(repos))
|
||||||
assert error is None
|
assert error is None
|
||||||
assert args == ['git', 'pull', '--rebase', '--autostash']
|
assert args == ['git', 'pull', '--rebase']
|
||||||
assert note == ''
|
assert note == ''
|
||||||
|
|
||||||
|
|
||||||
@@ -200,37 +200,3 @@ def test_stash_option_lets_the_switch_through_and_keeps_the_work(repos):
|
|||||||
assert _git('branch', '--show-current', cwd=repos).stdout.strip() == 'other'
|
assert _git('branch', '--show-current', cwd=repos).stdout.strip() == 'other'
|
||||||
# The edit is not lost — it is on the stash.
|
# The edit is not lost — it is on the stash.
|
||||||
assert 'switch to other' in _git('stash', 'list', cwd=repos).stdout
|
assert 'switch to other' in _git('stash', 'list', cwd=repos).stdout
|
||||||
|
|
||||||
|
|
||||||
class TestInstallerDoesNotBlockTheUpdateButton:
|
|
||||||
"""first_time_install.sh chmods scripts that git tracked as 644.
|
|
||||||
|
|
||||||
With core.fileMode true -- the default on Linux -- that leaves five
|
|
||||||
permanently modified tracked files on every machine that ran the
|
|
||||||
installer, and `git pull --rebase` refuses to start:
|
|
||||||
|
|
||||||
error: cannot pull with rebase: You have unstaged changes.
|
|
||||||
|
|
||||||
Tracking them as executable makes the installer's chmod a no-op.
|
|
||||||
"""
|
|
||||||
|
|
||||||
CHMODDED = [
|
|
||||||
'first_time_install.sh',
|
|
||||||
'start_display.sh',
|
|
||||||
'stop_display.sh',
|
|
||||||
'scripts/install/install_service.sh',
|
|
||||||
'scripts/install/install_web_service.sh',
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_scripts_the_installer_chmods_are_tracked_executable(self):
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
root = Path(__file__).resolve().parent.parent
|
|
||||||
out = subprocess.run(['git', 'ls-files', '-s', *self.CHMODDED],
|
|
||||||
capture_output=True, text=True, cwd=str(root)).stdout
|
|
||||||
modes = {line.split()[3]: line.split()[0] for line in out.strip().split('\n') if line}
|
|
||||||
non_exec = sorted(f for f, m in modes.items() if m != '100755')
|
|
||||||
assert not non_exec, (
|
|
||||||
f"{non_exec} are chmodded by the installer but tracked non-executable, "
|
|
||||||
"so every install leaves the working tree dirty and the update "
|
|
||||||
"button cannot pull")
|
|
||||||
|
|||||||
@@ -194,15 +194,46 @@ class TestSavePluginConfig:
|
|||||||
|
|
||||||
def test_secret_count_message_counts_top_level_keys(self, env):
|
def test_secret_count_message_counts_top_level_keys(self, env):
|
||||||
# Pinned: the "(N secret field(s))" message counts TOP-LEVEL keys of
|
# Pinned: the "(N secret field(s))" message counts TOP-LEVEL keys of
|
||||||
# the separated secrets dict. Here that is 2: the posted accounts
|
# the separated secrets dict. Here that is 1: the posted accounts
|
||||||
# array (all its item tokens count as ONE key) plus the schema's
|
# array, whose item tokens all count as ONE key.
|
||||||
# api_key default ("") that merge_with_defaults adds before
|
#
|
||||||
# separation.
|
# It was 2 before blank secrets were dropped, the second being the
|
||||||
|
# schema's api_key default (""), which merge_with_defaults adds to
|
||||||
|
# every save. Counting it was the visible edge of a real bug: that
|
||||||
|
# injected blank was merged over the stored api_key, so saving any
|
||||||
|
# unrelated field destroyed the credential. See
|
||||||
|
# test_an_unrelated_edit_does_not_erase_a_stored_secret.
|
||||||
resp = self._save(env, {
|
resp = self._save(env, {
|
||||||
"accounts": [{"name": "a", "token": "t"}],
|
"accounts": [{"name": "a", "token": "t"}],
|
||||||
})
|
})
|
||||||
message = resp.get_json()["message"]
|
message = resp.get_json()["message"]
|
||||||
assert "(2 secret field(s) saved to config_secrets.json)" in message
|
assert "(1 secret field(s) saved to config_secrets.json)" in message
|
||||||
|
|
||||||
|
def test_an_unrelated_edit_does_not_erase_a_stored_secret(self, env):
|
||||||
|
"""Editing one field must not wipe the plugin's API key.
|
||||||
|
|
||||||
|
The config form renders secrets masked, so the browser posts them
|
||||||
|
back blank; merge_with_defaults injects a blank api_key even when
|
||||||
|
the client omits it entirely. Either way a "" reached the secrets
|
||||||
|
file and deep_merge wrote it over the stored credential.
|
||||||
|
"""
|
||||||
|
assert self._save(env, {"api_key": "REAL-KEY-0123456789",
|
||||||
|
"city": "Austin"}).status_code == 200
|
||||||
|
assert _on_disk(env.secrets_file)[PLUGIN_ID]["api_key"] == \
|
||||||
|
"REAL-KEY-0123456789"
|
||||||
|
|
||||||
|
# the user changes the city; the masked api_key rides along blank
|
||||||
|
assert self._save(env, {"api_key": "", "city": "Dallas"}).status_code == 200
|
||||||
|
|
||||||
|
assert _on_disk(env.secrets_file)[PLUGIN_ID]["api_key"] == \
|
||||||
|
"REAL-KEY-0123456789", "an unrelated edit destroyed the API key"
|
||||||
|
assert env.fresh_load()[PLUGIN_ID]["city"] == "Dallas"
|
||||||
|
|
||||||
|
def test_a_secret_can_still_be_changed(self, env):
|
||||||
|
"""Dropping blanks must not stop a real new value from being saved."""
|
||||||
|
self._save(env, {"api_key": "first-key"})
|
||||||
|
self._save(env, {"api_key": "second-key"})
|
||||||
|
assert _on_disk(env.secrets_file)[PLUGIN_ID]["api_key"] == "second-key"
|
||||||
|
|
||||||
def test_resave_replaces_stored_secrets_list_wholesale(self, env):
|
def test_resave_replaces_stored_secrets_list_wholesale(self, env):
|
||||||
# Characterized: api_v3's deep_merge intentionally replaces lists,
|
# Characterized: api_v3's deep_merge intentionally replaces lists,
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# Import new infrastructure
|
# Import new infrastructure
|
||||||
from src.web_interface.api_helpers import success_response, error_response, validate_request_json
|
from src.web_interface.api_helpers import success_response, error_response, validate_request_json
|
||||||
from src.web_interface.errors import ErrorCode
|
from src.web_interface.errors import ErrorCode
|
||||||
from src.web_interface.secret_helpers import find_secret_fields, separate_secrets
|
from src.web_interface.secret_helpers import (find_secret_fields, remove_empty_secrets,
|
||||||
|
separate_secrets)
|
||||||
from src.web_interface.error_handler import describe_exception, redact_text
|
from src.web_interface.error_handler import describe_exception, redact_text
|
||||||
from src.plugin_system.operation_types import OperationType
|
from src.plugin_system.operation_types import OperationType
|
||||||
from src.web_interface.validators import (
|
from src.web_interface.validators import (
|
||||||
@@ -1216,6 +1217,11 @@ def save_main_config():
|
|||||||
|
|
||||||
# Separate secrets from regular config (same logic as save_plugin_config)
|
# Separate secrets from regular config (same logic as save_plugin_config)
|
||||||
regular_config, secrets_config = separate_secrets(plugin_config, secret_fields)
|
regular_config, secrets_config = separate_secrets(plugin_config, secret_fields)
|
||||||
|
# The config form renders secrets masked, so every save posts
|
||||||
|
# them back blank. Without this the blank is merged over the
|
||||||
|
# stored value and the credential is destroyed by the act of
|
||||||
|
# changing an unrelated setting. A blank means "unchanged".
|
||||||
|
secrets_config = remove_empty_secrets(secrets_config)
|
||||||
|
|
||||||
# PRE-PROCESSING: Preserve 'enabled' state if not in regular_config
|
# PRE-PROCESSING: Preserve 'enabled' state if not in regular_config
|
||||||
# This prevents overwriting the enabled state when saving config from a form that doesn't include the toggle
|
# This prevents overwriting the enabled state when saving config from a form that doesn't include the toggle
|
||||||
@@ -1657,22 +1663,13 @@ def resolve_pull_command(project_dir):
|
|||||||
backup, or following an install guide that names one. The update button
|
backup, or following an install guide that names one. The update button
|
||||||
then reports a failure the user cannot act on.
|
then reports a failure the user cannot act on.
|
||||||
|
|
||||||
``--autostash`` is passed for the same reason. Rebase refuses to start
|
|
||||||
when any tracked file is modified, and on these installs something always
|
|
||||||
is: first_time_install.sh chmods five scripts that git tracked as 644, so
|
|
||||||
every machine that ran the installer carries five permanent mode changes
|
|
||||||
and the update button reports "cannot pull with rebase: You have unstaged
|
|
||||||
changes". Those modes are corrected in this commit, but a user cannot pull
|
|
||||||
the correction while the pull is what is blocked, and any other local edit
|
|
||||||
would reproduce it anyway. Autostash reapplies the changes afterwards.
|
|
||||||
|
|
||||||
Returns ``(args, note, error)``. When ``origin/<branch>`` exists the pull
|
Returns ``(args, note, error)``. When ``origin/<branch>`` exists the pull
|
||||||
is made explicit against it, so the update proceeds and the branch is
|
is made explicit against it, so the update proceeds and the branch is
|
||||||
given tracking information afterwards.
|
given tracking information afterwards.
|
||||||
"""
|
"""
|
||||||
upstream = _git_upstream(project_dir)
|
upstream = _git_upstream(project_dir)
|
||||||
if upstream:
|
if upstream:
|
||||||
return ['git', 'pull', '--rebase', '--autostash'], '', None
|
return ['git', 'pull', '--rebase'], '', None
|
||||||
|
|
||||||
branch = _git_current_branch(project_dir)
|
branch = _git_current_branch(project_dir)
|
||||||
if not branch:
|
if not branch:
|
||||||
@@ -1682,7 +1679,7 @@ def resolve_pull_command(project_dir):
|
|||||||
)
|
)
|
||||||
if _git_remote_branch_exists(project_dir, branch):
|
if _git_remote_branch_exists(project_dir, branch):
|
||||||
return (
|
return (
|
||||||
['git', 'pull', '--rebase', '--autostash', 'origin', branch],
|
['git', 'pull', '--rebase', 'origin', branch],
|
||||||
f"Branch '{branch}' had no upstream; pulled from origin/{branch} and set it as the upstream.",
|
f"Branch '{branch}' had no upstream; pulled from origin/{branch} and set it as the upstream.",
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -5608,6 +5605,11 @@ def save_plugin_config():
|
|||||||
# Separate secrets from regular config (handles nested configs and
|
# Separate secrets from regular config (handles nested configs and
|
||||||
# array-item secrets — see src/web_interface/secret_helpers.py)
|
# array-item secrets — see src/web_interface/secret_helpers.py)
|
||||||
regular_config, secrets_config = separate_secrets(plugin_config, secret_fields)
|
regular_config, secrets_config = separate_secrets(plugin_config, secret_fields)
|
||||||
|
# The config form renders secrets masked, so every save posts
|
||||||
|
# them back blank. Without this the blank is merged over the
|
||||||
|
# stored value and the credential is destroyed by the act of
|
||||||
|
# changing an unrelated setting. A blank means "unchanged".
|
||||||
|
secrets_config = remove_empty_secrets(secrets_config)
|
||||||
|
|
||||||
# Get current configs
|
# Get current configs
|
||||||
current_config = api_v3.config_manager.load_config()
|
current_config = api_v3.config_manager.load_config()
|
||||||
|
|||||||
Reference in New Issue
Block a user