Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .gitguardian.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
ignore-paths:
- tests/
ignored-detectors:
- name: "Generic Password"
- name: "Generic High Entropy Secret"
30 changes: 29 additions & 1 deletion src/portkeydrop/dialogs/site_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,15 @@ def _build_ui(self) -> None:
ctrl.SetName(ctrl_name)
setattr(self, attr_name, ctrl)
grid.Add(lbl, 0, wx.ALIGN_CENTER_VERTICAL)
if attr_name == "key_path_text":
if attr_name == "password_text":
row = wx.BoxSizer(wx.HORIZONTAL)
row.Add(ctrl, 1, wx.EXPAND)
self.show_password_btn = wx.Button(self, label="S&how")
self.show_password_btn.SetName("Show password")
self.show_password_btn.Bind(wx.EVT_BUTTON, self._on_toggle_password)
row.Add(self.show_password_btn, 0, wx.LEFT, 4)
grid.Add(row, 1, wx.EXPAND)
elif attr_name == "key_path_text":
row = wx.BoxSizer(wx.HORIZONTAL)
row.Add(ctrl, 1, wx.EXPAND)
browse_btn = wx.Button(self, label="&Browse...")
Expand Down Expand Up @@ -146,6 +154,26 @@ def _on_remove(self, event: wx.CommandEvent) -> None:
self._selected_site = None
self._refresh_site_list()

def _on_toggle_password(self, event: wx.CommandEvent) -> None:
"""Toggle password field between masked and plain text."""
current_value = self.password_text.GetValue()
is_masked = bool(self.password_text.GetWindowStyle() & wx.TE_PASSWORD)
sizer_item = self.password_text.GetContainingSizer()
new_style = 0 if is_masked else wx.TE_PASSWORD
new_ctrl = wx.TextCtrl(self, style=new_style)
new_ctrl.SetName("Password")
new_ctrl.SetValue(current_value)

if sizer_item:
sizer_item.Replace(self.password_text, new_ctrl)

self.password_text.Destroy()
self.password_text = new_ctrl
self.show_password_btn.SetLabel("H&ide" if is_masked else "S&how")
self.show_password_btn.SetName("Hide password" if is_masked else "Show password")
self.Layout()
new_ctrl.SetFocus()

def _on_save(self, event: wx.CommandEvent) -> None:
if not self._selected_site:
return
Expand Down
279 changes: 279 additions & 0 deletions tests/test_site_manager_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
"""Tests for SiteManagerDialog, including show/hide password toggle."""

from __future__ import annotations

import importlib
import sys
import types
from unittest.mock import MagicMock
import pytest


# ---------------------------------------------------------------------------
# Minimal wx stub for SiteManagerDialog
# ---------------------------------------------------------------------------


class _Window:
def __init__(self, parent=None, **_kw):
pass

def Bind(self, *a, **kw):
pass

def SetName(self, *a):
pass

def SetSizer(self, *a):
pass

def Layout(self):
pass

def SetFocus(self):
pass

def Destroy(self):
pass

def GetContainingSizer(self):
return _Sizer()

def GetWindowStyle(self):
return 0

def GetValue(self):
return ""

def SetValue(self, v):
self._value = v

def SetLabel(self, v):
pass


class _Dialog(_Window):
def __init__(self, parent=None, title="", size=None, style=0, **_kw):
pass

def EndModal(self, r):
pass


class _Sizer:
def GetItem(self, ctrl):
return self

def Replace(self, old, new):
pass

def Add(self, *a, **kw):
pass


class _TextCtrl(_Window):
def __init__(self, parent=None, style=0, **_kw):
self._style = style
self._value = ""

def GetWindowStyle(self):
return self._style

def GetValue(self):
return self._value

def SetValue(self, v):
self._value = v

def GetContainingSizer(self):
return _Sizer()

def Destroy(self):
pass


class _Button(_Window):
def __init__(self, parent=None, label="", **_kw):
self._label = label
self._name = ""

def SetLabel(self, v):
self._label = v

def SetName(self, v):
self._name = v


class _Choice(_Window):
def __init__(self, *a, choices=None, **kw):
self._choices = choices or []

def SetSelection(self, i):
pass

def GetStringSelection(self):
return self._choices[0] if self._choices else ""


class _StaticText(_Window):
pass


class _FlexGridSizer(_Sizer):
def __init__(self, *a, **kw):
pass

def AddGrowableCol(self, *a):
pass

def Add(self, *a, **kw):
pass


class _BoxSizer(_Sizer):
def __init__(self, *a, **kw):
pass

def Add(self, *a, **kw):
pass


