199 lines
5.6 KiB
Python
199 lines
5.6 KiB
Python
"""
|
|
AngelaMos | 2026
|
|
test_tasking.py
|
|
|
|
Unit tests for TaskManager queue behavior and SQLite persistence
|
|
|
|
Verifies task submission, FIFO ordering, blocking get_next behavior,
|
|
result storage and status transitions, task history queries, and queue
|
|
cleanup on beacon disconnect.
|
|
|
|
Tests:
|
|
beacon/tasking.py - TaskManager
|
|
"""
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
import aiosqlite
|
|
import pytest
|
|
|
|
from beacon.tasking import TaskManager
|
|
from core.models import CommandType, TaskRecord, TaskResult
|
|
from database import SCHEMA
|
|
|
|
|
|
@pytest.fixture
|
|
async def db(tmp_path: Path) -> aiosqlite.Connection:
|
|
"""
|
|
Provide a fresh SQLite connection with schema
|
|
"""
|
|
db_path = tmp_path / "test.db"
|
|
conn = await aiosqlite.connect(db_path)
|
|
conn.row_factory = aiosqlite.Row
|
|
await conn.executescript(SCHEMA)
|
|
await conn.execute(
|
|
"""
|
|
INSERT INTO beacons (id, hostname, os, username, pid, internal_ip, arch, first_seen, last_seen)
|
|
VALUES ('beacon-t', 'test', 'Linux', 'root', 1, '10.0.0.1', 'x86_64', '2026-01-01', '2026-01-01')
|
|
"""
|
|
)
|
|
await conn.commit()
|
|
yield conn
|
|
await conn.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def task_manager() -> TaskManager:
|
|
"""
|
|
Provide a fresh TaskManager instance
|
|
"""
|
|
return TaskManager()
|
|
|
|
|
|
def _make_task(task_id: str, beacon_id: str = "beacon-t") -> TaskRecord:
|
|
"""
|
|
Create a sample TaskRecord
|
|
"""
|
|
return TaskRecord(
|
|
id=task_id,
|
|
beacon_id=beacon_id,
|
|
command=CommandType.SHELL,
|
|
args="whoami",
|
|
)
|
|
|
|
|
|
class TestTaskManager:
|
|
"""
|
|
Verify task submission, retrieval, and result storage
|
|
"""
|
|
|
|
async def test_submit_and_get_next(
|
|
self,
|
|
task_manager: TaskManager,
|
|
db: aiosqlite.Connection,
|
|
) -> None:
|
|
"""
|
|
Submitted task can be retrieved from the queue
|
|
"""
|
|
task = _make_task("task-001")
|
|
await task_manager.submit(task, db)
|
|
|
|
retrieved = await asyncio.wait_for(
|
|
task_manager.get_next("beacon-t"), timeout=1.0
|
|
)
|
|
assert retrieved.id == "task-001"
|
|
assert retrieved.command == CommandType.SHELL
|
|
|
|
async def test_submit_persists_to_db(
|
|
self,
|
|
task_manager: TaskManager,
|
|
db: aiosqlite.Connection,
|
|
) -> None:
|
|
"""
|
|
Submitted task is persisted in SQLite
|
|
"""
|
|
task = _make_task("task-002")
|
|
await task_manager.submit(task, db)
|
|
|
|
cursor = await db.execute(
|
|
"SELECT * FROM tasks WHERE id = ?", ("task-002",)
|
|
)
|
|
row = await cursor.fetchone()
|
|
assert row is not None
|
|
assert dict(row)["command"] == "shell"
|
|
|
|
async def test_tasks_queue_in_order(
|
|
self,
|
|
task_manager: TaskManager,
|
|
db: aiosqlite.Connection,
|
|
) -> None:
|
|
"""
|
|
Tasks are dequeued in FIFO order
|
|
"""
|
|
await task_manager.submit(_make_task("first"), db)
|
|
await task_manager.submit(_make_task("second"), db)
|
|
await task_manager.submit(_make_task("third"), db)
|
|
|
|
t1 = await asyncio.wait_for(task_manager.get_next("beacon-t"), timeout=1.0)
|
|
t2 = await asyncio.wait_for(task_manager.get_next("beacon-t"), timeout=1.0)
|
|
t3 = await asyncio.wait_for(task_manager.get_next("beacon-t"), timeout=1.0)
|
|
|
|
assert [t1.id, t2.id, t3.id] == ["first", "second", "third"]
|
|
|
|
async def test_store_result(
|
|
self,
|
|
task_manager: TaskManager,
|
|
db: aiosqlite.Connection,
|
|
) -> None:
|
|
"""
|
|
Storing a result updates the task status to completed
|
|
"""
|
|
task = _make_task("task-003")
|
|
await task_manager.submit(task, db)
|
|
|
|
result = TaskResult(
|
|
id="result-001",
|
|
task_id="task-003",
|
|
output="root\n",
|
|
)
|
|
await task_manager.store_result(result, db)
|
|
|
|
cursor = await db.execute(
|
|
"SELECT status FROM tasks WHERE id = ?", ("task-003",)
|
|
)
|
|
row = await cursor.fetchone()
|
|
assert dict(row)["status"] == "completed"
|
|
|
|
async def test_get_history(
|
|
self,
|
|
task_manager: TaskManager,
|
|
db: aiosqlite.Connection,
|
|
) -> None:
|
|
"""
|
|
Task history includes task details joined with results
|
|
"""
|
|
task = _make_task("task-004")
|
|
await task_manager.submit(task, db)
|
|
|
|
result = TaskResult(
|
|
id="result-002",
|
|
task_id="task-004",
|
|
output="test output",
|
|
)
|
|
await task_manager.store_result(result, db)
|
|
|
|
history = await task_manager.get_history("beacon-t", db)
|
|
assert len(history) >= 1
|
|
entry = history[0]
|
|
assert entry["command"] == "shell"
|
|
assert entry["output"] == "test output"
|
|
|
|
async def test_get_next_blocks_until_task(
|
|
self,
|
|
task_manager: TaskManager,
|
|
db: aiosqlite.Connection,
|
|
) -> None:
|
|
"""
|
|
get_next blocks when the queue is empty and resolves when a task arrives
|
|
"""
|
|
async def delayed_submit() -> None:
|
|
await asyncio.sleep(0.1)
|
|
await task_manager.submit(_make_task("delayed"), db)
|
|
|
|
background_task = asyncio.create_task(delayed_submit())
|
|
retrieved = await asyncio.wait_for(
|
|
task_manager.get_next("beacon-t"), timeout=2.0
|
|
)
|
|
assert retrieved.id == "delayed"
|
|
await background_task
|
|
|
|
def test_remove_queue(self, task_manager: TaskManager) -> None:
|
|
"""
|
|
remove_queue cleans up the beacon queue
|
|
"""
|
|
task_manager._ensure_queue("beacon-x")
|
|
assert "beacon-x" in task_manager._queues
|
|
task_manager.remove_queue("beacon-x")
|
|
assert "beacon-x" not in task_manager._queues
|