Fix errors w/ Windows & w/ GitHub runner. Configs.
authorShane Jaroch <chown_tee@proton.me>
Fri, 26 Dec 2025 11:51:15 +0000 (06:51 -0500)
committerShane Jaroch <chown_tee@proton.me>
Fri, 26 Dec 2025 14:11:26 +0000 (09:11 -0500)
.github/workflows/testing.yaml
.github/workflows/windows-and-mac.yaml
Makefile
ffpass/__init__.py
requirements-dev.txt
requirements.txt
scripts/generate_mp_profile.py
setup.cfg [new file with mode: 0644]
tests/__init__.py [new file with mode: 0644]
tests/test_mixed_keys_run.py
tests/test_run.py

index d00081b2dbb29e0f115b15f25770ea240142a0f7..1446a7e059fb641ff3185fbc710e68631632d1da 100644 (file)
@@ -1,3 +1,4 @@
+---
 name: ffpass
 
 on: [push, pull_request_target]
@@ -7,53 +8,30 @@ jobs:
     name: Test
     runs-on: [ubuntu-latest]
     steps:
-    - name: Checkout
-      uses: actions/checkout@v4
+      - name: Checkout
+        uses: actions/checkout@v4
 
-    - name: Set up Python
-      uses: actions/setup-python@v5
-      with:
-        python-version: '3.x'
-
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install .
-
-    - name: Test with pytest
-      run: |
-        pip install pytest
-        pip install pytest-cov
-        python -m pytest tests --junit-xml pytest.xml
-
-    - name: Lint with flake8
-      run: |
-        pip install flake8
-        flake8 . --exclude='*venv,build' --ignore=E741,E501
-
-    - name: Upload Unit Test Results
-      if: always()
-      uses: actions/upload-artifact@v4
-      with:
-        name: Unit Test Results
-        path: pytest.xml
-
-
-  publish-test-results:
-    name: "Publish Unit Tests Results"
-    needs: test
-    runs-on: ubuntu-latest
-    if: success() || failure()
-
-    steps:
-      - name: Download Artifacts
-        uses: actions/download-artifact@v4
-        with:
-          path: artifacts
-
-      - name: Publish Unit Test Results
-        uses: EnricoMi/publish-unit-test-result-action@v2
+      - name: Set up Python
+        uses: actions/setup-python@v5
         with:
-          check_name: Unit Test Results
-          github_token: ${{ secrets.GITHUB_TOKEN }}
-          files: "artifacts/Unit Test Results/pytest.xml"
+          python-version: "3.x"
+
+      - name: Install dependencies
+        run: >
+          python -m pip install --upgrade pip &&
+          python -m pip install
+          coveralls
+          -r requirements.txt
+          -r requirements-dev.txt
+
+      - name: Test with pytest
+        run: make test
+
+      - name: Lint with flake8
+        run: make lint
+
+      - name: Submit coverage report / coveralls
+        if: always()
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: python -m coveralls --service=github
index 29f3b2f845e78df296e65088a84326dbfd31ea78..0ede6a50f0c25c5a7191bd96a0eee012bf519f44 100644 (file)
@@ -1,50 +1,49 @@
+---
 name: windows-and-mac
 
 on: [push, pull_request_target]
 
 jobs:
   windows:
-    name: windows
     runs-on: [windows-latest]
     steps:
-    - name: Checkout
-      uses: actions/checkout@v4
+      - name: Checkout
+        uses: actions/checkout@v4
 
-    - name: Set up Python
-      uses: actions/setup-python@v5
-      with:
-        python-version: '3.x'
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.x"
 
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install .
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install .
 
-    - name: Test with pytest
-      run: |
-        pip install pytest
-        pip install pytest-cov
-        python -m pytest tests --junit-xml pytest.xml
+      - name: Test with pytest
+        run: |
+          pip install pytest
+          pip install pytest-cov
+          python -m pytest tests --junit-xml pytest.xml
 
   macOS:
-    name: macOS
     runs-on: [macos-latest]
     steps:
-    - name: Checkout
-      uses: actions/checkout@v4
-
-    - name: Set up Python
-      uses: actions/setup-python@v5
-      with:
-        python-version: '3.x'
-
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install .
-
-    - name: Test with pytest
-      run: |
-        pip install pytest
-        pip install pytest-cov
-        python -m pytest tests --junit-xml pytest.xml
\ No newline at end of file
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.x"
+
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install .
+
+      - name: Test with pytest
+        run: |
+          pip install pytest
+          pip install pytest-cov
+          python -m pytest tests --junit-xml pytest.xml
index 2390acd299dc65b0b72483cbf357d6331f4a40ec..9e052482f3c020cd7f8ea16ec8b2b368691bf100 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,35 +1,54 @@
 SHELL:=/bin/bash
 
-.PHONY: pypi
-pypi: dist
-       twine upload dist/*
+.DEFAULT_GOAL=_help
+
+# NOTE: must put a <TAB> character and two pound "\t##" to show up in this list.  Keep it brief! IGNORE_ME
+.PHONY: _help
+_help:
+       @printf "\nUsage: make <command>, valid commands:\n\n"
+       @grep "##" $(MAKEFILE_LIST) | grep -v IGNORE_ME | sed -e 's/##//' | column -t -s $$'\t'
+
 
-.PHONY: dist
-dist: flake8
+.PHONY: build
+build: lint    ## Build release
+build:
        -rm dist/*
        ./setup.py sdist bdist_wheel
 
-.PHONY: flake8
-flake8:
-       flake8 . --exclude '*venv,build' --count --select=E901,E999,F821,F822,F823 --show-source --statistics
-       flake8 . --exclude '*venv,build' --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
-       # CI pipeline
-       flake8 . --exclude='*venv,build' --ignore=E741,E501
 
+.PHONY: release
+release: build ## Upload release to PyPI (via Twine)
+       twine upload dist/*
+
+
+
+LINT_LOCS_PY ?= ffpass/ scripts tests/
+
+.PHONY: format
+format:        ## Not phased in yet, no-op
+       -black --check ${LINT_LOCS_PY}
+       -isort --check ${LINT_LOCS_PY}
+
+
+.PHONY: lint
+lint:  ## Lint the code
+       flake8 --count --show-source --statistics
 
-.PHONY: install
-install:
-       pip install .
 
 .PHONY: test
-test:
-       @echo 'Remember to run make install to test against the latest :)'
-       coverage run -m pytest -svv tests/
-       coverage report -m --omit="tests/*"
+test:  ## Run pytest & show coverage report
+       coverage run
+       coverage report
+
+
+
+.PHONY: install
+install:       ## Install from local source (via pip)
+       pip install .
 
 
 .PHONY: clean
-clean:
+clean: ## Clean up build files/cache
        rm -rf *.egg-info build dist
        rm -f .coverage
        find . \
index e7f7a09ff14e51aafd18224ef14bf1d4b9c57551..31849340db137990f9014ff96732be87891ba906 100755 (executable)
@@ -50,6 +50,8 @@ from pyasn1.codec.der.encoder import encode as der_encode
 from pyasn1.type.univ import Sequence, OctetString, ObjectIdentifier
 from Crypto.Cipher import AES, DES3
 
+# noqa: W503
+# line break before binary operator
 
 MAGIC1 = b"\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"
 
@@ -345,7 +347,7 @@ def getJsonLogins(directory):
 
 def dumpJsonLogins(directory, jsonLogins):
     with open(directory / "logins.json", "w") as loginf:
-        json.dump(jsonLogins, loginf, separators=",:")
+        json.dump(jsonLogins, loginf, separators=(",", ":"))
 
 
 def exportLogins(key, jsonLogins):
@@ -390,11 +392,13 @@ def readCSV(csv_file):
                 break
             # Heuristic: if it lacks a URL (index=1) and has user,pass (index=2,3), assume it's a header and continue
             if (
-                "http://" not in first_row[0]
-                and first_row[1].lower() in {"username", "uname", "user", "u"}  # noqa: W503 line break before binary operator
-                and first_row[2].lower() in {"password", "passwd", "pass", "p"}  # noqa: W503
+                first_row[0].lower() in {"url", "hostname", "website", "site", "address", "link"}
+                or (
+                    first_row[1].lower() in {"username", "uname", "user", "u"}
+                    and first_row[2].lower() in {"password", "passwd", "pass", "p"}
+                )
             ):
-                logging.debug(f"Continuing (skipping) over first row: [is_header={True}].")
+                logging.debug(f"Continuing (skipping) over first row: index=0, [is_header={True}].")
                 continue
 
             # ~~~ END peek at first row ~~~~~~~~~~
@@ -594,11 +598,13 @@ def makeParser():
         import argcomplete
         argcomplete.autocomplete(parser)
 
-    except ModuleNotFoundError:
-        sys.stderr(
-            "NOTE: You can run 'pip install argcomplete' "
+    except (ImportError, ModuleNotFoundError):
+        sys.stderr.write(
+            "Error: argcomplete not found, run 'pip install argcomplete' "
             "and add the hook to your shell RC for tab completion."
         )
+        sys.stderr.write(os.linesep)
+        sys.stderr.flush()
 
     return parser
 
index ded13e8bdefce9768c768bcb207d01edf863a949..64ce1efe455c1f261dfc02c76702be8a4985f8a2 100644 (file)
@@ -1,4 +1,3 @@
 coverage==7.13.0
 flake8==7.3.0
 pytest==9.0.2
-
index 9a633f250bdf814dd9ff07bc04d8f2e0807f4c34..b7383abeafbf2c461f508260043591b67d6927e9 100644 (file)
@@ -1,4 +1,3 @@
 argcomplete>=3.5.2
 pyasn1~=0.6.1
 pycryptodome~=3.23.0
-
index 59d7e045a2b338bb3d3d35255d6e382a4c81024d..2db925702a4295e6b13e7eb4163e170f3ac55e31 100755 (executable)
@@ -14,7 +14,6 @@ from hashlib import sha1
 from pathlib import Path
 
 from Crypto.Cipher import AES, DES3
-# Dependencies: pyasn1, pycryptodome
 from pyasn1.codec.der.encoder import encode as der_encode
 from pyasn1.type.univ import Integer, ObjectIdentifier, OctetString, Sequence
 
diff --git a/setup.cfg b/setup.cfg
new file mode 100644 (file)
index 0000000..af3409b
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,81 @@
+[tool:pytest]
+# See: https://docs.pytest.org/en/7.1.x/reference/customize.html
+testpaths =
+    tests
+
+[coverage:run]
+# See: https://coverage.readthedocs.io/en/7.2.2/config.html#run
+command_line = -m pytest -svv
+source = ffpass
+
+[coverage:report]
+fail_under = 75.00
+precision = 2
+
+show_missing = True
+skip_empty = True
+skip_covered = True
+
+exclude_lines =
+    pragma: no cover
+
+
+
+[flake8]
+exclude = .venv,venv,build,dist
+
+max-complexity = 10
+max-line-length = 127
+
+ignore =
+    # line break before binary operator
+    W503,
+    # line too long
+    E501,
+    # ambiguous variable name
+    E741,
+
+
+
+[isort]
+line_length = 88
+known_first_party = ntclient
+
+# See: https://copdips.com/2020/04/making-isort-compatible-with-black.html
+multi_line_output = 3
+include_trailing_comma = True
+
+
+
+[mypy]
+show_error_codes = True
+;show_error_context = True
+;pretty = True
+
+disallow_incomplete_defs = True
+disallow_untyped_defs = True
+disallow_untyped_calls = True
+disallow_untyped_decorators = True
+
+;strict_optional = True
+no_implicit_optional = True
+
+warn_return_any = True
+warn_redundant_casts = True
+warn_unreachable = True
+
+warn_unused_ignores = True
+warn_unused_configs = True
+warn_incomplete_stub = True
+
+# Our tests, they don't return a value typically
+[mypy-tests.*]
+disallow_untyped_defs = False
+
+# Our packages, nested dependencies
+; [mypy-<pkgname>]
+; ignore_missing_imports = True
+
+# 3rd party packages missing types
+[mypy-argcomplete]
+ignore_missing_imports = True
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
index e77ed028c4c0bfcc00886897e22f3d0797bdc9a2..5d9582ec8465ee1eb612071baf0f5feddf67b705 100644 (file)
@@ -6,7 +6,6 @@ Created on Fri Dec 25 19:30:48 2025
 @author: shane
 """
 
-import os
 import shutil
 import sys
 from pathlib import Path
@@ -14,7 +13,6 @@ from unittest.mock import patch
 
 import pytest
 
-OS_NEWLINE = os.linesep
 HEADER = "url,username,password"
 EXPECTED_MIXED_OUTPUT = [HEADER, "http://www.mixedkeys.com,modern_user,modern_pass"]
 
index b6aea287b197f0c1fc342e6c0aa4c50a4d16013e..801a56add156000b14ae36626e94dc3e7a42841c 100644 (file)
@@ -1,16 +1,14 @@
 #!/usr/bin/env python3
 
-import os
 import subprocess
 import shutil
-import pytest
 from pathlib import Path
 
-OS_NEWLINE = os.linesep
+import pytest
 
 MASTER_PASSWORD = 'test'
 HEADER = 'url,username,password'
-IMPORT_CREDENTIAL = 'http://www.example.com,foo,bar'
+IMPORT_CREDENTIAL = 'https://www.example.com,foo,bar'
 EXPECTED_EXPORT_OUTPUT = [HEADER, 'http://www.stealmylogin.com,test,test']
 EXPECTED_IMPORT_OUTPUT = EXPECTED_EXPORT_OUTPUT + [IMPORT_CREDENTIAL]
 
@@ -29,11 +27,11 @@ def clean_profile(tmp_path):
     return _setup
 
 
-def run_ffpass(mode, path):
-    command = ["python", "./ffpass/__init__.py", mode, "-d", str(path)]
+def run_ffpass_cmd(mode, path):
+    command = ["python", "./ffpass/__init__.py", mode, "--debug", "--dir", str(path)]
 
     if mode == 'import':
-        ffpass_input = OS_NEWLINE.join([HEADER, IMPORT_CREDENTIAL])
+        ffpass_input = "\n".join([HEADER, IMPORT_CREDENTIAL])
     else:
         ffpass_input = None
 
@@ -45,14 +43,14 @@ def stdout_splitter(input_text):
 
 
 def test_legacy_firefox_export(clean_profile):
-    r = run_ffpass('export', clean_profile('firefox-70'))
+    r = run_ffpass_cmd('export', clean_profile('firefox-70'))
     r.check_returncode()
     actual_export_output = stdout_splitter(r.stdout)
     assert actual_export_output == EXPECTED_EXPORT_OUTPUT
 
 
 def test_firefox_export(clean_profile):
-    r = run_ffpass('export', clean_profile('firefox-84'))
+    r = run_ffpass_cmd('export', clean_profile('firefox-84'))
     r.check_returncode()
     assert stdout_splitter(r.stdout) == EXPECTED_EXPORT_OUTPUT
 
@@ -60,7 +58,7 @@ def test_firefox_export(clean_profile):
 def test_firefox_aes_export(clean_profile):
     # This uses your new AES-encrypted profile
     profile_path = clean_profile('firefox-146-aes')
-    r = run_ffpass('export', profile_path)
+    r = run_ffpass_cmd('export', profile_path)
     r.check_returncode()
     assert stdout_splitter(r.stdout) == EXPECTED_EXPORT_OUTPUT
 
@@ -69,10 +67,10 @@ def test_legacy_firefox(clean_profile):
     profile_path = clean_profile('firefox-70')
 
     # modifies the temp file, not the original
-    r = run_ffpass('import', profile_path)
+    r = run_ffpass_cmd('import', profile_path)
     r.check_returncode()
 
-    r = run_ffpass('export', profile_path)
+    r = run_ffpass_cmd('export', profile_path)
     r.check_returncode()
     assert stdout_splitter(r.stdout) == EXPECTED_IMPORT_OUTPUT
 
@@ -80,9 +78,20 @@ def test_legacy_firefox(clean_profile):
 def test_firefox(clean_profile):
     profile_path = clean_profile('firefox-84')
 
-    r = run_ffpass('import', profile_path)
+    r = run_ffpass_cmd('import', profile_path)
+    r.check_returncode()
+
+    r = run_ffpass_cmd('export', profile_path)
+    r.check_returncode()
+    assert stdout_splitter(r.stdout) == EXPECTED_IMPORT_OUTPUT
+
+
+def test_firefox_aes(clean_profile):
+    profile_path = clean_profile('firefox-146-aes')
+
+    r = run_ffpass_cmd('import', profile_path)
     r.check_returncode()
 
-    r = run_ffpass('export', profile_path)
+    r = run_ffpass_cmd('export', profile_path)
     r.check_returncode()
     assert stdout_splitter(r.stdout) == EXPECTED_IMPORT_OUTPUT