class _ListBox(_Window):
def __init__(self, *a, **kw):
self._items = []

def Clear(self):
self._items = []

def Append(self, label, data=None):
self._items.append((label, data))

def GetSelection(self):
return -1

def GetClientData(self, i):
return None

def Bind(self, *a, **kw):
pass


def _make_fake_wx():
wx = types.ModuleType("wx")
wx.Dialog = _Dialog
wx.Frame = _Window
wx.Panel = _Window
wx.TextCtrl = _TextCtrl
wx.Button = _Button
wx.Choice = _Choice
wx.StaticText = _StaticText
wx.FlexGridSizer = _FlexGridSizer
wx.BoxSizer = _BoxSizer
wx.ListBox = _ListBox
wx.StaticBox = _Window
wx.StaticBoxSizer = _BoxSizer
wx.FileDialog = _Dialog
wx.OK = 5100
wx.ID_OK = 5100
wx.CANCEL = 5101
wx.ALIGN_CENTER_VERTICAL = 1
wx.ALIGN_RIGHT = 2
wx.LEFT = 4
wx.RIGHT = 8
wx.TOP = 16
wx.BOTTOM = 32
wx.EXPAND = 64
wx.ALL = 128
wx.VERTICAL = 256
wx.HORIZONTAL = 512
wx.TE_PASSWORD = 64
wx.DEFAULT_DIALOG_STYLE = 128
wx.RESIZE_BORDER = 256
wx.FD_OPEN = 512
wx.FD_FILE_MUST_EXIST = 1024
wx.NOT_FOUND = -1
wx.EVT_BUTTON = object()
wx.EVT_LISTBOX = object()
wx.MessageBox = MagicMock(return_value=wx.OK)
return wx


@pytest.fixture
def dialog_module(monkeypatch):
fake_wx = _make_fake_wx()
monkeypatch.setitem(sys.modules, "wx", fake_wx)
sys.modules.pop("portkeydrop.dialogs.site_manager", None)
mod = importlib.import_module("portkeydrop.dialogs.site_manager")
return mod, fake_wx


def _make_dialog(mod, fake_wx, masked=True):
site_manager = MagicMock()
site_manager.sites = []
dlg = object.__new__(mod.SiteManagerDialog)
dlg._site_manager = site_manager
dlg._selected_site = None
dlg._connect_requested = False

# Password ctrl
pw = _TextCtrl(style=fake_wx.TE_PASSWORD if masked else 0)
pw.SetValue("secret123")
dlg.password_text = pw

dlg.show_password_btn = _Button()
dlg.Layout = MagicMock()
return dlg


class TestTogglePassword:
def test_show_reveals_password(self, dialog_module):
mod, fake_wx = dialog_module
dlg = _make_dialog(mod, fake_wx, masked=True)

mod.SiteManagerDialog._on_toggle_password(dlg, MagicMock())

# New ctrl is plain text (no TE_PASSWORD)
assert dlg.password_text.GetWindowStyle() == 0
# Value preserved
assert dlg.password_text.GetValue() == "secret123"
# Button updated
assert dlg.show_password_btn._label == "H&ide"
assert dlg.show_password_btn._name == "Hide password"
dlg.Layout.assert_called_once()

def test_hide_masks_password(self, dialog_module):
mod, fake_wx = dialog_module
dlg = _make_dialog(mod, fake_wx, masked=False)
dlg.password_text.SetValue("secret123")

mod.SiteManagerDialog._on_toggle_password(dlg, MagicMock())

assert dlg.password_text.GetWindowStyle() == fake_wx.TE_PASSWORD
assert dlg.show_password_btn._label == "S&how"
assert dlg.show_password_btn._name == "Show password"

def test_value_preserved_on_toggle(self, dialog_module):
mod, fake_wx = dialog_module
dlg = _make_dialog(mod, fake_wx, masked=True)
dlg.password_text.SetValue("mypassword")

mod.SiteManagerDialog._on_toggle_password(dlg, MagicMock())

assert dlg.password_text.GetValue() == "mypassword"


class TestSiteManagerDialogInit:
def test_init_creates_show_password_btn(self, dialog_module):
"""__init__ should wire up the Show password button."""
from portkeydrop.sites import SiteManager

mod, fake_wx = dialog_module
site_manager = MagicMock(spec=SiteManager)
site_manager.sites = []

dlg = mod.SiteManagerDialog(None, site_manager)

assert hasattr(dlg, "show_password_btn")
assert dlg.show_password_btn._label == "S&how"
assert dlg.show_password_btn._name == "Show password"
assert hasattr(dlg, "password_text")
Loading