mirror of
https://github.com/mvt-project/mvt.git
synced 2026-10-25 00:28:30 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8456059db |
No files matched your search
+2
-2
@@ -44,8 +44,8 @@ homepage = "https://docs.mvt.re/en/latest/"
|
|||||||
repository = "https://github.com/mvt-project/mvt"
|
repository = "https://github.com/mvt-project/mvt"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
mvt-ios = "mvt.ios:cli"
|
mvt-ios = "mvt.ios:main"
|
||||||
mvt-android = "mvt.android:cli"
|
mvt-android = "mvt.android:main"
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
from .cli import cli
|
from .cli import cli, main
|
||||||
+16
-5
@@ -514,8 +514,19 @@ def download_indicators():
|
|||||||
ioc_updates.update()
|
ioc_updates.update()
|
||||||
|
|
||||||
|
|
||||||
register_cli_plugins(
|
# ==============================================================================
|
||||||
cli,
|
# Entry point of the mvt-android console script
|
||||||
entry_point_group=ANDROID_CLI_PLUGIN_GROUP,
|
# ==============================================================================
|
||||||
environment_variable=MVT_ANDROID_CUSTOM_COMMANDS_ENV,
|
def main() -> None:
|
||||||
)
|
"""Register the external commands and run the mvt-android CLI.
|
||||||
|
|
||||||
|
External commands are registered here rather than when this module is
|
||||||
|
imported, so that importing MVT never runs third-party code and a plugin
|
||||||
|
importing from MVT cannot re-enter a module that is still initializing.
|
||||||
|
"""
|
||||||
|
register_cli_plugins(
|
||||||
|
cli,
|
||||||
|
entry_point_group=ANDROID_CLI_PLUGIN_GROUP,
|
||||||
|
environment_variable=MVT_ANDROID_CUSTOM_COMMANDS_ENV,
|
||||||
|
)
|
||||||
|
cli()
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
# Use of this software is governed by the MVT License 1.1 that can be found at
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
# https://license.mvt.re/1.1/
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
from .cli import cli
|
from .cli import cli, main
|
||||||
+16
-5
@@ -532,8 +532,19 @@ def download_iocs():
|
|||||||
ioc_updates.update()
|
ioc_updates.update()
|
||||||
|
|
||||||
|
|
||||||
register_cli_plugins(
|
# ==============================================================================
|
||||||
cli,
|
# Entry point of the mvt-ios console script
|
||||||
entry_point_group=IOS_CLI_PLUGIN_GROUP,
|
# ==============================================================================
|
||||||
environment_variable=MVT_IOS_CUSTOM_COMMANDS_ENV,
|
def main() -> None:
|
||||||
)
|
"""Register the external commands and run the mvt-ios CLI.
|
||||||
|
|
||||||
|
External commands are registered here rather than when this module is
|
||||||
|
imported, so that importing MVT never runs third-party code and a plugin
|
||||||
|
importing from MVT cannot re-enter a module that is still initializing.
|
||||||
|
"""
|
||||||
|
register_cli_plugins(
|
||||||
|
cli,
|
||||||
|
entry_point_group=IOS_CLI_PLUGIN_GROUP,
|
||||||
|
environment_variable=MVT_IOS_CUSTOM_COMMANDS_ENV,
|
||||||
|
)
|
||||||
|
cli()
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2026 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
"""Helpers building throwaway plugin distributions for the tests.
|
||||||
|
|
||||||
|
Some plugin behaviour only shows up in a fresh interpreter: what an import
|
||||||
|
executes, and what a plugin sees when MVT is imported before or after it.
|
||||||
|
These helpers write an importable distribution with a real entry point and
|
||||||
|
run a script against it in a subprocess, with a temporary home so that the
|
||||||
|
subprocess cannot touch the configuration of whoever runs the tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
FIXTURE_COMMAND_NAME = "fixture-plugin"
|
||||||
|
FIXTURE_MODULE_NAME = "fixture_cli_plugin"
|
||||||
|
FIXTURE_DISTRIBUTION_NAME = "fixture-cli-plugin"
|
||||||
|
|
||||||
|
|
||||||
|
def write_cli_plugin_distribution(
|
||||||
|
site_path: Path,
|
||||||
|
entry_point_group: str,
|
||||||
|
module_source: str,
|
||||||
|
) -> Path:
|
||||||
|
"""Write a distribution registering a CLI plugin entry point.
|
||||||
|
|
||||||
|
:param site_path: Folder to write the distribution into, to be added to
|
||||||
|
the import path of the interpreter loading it.
|
||||||
|
:param entry_point_group: Entry-point group to register the command in.
|
||||||
|
:param module_source: Source of the plugin module, which must define a
|
||||||
|
Click command named `cli`.
|
||||||
|
:returns: The folder the distribution was written to.
|
||||||
|
"""
|
||||||
|
site_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
(site_path / f"{FIXTURE_MODULE_NAME}.py").write_text(
|
||||||
|
module_source, encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
dist_info = (
|
||||||
|
site_path / f"{FIXTURE_DISTRIBUTION_NAME.replace('-', '_')}-1.0.dist-info"
|
||||||
|
)
|
||||||
|
dist_info.mkdir(exist_ok=True)
|
||||||
|
(dist_info / "METADATA").write_text(
|
||||||
|
f"Metadata-Version: 2.1\nName: {FIXTURE_DISTRIBUTION_NAME}\nVersion: 1.0\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(dist_info / "entry_points.txt").write_text(
|
||||||
|
f"[{entry_point_group}]\n{FIXTURE_COMMAND_NAME} = {FIXTURE_MODULE_NAME}:cli\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return site_path
|
||||||
|
|
||||||
|
|
||||||
|
def run_isolated_python(
|
||||||
|
script: str,
|
||||||
|
home: Path,
|
||||||
|
site_path: Optional[Path] = None,
|
||||||
|
**environment: str,
|
||||||
|
) -> subprocess.CompletedProcess:
|
||||||
|
"""Run a script in a fresh interpreter with its own configuration folder.
|
||||||
|
|
||||||
|
Importing MVT writes its configuration file, so the subprocess gets a
|
||||||
|
temporary home and no MVT environment variables from the test session.
|
||||||
|
"""
|
||||||
|
isolated_environment = {
|
||||||
|
key: value for key, value in os.environ.items() if not key.startswith("MVT_")
|
||||||
|
}
|
||||||
|
isolated_environment["HOME"] = str(home)
|
||||||
|
isolated_environment["XDG_CONFIG_HOME"] = str(home / "config")
|
||||||
|
if site_path is not None:
|
||||||
|
isolated_environment["PYTHONPATH"] = str(site_path)
|
||||||
|
isolated_environment.update(environment)
|
||||||
|
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, "-c", script],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=isolated_environment,
|
||||||
|
)
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# Copyright (c) 2021-2026 The MVT Authors.
|
||||||
|
# Use of this software is governed by the MVT License 1.1 that can be found at
|
||||||
|
# https://license.mvt.re/1.1/
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import click
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import mvt.android
|
||||||
|
import mvt.ios
|
||||||
|
from mvt.android.cli import cli as android_cli
|
||||||
|
from mvt.android.cli import main as android_main
|
||||||
|
from mvt.common.cli_plugins import ANDROID_CLI_PLUGIN_GROUP, IOS_CLI_PLUGIN_GROUP
|
||||||
|
from mvt.ios.cli import cli as ios_cli
|
||||||
|
from mvt.ios.cli import main as ios_main
|
||||||
|
|
||||||
|
from .plugin_fixtures import (
|
||||||
|
FIXTURE_COMMAND_NAME,
|
||||||
|
run_isolated_python,
|
||||||
|
write_cli_plugin_distribution,
|
||||||
|
)
|
||||||
|
|
||||||
|
MARKER_PLUGIN_TEMPLATE = """
|
||||||
|
import os
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
# Touched when this module is imported, so a test can tell whether loading MVT
|
||||||
|
# executed the plugin.
|
||||||
|
open(os.environ["FIXTURE_PLUGIN_MARKER"], "a").close()
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def cli():
|
||||||
|
click.echo("fixture plugin ran")
|
||||||
|
"""
|
||||||
|
|
||||||
|
PROGRAMS = {
|
||||||
|
"mvt-ios": (mvt.ios, ios_cli, IOS_CLI_PLUGIN_GROUP),
|
||||||
|
"mvt-android": (mvt.android, android_cli, ANDROID_CLI_PLUGIN_GROUP),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def restore_cli_commands():
|
||||||
|
"""Undo the plugin registration main() performs on the shared CLI groups."""
|
||||||
|
originals = {
|
||||||
|
program: dict(group.commands) for program, (_, group, _) in PROGRAMS.items()
|
||||||
|
}
|
||||||
|
yield
|
||||||
|
for program, (_, group, _) in PROGRAMS.items():
|
||||||
|
group.commands.clear()
|
||||||
|
group.commands.update(originals[program])
|
||||||
|
if hasattr(group, "_mvt_external_command_sources"):
|
||||||
|
delattr(group, "_mvt_external_command_sources")
|
||||||
|
|
||||||
|
|
||||||
|
def _install_fixture_entry_point(monkeypatch, entry_point_group, command):
|
||||||
|
def entry_points(*, group):
|
||||||
|
if group != entry_point_group:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
SimpleNamespace(
|
||||||
|
name=FIXTURE_COMMAND_NAME,
|
||||||
|
value="fixture_cli_plugin:cli",
|
||||||
|
load=lambda: command,
|
||||||
|
dist=SimpleNamespace(
|
||||||
|
metadata={"Name": "fixture-cli-plugin"}, version="1.0"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"mvt.common.cli_plugins.importlib.metadata.entry_points", entry_points
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _offline_argv(program, *arguments):
|
||||||
|
"""Build an argument list which keeps the CLI from checking for updates."""
|
||||||
|
return [
|
||||||
|
program,
|
||||||
|
"--disable-update-check",
|
||||||
|
"--disable-indicator-update-check",
|
||||||
|
*arguments,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("program", sorted(PROGRAMS))
|
||||||
|
def test_main_registers_installed_plugins_before_running_the_cli(
|
||||||
|
program, monkeypatch, capsys, restore_cli_commands
|
||||||
|
):
|
||||||
|
package, group, entry_point_group = PROGRAMS[program]
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def fixture_command():
|
||||||
|
click.echo("fixture plugin ran")
|
||||||
|
|
||||||
|
_install_fixture_entry_point(monkeypatch, entry_point_group, fixture_command)
|
||||||
|
monkeypatch.setattr(sys, "argv", _offline_argv(program, FIXTURE_COMMAND_NAME))
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exit_info:
|
||||||
|
package.main()
|
||||||
|
|
||||||
|
assert exit_info.value.code == 0
|
||||||
|
assert "fixture plugin ran" in capsys.readouterr().out
|
||||||
|
assert FIXTURE_COMMAND_NAME in group.commands
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("program", sorted(PROGRAMS))
|
||||||
|
def test_main_completes_plugin_command_names(
|
||||||
|
program, monkeypatch, capsys, restore_cli_commands
|
||||||
|
):
|
||||||
|
package, _, entry_point_group = PROGRAMS[program]
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
def fixture_command():
|
||||||
|
pass
|
||||||
|
|
||||||
|
_install_fixture_entry_point(monkeypatch, entry_point_group, fixture_command)
|
||||||
|
complete_variable = f"_{program.upper().replace('-', '_')}_COMPLETE"
|
||||||
|
monkeypatch.setenv(complete_variable, "bash_complete")
|
||||||
|
monkeypatch.setenv("COMP_WORDS", f"{program} fixture")
|
||||||
|
monkeypatch.setenv("COMP_CWORD", "1")
|
||||||
|
monkeypatch.setattr(sys, "argv", [program])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
package.main()
|
||||||
|
|
||||||
|
assert f"plain,{FIXTURE_COMMAND_NAME}" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("program", sorted(PROGRAMS))
|
||||||
|
def test_main_still_loads_commands_from_a_file(
|
||||||
|
program, monkeypatch, capsys, tmp_path, restore_cli_commands
|
||||||
|
):
|
||||||
|
package, _, entry_point_group = PROGRAMS[program]
|
||||||
|
command_path = tmp_path / "case_summary.py"
|
||||||
|
command_path.write_text(
|
||||||
|
"import click\n"
|
||||||
|
"\n"
|
||||||
|
"\n"
|
||||||
|
'@click.command("case-summary")\n'
|
||||||
|
"def cli():\n"
|
||||||
|
' click.echo("case summary ran")\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
_install_fixture_entry_point(
|
||||||
|
monkeypatch, entry_point_group, click.Command("unused")
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
sys,
|
||||||
|
"argv",
|
||||||
|
_offline_argv(program, "--load-command", str(command_path), "case-summary"),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exit_info:
|
||||||
|
package.main()
|
||||||
|
|
||||||
|
assert exit_info.value.code == 0
|
||||||
|
assert "case summary ran" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_console_script_targets_are_importable():
|
||||||
|
# [project.scripts] points at these, so they must stay on the packages.
|
||||||
|
assert mvt.ios.main is ios_main
|
||||||
|
assert mvt.android.main is android_main
|
||||||
|
|
||||||
|
|
||||||
|
def test_importing_mvt_does_not_run_installed_plugins(tmp_path):
|
||||||
|
site_path = write_cli_plugin_distribution(
|
||||||
|
tmp_path / "site", IOS_CLI_PLUGIN_GROUP, MARKER_PLUGIN_TEMPLATE
|
||||||
|
)
|
||||||
|
marker = tmp_path / "plugin-imported"
|
||||||
|
|
||||||
|
result = run_isolated_python(
|
||||||
|
"import mvt.ios.cli\nimport mvt.android.cli\nprint('imported')",
|
||||||
|
home=tmp_path / "home",
|
||||||
|
site_path=site_path,
|
||||||
|
FIXTURE_PLUGIN_MARKER=str(marker),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert "imported" in result.stdout
|
||||||
|
assert not marker.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_registering_the_plugins_runs_the_entry_point(tmp_path):
|
||||||
|
site_path = write_cli_plugin_distribution(
|
||||||
|
tmp_path / "site", IOS_CLI_PLUGIN_GROUP, MARKER_PLUGIN_TEMPLATE
|
||||||
|
)
|
||||||
|
marker = tmp_path / "plugin-imported"
|
||||||
|
|
||||||
|
result = run_isolated_python(
|
||||||
|
"import click\n"
|
||||||
|
"from mvt.common.cli_plugins import (\n"
|
||||||
|
" IOS_CLI_PLUGIN_GROUP,\n"
|
||||||
|
" BrokenPluginCommand,\n"
|
||||||
|
" register_installed_cli_commands,\n"
|
||||||
|
")\n"
|
||||||
|
"group = click.Group()\n"
|
||||||
|
"register_installed_cli_commands(group, IOS_CLI_PLUGIN_GROUP)\n"
|
||||||
|
f"command = group.commands[{FIXTURE_COMMAND_NAME!r}]\n"
|
||||||
|
"assert not isinstance(command, BrokenPluginCommand), command.help\n"
|
||||||
|
"print('registered')\n",
|
||||||
|
home=tmp_path / "home",
|
||||||
|
site_path=site_path,
|
||||||
|
FIXTURE_PLUGIN_MARKER=str(marker),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
assert "registered" in result.stdout
|
||||||
|
assert marker.exists()
|
||||||
Reference in new issue
Block a user