[GEMINI] Organize test suite to mirror new ctf.cli and ctf.forensics package structures

This commit is contained in:
venus
2026-07-19 03:46:26 -05:00
parent c61bfa11e5
commit 058b5c1eb5
6 changed files with 304 additions and 281 deletions

121
tests/cli/test_basic.py Normal file
View File

@@ -0,0 +1,121 @@
# tests/cli/test_basic.py
# {{{ imports
from click.testing import CliRunner
from ctf.cli.basic import basic_group
from ctf.config import Config
# }}}
# {{{ test_basic_test_cli
def test_basic_test_cli():
"""
Verifies that 'basic test' runs successfully and outputs 'hello from test'.
"""
runner = CliRunner()
result = runner.invoke(basic_group, ["test"])
assert result.exit_code == 0
assert "hello from test" in result.output
# }}}
# {{{ test_basic_greet_cli
def test_basic_greet_cli():
"""
Verifies that 'basic greet' runs successfully and greets the name argument.
"""
runner = CliRunner()
result = runner.invoke(basic_group, ["greet", "Alice"])
assert result.exit_code == 0
assert "hello Alice" in result.output
# }}}
# {{{ test_basic_set_flag_format_cli
def test_basic_set_flag_format_cli():
"""
Verifies that 'basic set-flag-format' CLI command writes the pattern to config.toml.
"""
runner = CliRunner()
cfg = Config()
assert cfg.data["Competition"]["flag_format"] == r"picoCTF\{.*\}"
result = runner.invoke(basic_group, ["set-flag-format", "TEST_FLAG{[a-z]+}"])
assert result.exit_code == 0
assert "Flag format set to" in result.output
updated_cfg = Config()
assert updated_cfg.data["Competition"]["flag_format"] == "TEST_FLAG{[a-z]+}"
# }}}
# {{{ test_basic_set_flag_format_original_option_cli
def test_basic_set_flag_format_original_option_cli():
"""
Verifies that 'basic set-flag-format' with --original/-o option prompts for selection and writes it.
"""
runner = CliRunner()
result = runner.invoke(basic_group, ["set-flag-format", "-o", "SKY-1111-000"], input="2\n")
assert result.exit_code == 0
assert "Select a pattern index" in result.output
assert "Flag format set to" in result.output
updated_cfg = Config()
selected_pattern = updated_cfg.data["Competition"]["flag_format"]
assert len(selected_pattern) > 0
# }}}
# {{{ test_basic_set_flag_format_validation_cli
def test_basic_set_flag_format_validation_cli():
"""
Verifies validation rules:
- Specifying both PATTERN and --original should fail.
- Specifying neither PATTERN nor --original should fail.
- Specifying empty/whitespace-only original flag should fail.
"""
runner = CliRunner()
# Both specified
result1 = runner.invoke(basic_group, ["set-flag-format", "PAT", "-o", "SKY-1111-000"])
assert result1.exit_code != 0
assert "Cannot specify both PATTERN and --original/-o option." in result1.output
# Neither specified
result2 = runner.invoke(basic_group, ["set-flag-format"])
assert result2.exit_code != 0
assert "Either PATTERN positional argument or --original/-o option must be specified." in result2.output
# Empty original
result3 = runner.invoke(basic_group, ["set-flag-format", "-o", " "])
assert result3.exit_code != 0
assert "Original flag cannot be empty or whitespace only." in result3.output
# }}}
# {{{ test_basic_set_competition_cli
def test_basic_set_competition_cli():
"""
Verifies that 'basic set-competition' updates the competition name in config.toml.
"""
runner = CliRunner()
result = runner.invoke(basic_group, ["set-competition", "CyberCTF2026"])
assert result.exit_code == 0
assert "Competition name set to: CyberCTF2026" in result.output
updated_cfg = Config()
assert updated_cfg.data["Competition"]["competition"] == "CyberCTF2026"
# }}}
# {{{ test_basic_set_flag_format_warning_cli
def test_basic_set_flag_format_warning_cli():
"""
Verifies that if competition name is set and not present in the example flag,
set-flag-format outputs a warning.
"""
runner = CliRunner()
# Set competition name first
cfg = Config()
cfg.data["Competition"]["competition"] = "SECURE"
cfg.save(cfg.data)
result = runner.invoke(basic_group, ["set-flag-format", "-o", "CTF{easy_flag_123}"], input="1\n")
assert result.exit_code == 0
assert "Warning: Current competition name 'SECURE' was not found in the example flag." in result.output
# }}}

