2026-06-10 20:28:49 +02:00
|
|
|
"""Unit tests for apply_task_to_simulation() mapping helper — §0.11 contract."""
|
2026-06-10 19:56:06 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
|
|
|
|
from backend.app.services.c2.mapping import apply_task_to_simulation
|
|
|
|
|
|
|
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
def _make_task(
|
|
|
|
|
command: str = "whoami",
|
|
|
|
|
output: str | None = "root",
|
|
|
|
|
mapping_applied: bool = False,
|
|
|
|
|
completed_at: datetime | None = None,
|
|
|
|
|
) -> MagicMock:
|
2026-06-10 19:56:06 +02:00
|
|
|
task = MagicMock()
|
2026-06-10 20:28:49 +02:00
|
|
|
task.command = command
|
2026-06-10 19:56:06 +02:00
|
|
|
task.output = output
|
|
|
|
|
task.mapping_applied = mapping_applied
|
2026-06-10 20:28:49 +02:00
|
|
|
task.completed_at = completed_at
|
2026-06-10 19:56:06 +02:00
|
|
|
return task
|
|
|
|
|
|
|
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
def _make_sim(
|
|
|
|
|
execution_result: str | None = None,
|
|
|
|
|
executed_at: datetime | None = None,
|
|
|
|
|
commands: str | None = None,
|
|
|
|
|
) -> MagicMock:
|
2026-06-10 19:56:06 +02:00
|
|
|
sim = MagicMock()
|
|
|
|
|
sim.execution_result = execution_result
|
2026-06-10 20:28:49 +02:00
|
|
|
sim.executed_at = executed_at
|
|
|
|
|
sim.commands = commands
|
2026-06-10 19:56:06 +02:00
|
|
|
sim.updated_at = None
|
|
|
|
|
return sim
|
|
|
|
|
|
|
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
class TestExecutionResult:
|
|
|
|
|
def test_first_task_produces_command_block(self):
|
|
|
|
|
task = _make_task(command="whoami", output="root")
|
|
|
|
|
sim = _make_sim()
|
2026-06-10 19:56:06 +02:00
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
assert sim.execution_result == "$ whoami\nroot\n"
|
2026-06-10 19:56:06 +02:00
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
def test_second_task_appended_with_block_separator(self):
|
|
|
|
|
"""Two tasks → two '$ command\noutput\n' blocks separated by a single newline."""
|
|
|
|
|
sim = _make_sim()
|
|
|
|
|
t1 = _make_task(command="whoami", output="root")
|
|
|
|
|
t2 = _make_task(command="hostname", output="lab-1")
|
2026-06-10 19:56:06 +02:00
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
apply_task_to_simulation(t1, sim)
|
|
|
|
|
apply_task_to_simulation(t2, sim)
|
2026-06-10 19:56:06 +02:00
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
assert sim.execution_result == "$ whoami\nroot\n$ hostname\nlab-1\n"
|
2026-06-10 19:56:06 +02:00
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
def test_no_double_blank_line_when_existing_ends_with_newline(self):
|
|
|
|
|
"""If existing result already ends with \n, no extra blank line is inserted."""
|
|
|
|
|
sim = _make_sim(execution_result="$ id\nuid=0\n")
|
|
|
|
|
task = _make_task(command="hostname", output="lab-1")
|
2026-06-10 19:56:06 +02:00
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
assert sim.execution_result == "$ id\nuid=0\n$ hostname\nlab-1\n"
|
2026-06-10 19:56:06 +02:00
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
def test_empty_output_skips_block_but_marks_applied(self):
|
2026-06-10 19:56:06 +02:00
|
|
|
task = _make_task(output="")
|
2026-06-10 20:28:49 +02:00
|
|
|
sim = _make_sim(execution_result="$ id\nuid=0\n")
|
2026-06-10 19:56:06 +02:00
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
assert sim.execution_result == "$ id\nuid=0\n"
|
2026-06-10 19:56:06 +02:00
|
|
|
assert task.mapping_applied is True
|
|
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
def test_none_output_skips_block_but_marks_applied(self):
|
2026-06-10 19:56:06 +02:00
|
|
|
task = _make_task(output=None)
|
2026-06-10 20:28:49 +02:00
|
|
|
sim = _make_sim()
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.execution_result is None
|
|
|
|
|
assert task.mapping_applied is True
|
|
|
|
|
|
|
|
|
|
def test_command_with_empty_string_produces_dollar_header(self):
|
|
|
|
|
"""Empty command → block header is '$ \n<output>\n' (consistent, not suppressed)."""
|
|
|
|
|
task = _make_task(command="", output="some output")
|
|
|
|
|
sim = _make_sim()
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.execution_result == "$ \nsome output\n" or sim.execution_result == "$ \nsome output\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestExecutedAt:
|
|
|
|
|
def test_sets_executed_at_from_task_when_null(self):
|
|
|
|
|
ts = datetime(2026, 6, 10, 12, 0, 0, tzinfo=UTC)
|
|
|
|
|
task = _make_task(completed_at=ts)
|
|
|
|
|
sim = _make_sim(executed_at=None)
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.executed_at == ts
|
|
|
|
|
|
|
|
|
|
def test_does_not_overwrite_existing_executed_at(self):
|
|
|
|
|
original_ts = datetime(2026, 6, 1, 0, 0, 0, tzinfo=UTC)
|
|
|
|
|
later_ts = datetime(2026, 6, 10, 12, 0, 0, tzinfo=UTC)
|
|
|
|
|
task = _make_task(completed_at=later_ts)
|
|
|
|
|
sim = _make_sim(executed_at=original_ts)
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.executed_at == original_ts
|
|
|
|
|
|
|
|
|
|
def test_executed_at_stays_null_when_task_completed_at_is_none(self):
|
|
|
|
|
task = _make_task(completed_at=None)
|
|
|
|
|
sim = _make_sim(executed_at=None)
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.executed_at is None
|
|
|
|
|
|
|
|
|
|
def test_first_task_sets_executed_at_second_does_not_overwrite(self):
|
|
|
|
|
ts1 = datetime(2026, 6, 10, 10, 0, 0, tzinfo=UTC)
|
|
|
|
|
ts2 = datetime(2026, 6, 10, 11, 0, 0, tzinfo=UTC)
|
|
|
|
|
t1 = _make_task(command="whoami", output="root", completed_at=ts1)
|
|
|
|
|
t2 = _make_task(command="hostname", output="lab-1", completed_at=ts2)
|
|
|
|
|
sim = _make_sim(executed_at=None)
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(t1, sim)
|
|
|
|
|
apply_task_to_simulation(t2, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.executed_at == ts1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCommandsDedup:
|
|
|
|
|
def test_appends_command_to_empty_commands(self):
|
|
|
|
|
task = _make_task(command="whoami", output="root")
|
|
|
|
|
sim = _make_sim(commands=None)
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.commands == "whoami"
|
|
|
|
|
|
|
|
|
|
def test_appends_second_distinct_command(self):
|
|
|
|
|
sim = _make_sim(commands=None)
|
|
|
|
|
t1 = _make_task(command="whoami", output="root")
|
|
|
|
|
t2 = _make_task(command="hostname", output="lab-1")
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(t1, sim)
|
|
|
|
|
apply_task_to_simulation(t2, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.commands == "whoami\nhostname"
|
|
|
|
|
|
|
|
|
|
def test_deduplicates_repeated_command(self):
|
|
|
|
|
sim = _make_sim(commands=None)
|
|
|
|
|
t1 = _make_task(command="whoami", output="root")
|
|
|
|
|
t2 = _make_task(command="whoami", output="root2")
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(t1, sim)
|
|
|
|
|
apply_task_to_simulation(t2, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.commands == "whoami"
|
|
|
|
|
|
|
|
|
|
def test_dedup_is_case_and_whitespace_stripped(self):
|
|
|
|
|
sim = _make_sim(commands="whoami")
|
|
|
|
|
task = _make_task(command=" whoami ", output="root")
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
|
|
|
|
# " whoami ".strip() == "whoami" which is already present → no append.
|
|
|
|
|
assert sim.commands == "whoami"
|
|
|
|
|
|
|
|
|
|
def test_empty_command_not_appended(self):
|
|
|
|
|
task = _make_task(command="", output="output")
|
|
|
|
|
sim = _make_sim(commands=None)
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
|
|
|
|
# task.command is falsy → commands block skipped.
|
|
|
|
|
assert sim.commands is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestIdempotency:
|
|
|
|
|
def test_no_op_when_mapping_already_applied(self):
|
|
|
|
|
task = _make_task(output="root", mapping_applied=True)
|
2026-06-10 19:56:06 +02:00
|
|
|
sim = _make_sim(execution_result="existing")
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.execution_result == "existing"
|
|
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
def test_always_marks_mapping_applied(self):
|
|
|
|
|
task = _make_task(output="root")
|
|
|
|
|
sim = _make_sim()
|
2026-06-10 19:56:06 +02:00
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
assert task.mapping_applied is True
|
2026-06-10 19:56:06 +02:00
|
|
|
|
2026-06-10 20:28:49 +02:00
|
|
|
def test_updated_at_is_set(self):
|
|
|
|
|
task = _make_task(output="root")
|
|
|
|
|
sim = _make_sim()
|
2026-06-10 19:56:06 +02:00
|
|
|
before = datetime.now(UTC)
|
|
|
|
|
|
|
|
|
|
apply_task_to_simulation(task, sim)
|
|
|
|
|
|
|
|
|
|
assert sim.updated_at is not None
|
|
|
|
|
assert sim.updated_at >= before
|