178 lines
5.6 KiB
Python
178 lines
5.6 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from infrastructure.storage_minio import downloads_prefix, excel_key, pdf_key
|
|
|
|
|
|
def test_minio_key_helpers():
|
|
assert excel_key("abc", "input.xlsx") == "uploads/abc/input.xlsx"
|
|
assert downloads_prefix("abc") == "downloads/abc/"
|
|
assert pdf_key("abc", "900", "123") == "downloads/abc/900/123.pdf"
|
|
|
|
|
|
@pytest.fixture()
|
|
def store(tmp_path, monkeypatch):
|
|
db_url = f"sqlite:///{(tmp_path / 'bot.db').as_posix()}"
|
|
monkeypatch.setenv("DATABASE_URL", db_url)
|
|
|
|
# Recargar módulos para que el engine use SQLITE
|
|
for name in [
|
|
"infrastructure.database",
|
|
"infrastructure.store",
|
|
]:
|
|
if name in sys.modules:
|
|
importlib.reload(sys.modules[name])
|
|
else:
|
|
importlib.import_module(name)
|
|
|
|
from domain.models import JobStatus, WorkerStatus
|
|
from infrastructure import database as db
|
|
from infrastructure.store import JobStore
|
|
|
|
importlib.reload(db)
|
|
import infrastructure.store as store_mod
|
|
|
|
importlib.reload(store_mod)
|
|
return store_mod.JobStore(), JobStatus, WorkerStatus
|
|
|
|
|
|
def test_register_heartbeat_and_offline(store):
|
|
job_store, _JobStatus, WorkerStatus = store
|
|
w = job_store.register_worker("PC-01", hostname="host1")
|
|
assert w.status == WorkerStatus.ONLINE.value
|
|
assert job_store.is_worker_online("PC-01")
|
|
|
|
job_store.heartbeat("PC-01")
|
|
workers = job_store.list_workers()
|
|
assert len(workers) == 1
|
|
assert workers[0].id == "PC-01"
|
|
|
|
|
|
def test_create_job_requires_worker_and_claim_is_atomic(store):
|
|
job_store, JobStatus, _WorkerStatus = store
|
|
job_store.register_worker("PC-01")
|
|
|
|
job = job_store.create_job(
|
|
worker_id="PC-01",
|
|
excel_key="uploads/x/input.xlsx",
|
|
original_filename="datos.xlsx",
|
|
title="Lote 1",
|
|
)
|
|
assert job.status == JobStatus.PENDING.value
|
|
assert job.worker_id == "PC-01"
|
|
assert job.downloads_prefix == downloads_prefix(job.id)
|
|
|
|
claimed = job_store.claim_pending("PC-01")
|
|
assert claimed is not None
|
|
assert claimed.id == job.id
|
|
assert claimed.status == JobStatus.RUNNING.value
|
|
|
|
again = job_store.claim_pending("PC-01")
|
|
assert again is None
|
|
|
|
job_store.register_worker("PC-02")
|
|
other = job_store.claim_pending("PC-02")
|
|
assert other is None
|
|
|
|
|
|
def test_reregister_abandons_leftover_job(store):
|
|
job_store, JobStatus, _WorkerStatus = store
|
|
job_store.register_worker("PC-01")
|
|
job = job_store.create_job(
|
|
worker_id="PC-01",
|
|
excel_key="uploads/x/input.xlsx",
|
|
original_filename="datos.xlsx",
|
|
)
|
|
job_store.claim_pending("PC-01")
|
|
|
|
job_store.register_worker("PC-01")
|
|
leftover = job_store.get_job(job.id)
|
|
assert leftover is not None
|
|
assert leftover.status == JobStatus.CANCELLED.value
|
|
assert job_store.claim_pending("PC-01") is None
|
|
|
|
fresh = job_store.create_job(
|
|
worker_id="PC-01",
|
|
excel_key="uploads/y/input.xlsx",
|
|
original_filename="nuevo.xlsx",
|
|
)
|
|
claimed = job_store.claim_pending("PC-01")
|
|
assert claimed is not None
|
|
assert claimed.id == fresh.id
|
|
|
|
|
|
def test_cancel_pending_marks_cancelled(store):
|
|
job_store, JobStatus, _WorkerStatus = store
|
|
job_store.register_worker("PC-01")
|
|
job = job_store.create_job("PC-01", "uploads/j/input.xlsx", "a.xlsx")
|
|
cancelled = job_store.request_cancel(job.id)
|
|
assert cancelled is not None
|
|
assert cancelled.status == JobStatus.CANCELLED.value
|
|
assert cancelled.cancel_requested is True
|
|
|
|
|
|
def test_cancel_running_sets_flag(store):
|
|
job_store, JobStatus, _WorkerStatus = store
|
|
job_store.register_worker("PC-01")
|
|
job = job_store.create_job("PC-01", "uploads/j/input.xlsx", "a.xlsx")
|
|
job_store.claim_pending("PC-01")
|
|
flagged = job_store.request_cancel(job.id)
|
|
assert flagged is not None
|
|
assert flagged.cancel_requested is True
|
|
assert flagged.status == JobStatus.RUNNING.value
|
|
assert job_store.is_cancel_requested(job.id) is True
|
|
|
|
|
|
def test_events_and_patch_complete(store):
|
|
job_store, JobStatus, _WorkerStatus = store
|
|
job_store.register_worker("PC-01")
|
|
job = job_store.create_job("PC-01", "uploads/j/input.xlsx", "a.xlsx")
|
|
job_store.claim_pending("PC-01")
|
|
e1 = job_store.add_event(
|
|
job.id, "radicado_saved", "ok", radicado="1", nit="9", index=1, total=2
|
|
)
|
|
assert e1.id >= 1
|
|
events = job_store.list_events(job.id, since_id=0)
|
|
assert len(events) == 1
|
|
|
|
done = job_store.patch_job(
|
|
job.id,
|
|
status=JobStatus.COMPLETED.value,
|
|
result={"exitosos": 1},
|
|
)
|
|
assert done is not None
|
|
assert done.status == JobStatus.COMPLETED.value
|
|
worker = job_store.get_worker("PC-01")
|
|
assert worker is not None
|
|
assert worker.current_job_id is None
|
|
|
|
|
|
def test_list_jobs_by_parent(store):
|
|
job_store, _JobStatus, _WorkerStatus = store
|
|
from infrastructure.database import Job as IpsJob
|
|
from infrastructure.database import session_scope, utcnow
|
|
|
|
job_store.register_worker("PC-01")
|
|
with session_scope() as session:
|
|
session.add(
|
|
IpsJob(
|
|
id="parent-1",
|
|
status="pending",
|
|
title="Trabajo",
|
|
job_metadata={},
|
|
created_at=utcnow(),
|
|
updated_at=utcnow(),
|
|
)
|
|
)
|
|
|
|
a = job_store.create_job("PC-01", "uploads/a/input.xlsx", "a.xlsx", job_id="parent-1")
|
|
job_store.create_job("PC-01", "uploads/b/input.xlsx", "b.xlsx", job_id="other")
|
|
listed = job_store.list_jobs(limit=50, job_id="parent-1")
|
|
assert len(listed) == 1
|
|
assert listed[0].id == a.id
|