97
tests/cli/test_flag.py Normal file
View File

@@ -0,0 +1,97 @@
# tests/cli/test_flag.py
# {{{ imports
import sys
import pytest
from pathlib import Path
from click.testing import CliRunner
from unittest.mock import patch
from ctf.config import Config
# }}}
# {{{ test_flag_cmd_cli
def test_flag_cmd_cli():
"""
Verifies the ctf flag command outputs last_flag with and without --plain.
"""
from ctf.cli import cli
cfg = Config()
cfg.data["Competition"]["last_flag"] = "flag{test_cli_flag}"
cfg.save(cfg.data)
runner = CliRunner()
# Test standard flag command output
result_std = runner.invoke(cli, ["flag"])
assert result_std.exit_code == 0
assert "Last detected flag: flag{test_cli_flag}" in result_std.output
# Test plain flag command output
result_plain = runner.invoke(cli, ["flag", "--plain"])
assert result_plain.exit_code == 0
assert result_plain.output.strip() == "flag{test_cli_flag}"
# }}}
# {{{ test_flag_cmd_exemption
def test_flag_cmd_exemption(capsys):
"""
Verifies that running ctf flag is exempted from triggering the stdout flag warning box.
"""
from ctf.main import main
cfg = Config()
cfg.data["Competition"]["flag_format"] = r"flag\{[a-z_]+\}"
cfg.data["Competition"]["last_flag"] = "flag{test_exempt_flag}"
cfg.save(cfg.data)
with patch.object(sys, "argv", ["ctf", "flag"]):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
captured = capsys.readouterr()
# Verify that the warning box is NOT in stdout
assert "Potential flag(s) detected in command output" not in captured.out
# Verify that the actual flag info IS printed
assert "Last detected flag: flag{test_exempt_flag}" in captured.out
# }}}
# {{{ test_flag_cmd_list_format
def test_flag_cmd_list_format():
"""
Verifies that ctf flag --list / -l prints the current flag format.
"""
from ctf.cli import cli
cfg = Config()
cfg.data["Competition"]["flag_format"] = "TEST_FORMAT{[a-z]+}"
cfg.save(cfg.data)
runner = CliRunner()
# Test standard output
result = runner.invoke(cli, ["flag", "-l"])
assert result.exit_code == 0
assert "Current flag format: TEST_FORMAT{[a-z]+}" in result.output
# Test plain output
result_plain = runner.invoke(cli, ["flag", "-l", "-p"])
assert result_plain.exit_code == 0
assert result_plain.output.strip() == "TEST_FORMAT{[a-z]+}"
# }}}
# {{{ test_flag_cmd_set_format
def test_flag_cmd_set_format():
"""
Verifies that ctf flag --set / -s updates the flag format in config.toml.
"""
from ctf.cli import cli
runner = CliRunner()
result = runner.invoke(cli, ["flag", "-s", "NEW_FLAG_FORMAT{[0-9]+}"])
assert result.exit_code == 0
assert "Flag format set to: NEW_FLAG_FORMAT{[0-9]+}" in result.output
updated_cfg = Config()
assert updated_cfg.data["Competition"]["flag_format"] == "NEW_FLAG_FORMAT{[0-9]+}"
# }}}

204
tests/cli/test_forensics.py Normal file
View File

