Compare commits

...
Author SHA1 Message Date
besendorf c9190e05ef Drop Python 3.10 support (#976) 2026-10-29 15:45:53 +01:00
besendorf df540d7c41 Merge pull request #984 from arxhr007/feat/979-reprompt-backup-password
Ask for the backup password again when it is wrong
2026-10-28 09:30:52 +01:00
arxhr007 706e81dff2 feat(android): ask for the backup password again when it is wrong
A wrong password typed at the prompt ended the check with "Invalid backup
password", and check-androidqf skipped the backup modules. An analyst who
was handed several candidate passwords had to start the whole run again
for each one.

Ask for the password again, up to three attempts in total. A password
given with --backup-password or in the environment is not asked for
again, as before, so scripted runs fail the same way.

Fixes #979
2026-10-27 21:43:01 +05:30
github-actions[bot] 3ba15716a2 Add new iOS versions and build numbers (#983) 2026-10-27 08:07:27 +01:00
8068569dce Hide the per-command --verbose aliases from --help (#965)
The group-level -v/--verbose on mvt-ios and mvt-android is the documented
form; the per-command flags stay as hidden aliases for compatibility.


Claude-Session: https://claude.ai/code/session_01FiSwVbasf7HbjC18fALafb

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2026-10-26 15:03:01 +01:00
CHE3MZandDonncha Ó Cearbhaill 6e9408e30c Add favicon and logo to mkdocs site and add dark/light mode toggle (#966)
* Add favicon and logo to mkdocs site and add dark/light mode toggle

* Use black primary colour and follow the OS light/dark mode

Switch the palette to the automatic light/dark mode setup from the
Material for MkDocs docs, with the same toggle icons the Material site
uses. Drop the blue grey accent, which Material does not support as an
accent colour, and remove trailing whitespace.

---------

Co-authored-by: Donncha Ó Cearbhaill <donncha.ocearbhaill@amnesty.org>
2026-10-26 13:20:34 +01:00
scribblemaniac db809cbc46 Update dockerfiles to revoke eMudhra intermediate certs (#981) 2026-10-26 12:25:47 +01:00
besendorf f4bfc39c27 Merge pull request #973 from besendorf/support/python-3.15
Add Python 3.15 support
2026-10-20 12:57:55 +02:00
besendorf a645913376 Use stable Python 3.15 in CI 2026-10-20 12:52:19 +02:00
Janik Besendorf 4990b22259 Add Python 3.15 to the CI matrix 2026-10-09 17:26:29 +02:00
besendorf b3aa443819 Merge pull request #970 from WingManOO7/fix/settings-leap-day
Keep settings history timestamps on 29 February
2026-10-09 12:52:44 +02:00
besendorf aac41ec8c2 Test settings leap-day resolution across a skipped century
Ensure a February 29 history entry in a January 2104 dump resolves to 2096, since 2100 is not a leap year.
2026-10-09 03:49:54 -07:00
besendorf 1e05f438d7 Include the eighth prior year when resolving settings leap days
Adjust the year range for leap year calculation to account for century years.
2026-10-09 03:48:37 -07:00
Muhammad Mohid Shafiq 10f739d669 Keep settings history timestamps on 29 February
dumpsys prints the settings change history without a year, so
_resolve_timestamp() parsed the value with strptime("%m-%d ...") and only
then replaced the year. strptime() parses a value without a year in 1900,
which has no 29 February, so every change made on a leap day raised
ValueError and lost its timestamp, dropping it from the timeline and from
the alert of a dangerous setting.

Parse the value together with a year instead, starting from the year the
section was dumped and going back until the date exists and is not later
than the dump. Eight years always reach a leap year.

This also removes the DeprecationWarning Python 3.13 raises for parsing a
day of month without a year (python/cpython#70647).
2026-10-08 19:35:29 +05:00
besendorf 94e9cd3712 Merge pull request #968 from va-resident/fix/adb-state-closing-brace
Fix spurious ADB state parse error on devices without stored keys
2026-10-07 17:27:15 +02:00
besendorf 300cc26112 Merge branch 'main' into fix/adb-state-closing-brace 2026-10-07 17:25:29 +02:00
besendorf 3c12e1a019 Merge pull request #925 from mvt-project/docs/recommend-pymobiledevice3
Recommend pymobiledevice3 for iOS acquisition
2026-10-07 16:12:55 +02:00
va@resident 3a462e1e2c Fix spurious ADB state parse error on devices without stored keys
Since #957 the parsed body keeps the closing brace of debugging_manager
and the blank lines before it. The brace popped two levels, and a blank
line after system_keys raised IndexError. Don't pop when the
indentation drops on a closing brace, and skip blank lines outside
multiline values.

Fixes #967.
2026-10-06 15:29:41 +05:00
23 changed files with 317 additions and 52 deletions

No files matched your search

+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v7
with:
python-version: "3.10"
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v10.0.0
with:
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v7
with:
python-version: "3.10"
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v10.0.0
with:
+2 -2
View File
@@ -16,10 +16,10 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
python-version: ['3.11', '3.12', '3.13', '3.14', '3.15']
env:
# Takes precedence over .python-version, which otherwise makes `uv run`
# rebuild the venv with 3.10 and test every matrix entry on 3.10.
# rebuild the venv with 3.11 and test every matrix entry on 3.11.
UV_PYTHON: ${{ matrix.python-version }}
steps:
+1 -1
View File
@@ -1 +1 @@
3.10
3.11
+2 -2
View File
@@ -1,6 +1,6 @@
# Create main image
FROM ubuntu:24.04 AS main
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /uvx /usr/local/bin/
COPY --from=ghcr.io/astral-sh/uv:0.13.5 /uv /uvx /usr/local/bin/
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
@@ -62,4 +62,4 @@ WORKDIR /home/cases
RUN echo 'echo "Mobile Verification Toolkit @ Docker\n------------------------------------\n\nYou can find information about how to use this image for Android (https://github.com/mvt-project/mvt/tree/master/docs/android) and iOS (https://github.com/mvt-project/mvt/tree/master/docs/ios) in the official docs of the project.\n"' >> ~/.bashrc \
&& echo 'echo "Note that to perform the debug via USB you might need to give the Docker image access to the USB using \"docker run -it --privileged -v /dev/bus/usb:/dev/bus/usb mvt\" or, preferably, the \"--device=\" parameter.\n"' >> ~/.bashrc
CMD /bin/bash
CMD [ "/bin/bash" ]
+3 -3
View File
@@ -1,6 +1,6 @@
# Create main image
FROM python:3.10.14-alpine3.20 AS main
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /uvx /usr/local/bin/
FROM python:3.11.17-alpine3.23 AS main
COPY --from=ghcr.io/astral-sh/uv:0.13.5 /uv /uvx /usr/local/bin/
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
@@ -8,7 +8,7 @@ LABEL org.opencontainers.image.source="https://github.com/mvt-project/mvt"
LABEL org.opencontainers.image.title="Mobile Verification Toolkit (Android)"
LABEL org.opencontainers.image.description="MVT is a forensic tool to look for signs of infection in smartphone devices."
LABEL org.opencontainers.image.licenses="MVT License 1.1"
LABEL org.opencontainers.image.base.name=docker.io/library/python:3.10.14-alpine3.20
LABEL org.opencontainers.image.base.name=docker.io/library/python:3.11.17-alpine3.23
# Install runtime dependencies
RUN apk add --no-cache \
+1 -1
View File
@@ -1,7 +1,7 @@
# Main image
# ----------
FROM ubuntu:24.04 AS main
COPY --from=ghcr.io/astral-sh/uv:0.11.8 /uv /uvx /usr/local/bin/
COPY --from=ghcr.io/astral-sh/uv:0.13.5 /uv /uvx /usr/local/bin/
LABEL org.opencontainers.image.url="https://mvt.re"
LABEL org.opencontainers.image.documentation="https://docs.mvt.re"
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
# Installation
Before proceeding, please note that MVT requires Python 3.10 or newer to run. While it should be available on most operating systems, please make sure of that before proceeding.
Before proceeding, please note that MVT requires Python 3.11 or newer to run. While it should be available on most operating systems, please make sure of that before proceeding.
## Dependencies on Linux
+20
View File
@@ -18,8 +18,28 @@ markdown_extensions:
use_pygments: true
theme:
name: material
logo: mvt.png
favicon: favicon.png
features:
- tabs
palette:
- media: "(prefers-color-scheme)"
primary: black
toggle:
icon: material/link
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: default
primary: black
toggle:
icon: material/toggle-switch
name: Switch to dark mode
- media: "(prefers-color-scheme: dark)"
scheme: slate
primary: black
toggle:
icon: material/toggle-switch-off
name: Switch to system preference
plugins:
- search
- mkdocstrings
+1 -1
View File
@@ -36,7 +36,7 @@ dependencies = [
"python-dateutil==2.9.0.post0",
"tzdata==2026.3",
]
requires-python = ">= 3.10"
requires-python = ">= 3.11"
[project.urls]
homepage = "https://docs.mvt.re/en/latest/"
+9 -1
View File
@@ -33,7 +33,12 @@ class DumpsysADBArtifact(AndroidArtifact):
line = line.removesuffix(b"\r")
# Track the level of indentation
indent = len(line) - len(line.lstrip())
if indent < cur_indent:
# A closing brace is dedented from the block it closes, and the
# brace handler below pops that block. Popping on the dedent as
# well would close two levels for one brace. A blank line has no
# indentation to compare: it ends a multiline value (handled
# below) and is otherwise skipped.
if indent < cur_indent and line.strip() not in (b"", b"}"):
# If the current line is less indented than the previous one, back out
if len(stack) <= 1:
# Dedenting below the outermost level means this is not the
@@ -72,6 +77,9 @@ class DumpsysADBArtifact(AndroidArtifact):
stack.pop()
current_dict = stack[-1]
if not line.strip():
continue
if key == "}":
if len(stack) <= 1:
self.log.error(
+14 -8
View File
@@ -255,19 +255,25 @@ class Settings(AndroidArtifact):
dumpsys prints the change history without a year, so it is resolved
against the time the section was dumped: the most recent matching date
at or before that time.
The year is parsed together with the value, because strptime() would
otherwise parse it in 1900, which has no 29 February. The most recent
matching leap day can be eight years earlier when a century year is
not a leap year and the current year's leap day is still in the future.
"""
if section_end is None:
return None
try:
partial = datetime.strptime(value, "%m-%d %H:%M:%S.%f")
timestamp = partial.replace(year=section_end.year)
if timestamp > section_end:
timestamp = partial.replace(year=section_end.year - 1)
except ValueError:
return None
for year in range(section_end.year, section_end.year - 9, -1):
try:
timestamp = datetime.strptime(f"{year}-{value}", "%Y-%m-%d %H:%M:%S.%f")
except ValueError:
continue
return convert_datetime_to_iso(timestamp)
if timestamp <= section_end:
return convert_datetime_to_iso(timestamp)
return None
def _parse_history(
self, line: str, section_end: Optional[datetime]
+12 -4
View File
@@ -160,7 +160,9 @@ def check_adb(ctx):
"zone is read from persist.sys.timezone in the bugreport itself."
),
)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("BUGREPORT_PATH", type=click.Path(exists=True))
@click.pass_context
def check_bugreport(
@@ -235,7 +237,9 @@ def check_bugreport(
)
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_backup(
@@ -310,7 +314,9 @@ def check_backup(
)
@click.option("--non-interactive", "-n", is_flag=True, help=HELP_MSG_NONINTERACTIVE)
@click.option("--backup-password", "-p", help=HELP_MSG_ANDROID_BACKUP_PASSWORD)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("ANDROIDQF_PATH", type=click.Path(exists=True))
@click.pass_context
def check_androidqf(
@@ -397,7 +403,9 @@ def check_androidqf(
"time instead of UTC."
),
)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("LOGS_PATH", type=click.Path(exists=True))
@click.pass_context
def check_intrusion_logs(
+42 -19
View File
@@ -27,6 +27,8 @@ from .modules.backup import BACKUP_MODULES
log = logging.getLogger(__name__)
MAX_BACKUP_PASSWORD_ATTEMPTS = 3
class InvalidAndroidBackup(Exception):
pass
@@ -72,6 +74,15 @@ class CmdAndroidCheckBackup(Command):
self.__tar: Optional[tarfile.TarFile] = None
self.__files: List[str] = []
def _get_backup_password(self) -> str:
password = prompt_or_load_android_backup_password(log, self.module_options)
if not password:
if self.sub_command:
raise InvalidAndroidBackup("No backup password provided")
log.critical("No backup password provided.")
sys.exit(1)
return password
def from_ab(self, ab_file_bytes: bytes) -> None:
self.__type = "ab"
header = parse_ab_header(ab_file_bytes)
@@ -84,28 +95,40 @@ class CmdAndroidCheckBackup(Command):
sys.exit(1)
password = None
# A password typed at the prompt is asked for again if it is wrong.
# One given on the command line or in the environment is not.
max_attempts = 1
if header["encryption"] != "none":
password = prompt_or_load_android_backup_password(log, self.module_options)
if not password:
if not self.module_options.get("backup_password", None):
max_attempts = MAX_BACKUP_PASSWORD_ATTEMPTS
password = self._get_backup_password()
attempt = 1
while True:
try:
tardata = parse_backup_file(ab_file_bytes, password=password)
break
except InvalidBackupPassword:
if attempt < max_attempts:
attempt += 1
log.error(
"Invalid backup password, try again (attempt %d of %d)",
attempt,
max_attempts,
)
password = self._get_backup_password()
continue
if self.sub_command:
raise InvalidAndroidBackup("No backup password provided")
log.critical("No backup password provided.")
raise InvalidAndroidBackup("Invalid backup password")
log.critical("Invalid backup password")
sys.exit(1)
except AndroidBackupParsingError as exc:
if self.sub_command:
raise InvalidAndroidBackup(
f"Impossible to parse this backup file: {exc}"
) from exc
log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1)
try:
tardata = parse_backup_file(ab_file_bytes, password=password)
except InvalidBackupPassword:
if self.sub_command:
raise InvalidAndroidBackup("Invalid backup password")
log.critical("Invalid backup password")
sys.exit(1)
except AndroidBackupParsingError as exc:
if self.sub_command:
raise InvalidAndroidBackup(
f"Impossible to parse this backup file: {exc}"
) from exc
log.critical("Impossible to parse this backup file: %s", exc)
log.critical("Please use Android Backup Extractor (ABE) instead")
sys.exit(1)
dbytes = io.BytesIO(tardata)
try:
+1 -1
View File
@@ -6,7 +6,7 @@
"""Password prompts with masked keyboard feedback.
This is based on the ``echo_char`` support added to :mod:`getpass` in Python
3.14. MVT supports Python 3.10 and later, so it cannot use that API directly.
3.14. MVT supports Python 3.11 and later, so it cannot use that API directly.
"""
import contextlib
+9 -3
View File
@@ -278,7 +278,9 @@ def extract_key(password, key_file, backup_path):
help=HELP_MSG_LOAD_MODULE,
)
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("BACKUP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_backup(
@@ -347,7 +349,9 @@ def check_backup(
help=HELP_MSG_LOAD_MODULE,
)
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("DUMP_PATH", type=click.Path(exists=True))
@click.pass_context
def check_fs(
@@ -416,7 +420,9 @@ def check_fs(
help=HELP_MSG_LOAD_MODULE,
)
@click.option("--hashes", "-H", is_flag=True, help=HELP_MSG_HASHES)
@click.option("--verbose", "-v", is_flag=True, help=HELP_MSG_VERBOSE_COMMAND)
@click.option(
"--verbose", "-v", is_flag=True, hidden=True, help=HELP_MSG_VERBOSE_COMMAND
)
@click.argument("SYSDIAGNOSE_PATH", type=click.Path(exists=True))
@click.pass_context
def check_sysdiagnose(
+12
View File
@@ -1224,6 +1224,10 @@
"version": "18.7.10",
"build": "22H374"
},
{
"version": "18.7.11",
"build": "22H384"
},
{
"version": "26",
"build": "23A341"
@@ -1283,5 +1287,13 @@
{
"version": "27.0.1",
"build": "24A446"
},
{
"version": "27.2",
"build": "24B91020"
},
{
"version": "27.2",
"build": "24B119"
}
]
@@ -209,6 +209,85 @@ class TestDumpsysADBArtifact:
assert len(da_adb.results) == 1
assert da_adb.results[0]["user_keys"][0]["user"] == user.decode()
def test_state_without_keys_is_parsed_without_error(self, caplog):
# Shape printed by every device with no stored ADB keys (Samsung
# SM-A525F, Android 12). The block's closing brace is dedented from
# the last entry and must close exactly one level.
da_adb = DumpsysADBArtifact()
da_adb.parse(
b"ADB MANAGER STATE (dumpsys adb):\n"
b"{\n"
b" debugging_manager={\n"
b" connected_to_adb=false\n"
b" }\n"
b"}\n"
b"--------- 0.015s was the duration of dumpsys adb\n"
)
assert len(da_adb.results) == 1
assert da_adb.results[0]["connected_to_adb"] is False
assert not [r for r in caplog.records if r.levelname == "ERROR"]
def test_nested_block_as_last_entry_is_parsed_without_error(self, caplog):
# Shape printed by Pixel devices with no stored ADB keys.
da_adb = DumpsysADBArtifact()
da_adb.parse(
b"ADB MANAGER STATE (dumpsys adb):\n"
b"{\n"
b" debugging_manager={\n"
b" connected_to_adb=false\n"
b" adb_wifi={\n"
b" enabled=false\n"
b" tls_port=0\n"
b" }\n"
b" }\n"
b"}\n"
)
assert len(da_adb.results) == 1
assert da_adb.results[0]["connected_to_adb"] is False
assert da_adb.results[0]["adb_wifi"]["tls_port"] == b"0"
assert not [r for r in caplog.records if r.levelname == "ERROR"]
def test_entry_after_nested_block_stays_in_debugging_manager(self):
da_adb = DumpsysADBArtifact()
da_adb.parse(
b"ADB MANAGER STATE (dumpsys adb):\n"
b"{\n"
b" debugging_manager={\n"
b" adb_wifi={\n"
b" enabled=false\n"
b" }\n"
b" connected_to_adb=true\n"
b" }\n"
b"}\n"
)
assert len(da_adb.results) == 1
assert da_adb.results[0]["connected_to_adb"] is True
def test_blank_line_after_single_line_value_does_not_raise(self):
# `system_keys` is the content of /adb_keys and ends with a newline,
# so a blank line follows it. It is not a multiline field, so the
# blank line is not a multiline terminator either.
da_adb = DumpsysADBArtifact()
da_adb.parse(
b"ADB MANAGER STATE (dumpsys adb):\n"
b"{\n"
b" debugging_manager={\n"
b" connected_to_adb=true\n"
b" user_keys=QUJDRA== host@example\n"
b"\n"
b" system_keys=RUZHSA== system@example\n"
b"\n"
b" }\n"
b"}\n"
)
assert len(da_adb.results) == 1
assert da_adb.results[0]["user_keys"][0]["user"] == "host@example"
assert da_adb.results[0]["system_keys"] == "RUZHSA== system@example"
def test_unbalanced_state_is_reported_rather_than_raising(self):
da_adb = DumpsysADBArtifact()
da_adb.parse(
+43
View File
@@ -3,6 +3,8 @@
# Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/
from typing import Optional
from mvt.android.artifacts.settings import Settings
from ..utils import get_artifact
@@ -21,6 +23,21 @@ def find(settings: Settings, name: str) -> list:
return [result for result in settings.results if result["name"] == name]
def resolve_history_time(time: str, section_end: str) -> Optional[str]:
settings = Settings()
settings.parse(
"SECURE SETTINGS (user 0)\n"
"_id:240 name:accessibility_enabled pkg:android value:1\n"
"\tHistory (accessibility_enabled)\n"
f"\t\ttime:{time} mode:update oldValue:0 newValue:1 "
"package:com.example.helper\n"
"\n"
"--------- 0.019s was the duration of dumpsys settings, "
f"ending at: {section_end}\n"
)
return settings.results[0]["history"][0]["timestamp"]
class TestSettingsArtifact:
def test_parsing(self):
settings = parse_bugreport_settings()
@@ -159,6 +176,32 @@ class TestSettingsArtifact:
}
]
def test_leap_day_history_keeps_its_timestamp(self):
assert (
resolve_history_time("02-29 22:41:07.980", "2024-03-05 23:14:28")
== "2024-02-29 22:41:07.980000"
)
def test_leap_day_history_resolved_to_the_last_leap_year(self):
# The most recent 29 February at or before the dump can be several
# years back when the dump was taken in a year without one.
assert (
resolve_history_time("02-29 22:41:07.980", "2025-03-01 10:00:00")
== "2024-02-29 22:41:07.980000"
)
assert (
resolve_history_time("02-29 22:41:07.980", "2024-01-10 10:00:00")
== "2020-02-29 22:41:07.980000"
)
def test_leap_day_history_before_a_leap_day_after_a_skipped_century(self):
# 2100 is not a leap year, so before 29 February 2104 the most
# recent matching date is eight years earlier, in 2096.
assert (
resolve_history_time("02-29 22:41:07.980", "2104-01-10 10:00:00")
== "2096-02-29 22:41:07.980000"
)
def test_dangerous_setting_is_detected_with_the_changing_package(self):
settings = parse_bugreport_settings()
settings.check_indicators()
+60
View File
@@ -59,6 +59,66 @@ class TestCheckAndroidBackupCommand:
assert result.exit_code == 1
assert "Invalid backup password" in caplog.text
def test_check_encrypted_backup_prompt_retry(self, mocker):
"""A wrong password at the prompt is asked for again"""
prompt_mock = mocker.patch(
"mvt.android.modules.backup.helpers.prompt_password",
side_effect=["wrong_password", TEST_BACKUP_PASSWORD],
)
runner = CliRunner()
path = os.path.join(get_artifact_folder(), "androidqf_encrypted/backup.ab")
result = runner.invoke(check_backup, [path])
assert prompt_mock.call_count == 2
assert result.exit_code == 0
def test_check_encrypted_backup_prompt_gives_up(self, mocker, caplog):
"""The prompt gives up after three wrong passwords"""
prompt_mock = mocker.patch(
"mvt.android.modules.backup.helpers.prompt_password",
return_value="wrong_password",
)
runner = CliRunner()
path = os.path.join(get_artifact_folder(), "androidqf_encrypted/backup.ab")
with caplog.at_level(logging.CRITICAL):
result = runner.invoke(check_backup, [path])
assert prompt_mock.call_count == 3
assert result.exit_code == 1
assert "Invalid backup password" in caplog.text
def test_check_encrypted_backup_prompt_retry_empty(self, mocker, caplog):
"""An empty answer on a retry ends the check"""
prompt_mock = mocker.patch(
"mvt.android.modules.backup.helpers.prompt_password",
side_effect=["wrong_password", ""],
)
runner = CliRunner()
path = os.path.join(get_artifact_folder(), "androidqf_encrypted/backup.ab")
with caplog.at_level(logging.CRITICAL):
result = runner.invoke(check_backup, [path])
assert prompt_mock.call_count == 2
assert result.exit_code == 1
assert "No backup password provided" in caplog.text
def test_check_encrypted_backup_cli_invalid_no_prompt(self, mocker):
"""A password given on the command line is not asked for again"""
prompt_mock = mocker.patch(
"mvt.android.modules.backup.helpers.prompt_password",
return_value=TEST_BACKUP_PASSWORD,
)
runner = CliRunner()
path = os.path.join(get_artifact_folder(), "androidqf_encrypted/backup.ab")
result = runner.invoke(
check_backup, ["--backup-password", "invalid_password", path]
)
assert prompt_mock.call_count == 0
assert result.exit_code == 1
def test_check_encrypted_backup_env(self, mocker):
"""Provide password as environment variable"""
prompt_mock = mocker.patch(
+2 -2
View File
@@ -100,8 +100,8 @@ class TestVerboseOnTheCheckCommands:
assert result.exit_code == 0
assert _console_level() == logging.DEBUG
def test_the_command_option_says_it_is_kept_for_compatibility(self):
def test_the_command_option_is_hidden_from_the_help(self):
result = CliRunner().invoke(ios_cli, [*OFFLINE, "check-backup", "--help"])
assert result.exit_code == 0
assert "kept for compatibility" in result.output
assert "--verbose" not in result.output
Generated
+1 -1
View File
@@ -1,6 +1,6 @@
version = 1
revision = 3
requires-python = ">=3.10"
requires-python = ">=3.11"
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version < '3.15'",