@@ -0,0 +1,204 @@
# tests/cli/test_forensics.py
# {{{ imports
import struct
from pathlib import Path
from click.testing import CliRunner
from ctf.cli.forensics import forensics_group
# }}}
# Define the persistent test environment path
TEST_ENV = Path("tests/env")
# {{{ test_forensics_metadata_cli_success
def test_forensics_metadata_cli_success():
"""
Verifies that 'forensics metadata' successfully runs via the CLI and prints
the metadata in a formatted table.
"""
runner = CliRunner()
test_file = TEST_ENV / "test_file.png"
with open(test_file, "wb") as f:
f.write(b"\x89PNG\r\n\x1a\n")
result = runner.invoke(forensics_group, ["metadata", str(test_file)])
assert result.exit_code == 0
assert "Metadata: test_file.png" in result.output
assert "Size" in result.output
assert "89504E47" in result.output
assert "Detected Type" in result.output
assert "PNG Image" in result.output
# }}}
# {{{ test_nested_metadata_cli_success
def test_nested_metadata_cli_success():
"""
Verifies that 'ctf forensics metadata' successfully runs via the root CLI.
"""
from ctf.main import cli
runner = CliRunner()
test_file = TEST_ENV / "test_file_nested.png"
with open(test_file, "wb") as f:
f.write(b"\x89PNG\r\n\x1a\n")
result = runner.invoke(cli, ["forensics", "metadata", str(test_file)])
assert result.exit_code == 0
assert "Metadata: test_file_nested.png" in result.output
assert "PNG Image" in result.output
# }}}
# {{{ test_forensics_metadata_cli_missing_file
def test_forensics_metadata_cli_missing_file():
"""
Verifies that the CLI fails gracefully when a non-existent file path is specified.
"""
runner = CliRunner()
result = runner.invoke(forensics_group, ["metadata", "non_existent_file.png"])
assert result.exit_code != 0
assert "does not exist" in result.output
# }}}
# {{{ test_forensics_signatures_cli
def test_forensics_signatures_cli():
"""
Verifies that 'forensics signatures' runs successfully and displays
the list of supported file signatures.
"""
runner = CliRunner()
result = runner.invoke(forensics_group, ["signatures"])
assert result.exit_code == 0
assert "Supported Magic Signatures" in result.output
assert "PNG Image" in result.output
assert "89504E47" in result.output
# }}}
# {{{ test_exif_cli_options
def test_exif_cli_options():
"""
Verifies CLI options -i/--inode and -e/--exif work as expected.
"""
from ctf.main import cli
runner = CliRunner()
test_file = TEST_ENV / "mock_exif_image.png"
# Verify mutual exclusion error
result_err = runner.invoke(cli, ["forensics", "metadata", "-i", "-e", str(test_file)])
assert result_err.exit_code != 0
assert "Cannot specify more than one" in result_err.output
# Verify --inode hides EXIF data table
result_inode = runner.invoke(cli, ["forensics", "metadata", "-i", str(test_file)])
assert result_inode.exit_code == 0
assert "Metadata: mock_exif_image.png" in result_inode.output
assert "EXIF Metadata" not in result_inode.output
# Verify --exif hides POSIX table but displays EXIF data table
result_exif = runner.invoke(cli, ["forensics", "metadata", "-e", str(test_file)])
assert result_exif.exit_code == 0
assert "Metadata: mock_exif_image.png" not in result_exif.output
assert "EXIF Metadata" in result_exif.output
assert "mock_exif_image" in result_exif.output
assert "Make" in result_exif.output
assert "TEST" in result_exif.output
# }}}
# {{{ test_exif_cli_options_warning
def test_exif_cli_options_warning():
"""
Verifies CLI warns when no EXIF or comment is found and -e option is used.
"""
from ctf.main import cli
runner = CliRunner()
test_file = TEST_ENV / "clean_no_exif.png"
with open(test_file, "wb") as f:
f.write(b"\x89PNG\r\n\x1a\n")
result = runner.invoke(cli, ["forensics", "metadata", "-e", str(test_file)])
assert result.exit_code == 0
assert "No EXIF or comment metadata found" in result.output
# }}}
# {{{ test_exif_cli_comment_only
def test_exif_cli_comment_only():
"""
Verifies CLI displays comment when -e is used and only comment is found.
"""
from ctf.main import cli
runner = CliRunner()
comment_text = b"flag{comment_only}"
comment_len = len(comment_text) + 2
mock_jpeg = b"\xff\xd8\xff\xfe" + struct.pack(">H", comment_len) + comment_text
test_file = TEST_ENV / "comment_only.jpg"
with open(test_file, "wb") as f:
f.write(mock_jpeg)
result = runner.invoke(cli, ["forensics", "metadata", "-e", str(test_file)])
assert result.exit_code == 0
assert "Comment" in result.output
assert "flag{comment_only}" in result.output
# }}}
# {{{ test_adobe_xmp_cli
def test_adobe_xmp_cli():
"""
Verifies that the ctf inspect CLI prints the parsed XMP fields.
"""
from ctf.main import cli
runner = CliRunner()
test_file = TEST_ENV / "mock_xmp.jpg"
result = runner.invoke(cli, ["forensics", "metadata", "-e", str(test_file)])
assert result.exit_code == 0
assert "EXIF Metadata" in result.output
assert "cc:license" in result.output
assert "cGljb0NURnt0ZXN0X3htcF9mbGFnfQ==" in result.output
# }}}
# {{{ test_cli_physical_option
def test_cli_physical_option():
"""
Verifies the ctf forensics metadata CLI supports -p/--physical and mutual exclusion rules.
"""
from ctf.main import cli
runner = CliRunner()
test_file = TEST_ENV / "mock_physical.jpg"
# Mutual exclusion check
result_err = runner.invoke(cli, ["forensics", "metadata", "-p", "-e", str(test_file)])
assert result_err.exit_code != 0
assert "Cannot specify more than one" in result_err.output
# Physical view check
result_phys = runner.invoke(cli, ["forensics", "metadata", "-p", str(test_file)])
assert result_phys.exit_code == 0
assert "Physical Metadata" in result_phys.output
assert "Image Size" in result_phys.output
assert "1500x1000" in result_phys.output
assert "Metadata: mock_physical.jpg" not in result_phys.output
assert "EXIF Metadata" not in result_phys.output
# }}}
# {{{ test_metadata_decoded_hints_cli
def test_metadata_decoded_hints_cli():
"""
Verifies that metadata command successfully decodes and prints hex/base64 encoded metadata hints.
"""
from ctf.main import cli
runner = CliRunner()
# Create a mock JPEG with a base64 encoded comment: "ZmxhZ3tiNjRfbWV0YWRhdGF9" -> "flag{b64_metadata}"
comment_text = b"ZmxhZ3tiNjRfbWV0YWRhdGF9"
comment_len = len(comment_text) + 2
mock_jpeg = b"\xff\xd8\xff\xfe" + struct.pack(">H", comment_len) + comment_text + b"\xff\xd9"
test_file = TEST_ENV / "mock_encoded_comment.jpg"
with open(test_file, "wb") as f:
f.write(mock_jpeg)
result = runner.invoke(cli, ["forensics", "metadata", str(test_file)])
assert result.exit_code == 0
assert "Decoded Metadata Hints" in result.output
assert "Comment" in result.output
assert "base64" in result.output
assert "flag{b64_metadata}" in result.output
# }}}

171
tests/cli/test_steg.py Normal file
View File

@@ -0,0 +1,171 @@
# tests/cli/test_steg.py
# {{{ imports
import shutil
import subprocess
from pathlib import Path
from unittest.mock import patch, MagicMock
import click
from click.testing import CliRunner
import pytest
from ctf.steg import crack_steghide, SteghideCrackResult
# }}}
# {{{ test_crack_steghide_missing_binary
def test_crack_steghide_missing_binary():
"""
Verifies that crack_steghide returns failure if steghide is not installed.
"""
with patch("shutil.which", return_value=None):
res = crack_steghide(Path("somefile.jpg"), ["pass1", "pass2"])
assert not res.success
assert "not found on system" in res.error_message
# }}}
# {{{ test_crack_steghide_file_not_found
def test_crack_steghide_file_not_found():
"""
Verifies that crack_steghide returns failure if the input file does not exist.
"""
with patch("shutil.which", return_value="steghide"):
res = crack_steghide(Path("non_existent_file_12345.jpg"), ["pass1", "pass2"])
assert not res.success
assert "File not found" in res.error_message
# }}}
# {{{ test_crack_steghide_success
def test_crack_steghide_success():
"""
Verifies that crack_steghide returns success, the password, and payload when cracked.
"""
wordlist = ["wrong1", "correct_pass", "wrong2"]
# We want to mock subprocess.run to write a payload to the out_file (-xf)
# when the correct password is tried.
def mock_run(cmd, *args, **kwargs):
password = cmd[5]
out_file = cmd[7]
mock_res = MagicMock()
if password == "correct_pass":
mock_res.returncode = 0
Path(out_file).write_bytes(b"flag{steghide_cracked_payload}")
else:
mock_res.returncode = 1
return mock_res
with patch("shutil.which", return_value="steghide"), \
patch("subprocess.run", side_effect=mock_run), \
patch("pathlib.Path.exists", return_value=True):
res = crack_steghide(Path("mock_image.jpg"), wordlist)
assert res.success
assert res.password == "correct_pass"
assert res.payload == b"flag{steghide_cracked_payload}"
assert res.error_message is None
# }}}
# {{{ test_crack_steghide_failure
def test_crack_steghide_failure():
"""
Verifies that crack_steghide returns failure if the wordlist is exhausted.
"""
wordlist = ["wrong1", "wrong2"]
def mock_run(cmd, *args, **kwargs):
mock_res = MagicMock()
mock_res.returncode = 1
return mock_res
with patch("shutil.which", return_value="steghide"), \
patch("subprocess.run", side_effect=mock_run), \
patch("pathlib.Path.exists", return_value=True):
res = crack_steghide(Path("mock_image.jpg"), wordlist)
assert not res.success
assert res.password is None
assert res.payload is None
assert "Password not found in wordlist" in res.error_message
# }}}
# {{{ test_cli_crack_steghide_success
def test_cli_crack_steghide_success(tmp_path):
"""
Verifies that the crack-steghide CLI command prints success panel and previews payload.
"""
from ctf.main import cli
runner = CliRunner()
# Create temp wordlist file
wordlist_file = tmp_path / "words.txt"
wordlist_file.write_text("pass1\npass2\n")
mock_file = tmp_path / "mock.jpg"
mock_file.write_text("dummy")
mock_result = SteghideCrackResult(
success=True,
password="pass2",
payload=b"flag{cli_steg_preview}"
)
with patch("ctf.cli.steg.crack_steghide", return_value=mock_result):
res = runner.invoke(cli, ["steg", "crack-steghide", str(mock_file), "-w", str(wordlist_file)])
assert res.exit_code == 0
assert "Successfully cracked!" in res.output
assert "pass2" in res.output
assert "flag{cli_steg_preview}" in res.output
# }}}
# {{{ test_cli_crack_steghide_success_output_file
def test_cli_crack_steghide_success_output_file(tmp_path):
"""
Verifies that the crack-steghide CLI command writes the payload to a file when specified.
"""
from ctf.main import cli
runner = CliRunner()
wordlist_file = tmp_path / "words.txt"
wordlist_file.write_text("pass1\n")
mock_file = tmp_path / "mock.jpg"
mock_file.write_text("dummy")
out_file = tmp_path / "out.txt"
mock_result = SteghideCrackResult(
success=True,
password="pass1",
payload=b"written_file_content"
)
with patch("ctf.cli.steg.crack_steghide", return_value=mock_result):
res = runner.invoke(cli, ["steg", "crack-steghide", str(mock_file), "-w", str(wordlist_file), "-o", str(out_file)])
assert res.exit_code == 0
assert "Successfully cracked!" in res.output
assert out_file.exists()
assert out_file.read_bytes() == b"written_file_content"
# }}}
# {{{ test_cli_crack_steghide_failure
def test_cli_crack_steghide_failure(tmp_path):
"""
Verifies that the crack-steghide CLI command displays correct error message on failure.
"""
from ctf.main import cli
runner = CliRunner()
wordlist_file = tmp_path / "words.txt"
wordlist_file.write_text("pass1\n")
mock_file = tmp_path / "mock.jpg"
mock_file.write_text("dummy")
mock_result = SteghideCrackResult(
success=False,
error_message="Password not found in wordlist."
)
with patch("ctf.cli.steg.crack_steghide", return_value=mock_result):
res = runner.invoke(cli, ["steg", "crack-steghide", str(mock_file), "-w", str(wordlist_file)])
assert res.exit_code == 0
assert "Cracking failed: Password not found in wordlist" in res.output
# }}}