fix: tasks should require timer when there are no handler support.
This commit is contained in:
parent
7ac7039592
commit
b438947b0d
5 changed files with 384 additions and 11 deletions
|
|
@ -151,6 +151,22 @@ def _serialize(model: Any) -> dict:
|
||||||
return _model(model).model_dump()
|
return _model(model).model_dump()
|
||||||
|
|
||||||
|
|
||||||
|
async def _require_timer_or_handler(task: Task, handler: TaskHandle) -> None:
|
||||||
|
if task.timer:
|
||||||
|
return
|
||||||
|
|
||||||
|
if task.handler_enabled is False:
|
||||||
|
msg = "Task requires a timer when the handler is disabled."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
result = await handler.inspect(url=task.url, preset=task.preset, static_only=True)
|
||||||
|
if isinstance(result, TaskResult) and result.metadata.get("matched") is True:
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = "Task requires a timer when no supported handler matches the URL."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
@route("GET", "api/tasks/", "tasks_list")
|
@route("GET", "api/tasks/", "tasks_list")
|
||||||
async def tasks_list(request: Request, repo: TasksRepository, encoder: Encoder) -> Response:
|
async def tasks_list(request: Request, repo: TasksRepository, encoder: Encoder) -> Response:
|
||||||
page, per_page = normalize_pagination(request)
|
page, per_page = normalize_pagination(request)
|
||||||
|
|
@ -166,7 +182,13 @@ async def tasks_list(request: Request, repo: TasksRepository, encoder: Encoder)
|
||||||
|
|
||||||
|
|
||||||
@route("POST", "api/tasks/", "tasks_add")
|
@route("POST", "api/tasks/", "tasks_add")
|
||||||
async def tasks_add(request: Request, repo: TasksRepository, encoder: Encoder, notify: EventBus) -> Response:
|
async def tasks_add(
|
||||||
|
request: Request,
|
||||||
|
repo: TasksRepository,
|
||||||
|
encoder: Encoder,
|
||||||
|
notify: EventBus,
|
||||||
|
handler: TaskHandle,
|
||||||
|
) -> Response:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
|
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
|
|
@ -205,6 +227,7 @@ async def tasks_add(request: Request, repo: TasksRepository, encoder: Encoder, n
|
||||||
status=web.HTTPConflict.status_code,
|
status=web.HTTPConflict.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pending_tasks: list[dict[str, Any]] = []
|
||||||
created_tasks: list[dict[str, Any]] = []
|
created_tasks: list[dict[str, Any]] = []
|
||||||
base_settings = first_task.model_dump(exclude={"id", "name", "url", "created_at", "updated_at"})
|
base_settings = first_task.model_dump(exclude={"id", "name", "url", "created_at", "updated_at"})
|
||||||
|
|
||||||
|
|
@ -243,11 +266,23 @@ async def tasks_add(request: Request, repo: TasksRepository, encoder: Encoder, n
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validated = Task.model_validate(task_dict)
|
validated = Task.model_validate(task_dict)
|
||||||
task_data = validated.model_dump()
|
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
LOG.warning(f"Skipping task {idx}: validation failed - {exc}")
|
return web.json_response(
|
||||||
continue
|
data={"error": "Failed to validate task.", "detail": format_validation_errors(exc)},
|
||||||
|
status=web.HTTPBadRequest.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await _require_timer_or_handler(validated, handler)
|
||||||
|
except ValueError as exc:
|
||||||
|
return web.json_response(
|
||||||
|
data={"error": str(exc)},
|
||||||
|
status=web.HTTPBadRequest.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
pending_tasks.append(validated.model_dump())
|
||||||
|
|
||||||
|
for idx, task_data in enumerate(pending_tasks):
|
||||||
try:
|
try:
|
||||||
created = await repo.create(task_data)
|
created = await repo.create(task_data)
|
||||||
saved = _serialize(created)
|
saved = _serialize(created)
|
||||||
|
|
@ -304,7 +339,13 @@ async def tasks_delete(request: Request, repo: TasksRepository, encoder: Encoder
|
||||||
|
|
||||||
|
|
||||||
@route("PATCH", r"api/tasks/{id:\d+}", "tasks_patch")
|
@route("PATCH", r"api/tasks/{id:\d+}", "tasks_patch")
|
||||||
async def tasks_patch(request: Request, repo: TasksRepository, encoder: Encoder, notify: EventBus) -> Response:
|
async def tasks_patch(
|
||||||
|
request: Request,
|
||||||
|
repo: TasksRepository,
|
||||||
|
encoder: Encoder,
|
||||||
|
notify: EventBus,
|
||||||
|
handler: TaskHandle,
|
||||||
|
) -> Response:
|
||||||
if not (identifier := request.match_info.get("id")):
|
if not (identifier := request.match_info.get("id")):
|
||||||
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
|
@ -334,6 +375,16 @@ async def tasks_patch(request: Request, repo: TasksRepository, encoder: Encoder,
|
||||||
status=web.HTTPConflict.status_code,
|
status=web.HTTPConflict.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
merged = _model(model).model_copy(update=validated.model_dump(exclude_unset=True))
|
||||||
|
|
||||||
|
try:
|
||||||
|
await _require_timer_or_handler(merged, handler)
|
||||||
|
except ValueError as exc:
|
||||||
|
return web.json_response(
|
||||||
|
data={"error": str(exc)},
|
||||||
|
status=web.HTTPBadRequest.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
updated = _serialize(await repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
updated = _serialize(await repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
||||||
notify.emit(Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.TASKS, action=CEAction.UPDATE, data=updated))
|
notify.emit(Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.TASKS, action=CEAction.UPDATE, data=updated))
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
|
|
@ -344,7 +395,13 @@ async def tasks_patch(request: Request, repo: TasksRepository, encoder: Encoder,
|
||||||
|
|
||||||
|
|
||||||
@route("PUT", r"api/tasks/{id:\d+}", "tasks_update")
|
@route("PUT", r"api/tasks/{id:\d+}", "tasks_update")
|
||||||
async def tasks_update(request: Request, repo: TasksRepository, encoder: Encoder, notify: EventBus) -> Response:
|
async def tasks_update(
|
||||||
|
request: Request,
|
||||||
|
repo: TasksRepository,
|
||||||
|
encoder: Encoder,
|
||||||
|
notify: EventBus,
|
||||||
|
handler: TaskHandle,
|
||||||
|
) -> Response:
|
||||||
if not (identifier := request.match_info.get("id")):
|
if not (identifier := request.match_info.get("id")):
|
||||||
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
return web.json_response({"error": "ID required"}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
|
|
@ -374,6 +431,14 @@ async def tasks_update(request: Request, repo: TasksRepository, encoder: Encoder
|
||||||
status=web.HTTPConflict.status_code,
|
status=web.HTTPConflict.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await _require_timer_or_handler(validated, handler)
|
||||||
|
except ValueError as exc:
|
||||||
|
return web.json_response(
|
||||||
|
data={"error": str(exc)},
|
||||||
|
status=web.HTTPBadRequest.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
updated = _serialize(await repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
updated = _serialize(await repo.update(model.id, validated.model_dump(exclude_unset=True)))
|
||||||
notify.emit(Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.TASKS, action=CEAction.UPDATE, data=updated))
|
notify.emit(Events.CONFIG_UPDATE, data=ConfigEvent(feature=CEFeature.TASKS, action=CEAction.UPDATE, data=updated))
|
||||||
|
|
||||||
|
|
|
||||||
183
app/features/tasks/tests/test_tasks_router.py
Normal file
183
app/features/tasks/tests/test_tasks_router.py
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
from aiohttp import web
|
||||||
|
from aiohttp.test_utils import make_mocked_request
|
||||||
|
|
||||||
|
from app.features.tasks import router
|
||||||
|
from app.features.tasks.definitions.results import TaskFailure, TaskResult
|
||||||
|
from app.features.tasks.repository import TasksRepository
|
||||||
|
from app.library.encoder import Encoder
|
||||||
|
from app.library.sqlite_store import SqliteStore
|
||||||
|
from app.tests.helpers import make_in_memory_db_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def repo():
|
||||||
|
TasksRepository._reset_singleton()
|
||||||
|
SqliteStore._reset_singleton()
|
||||||
|
|
||||||
|
store = SqliteStore(db_path=make_in_memory_db_path("tasks-router"))
|
||||||
|
await store.get_connection()
|
||||||
|
|
||||||
|
repository = TasksRepository.get_instance()
|
||||||
|
|
||||||
|
yield repository
|
||||||
|
|
||||||
|
await store.close()
|
||||||
|
TasksRepository._reset_singleton()
|
||||||
|
SqliteStore._reset_singleton()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def patch_get_info(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
async def _fake_get_info(_url: str, _preset: str) -> tuple[None, None]:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
monkeypatch.setattr(router, "_get_info", _fake_get_info)
|
||||||
|
|
||||||
|
|
||||||
|
def _json_request(method: str, path: str, payload: object, *, match_info: dict[str, str] | None = None) -> web.Request:
|
||||||
|
request = make_mocked_request(method, path, match_info=match_info or {}, app=SimpleNamespace())
|
||||||
|
|
||||||
|
async def _json() -> object:
|
||||||
|
return payload
|
||||||
|
|
||||||
|
request.json = _json # type: ignore[attr-defined]
|
||||||
|
return request
|
||||||
|
|
||||||
|
|
||||||
|
class _Notify:
|
||||||
|
def emit(self, *_args, **_kwargs) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class _Handler:
|
||||||
|
def __init__(self, matched: bool | dict[str, bool]) -> None:
|
||||||
|
self._matched = matched
|
||||||
|
|
||||||
|
async def inspect(self, *, url: str, preset: str | None = None, static_only: bool = False, **_kwargs):
|
||||||
|
del preset, static_only
|
||||||
|
|
||||||
|
if isinstance(self._matched, dict):
|
||||||
|
matched = self._matched.get(url, False)
|
||||||
|
else:
|
||||||
|
matched = self._matched
|
||||||
|
|
||||||
|
if matched:
|
||||||
|
return TaskResult(metadata={"matched": True, "handler": "TestHandler"})
|
||||||
|
|
||||||
|
return TaskFailure(message="No handler", metadata={"matched": False, "handler": None})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_requires_timer_without_handler(repo) -> None:
|
||||||
|
request = _json_request(
|
||||||
|
"POST",
|
||||||
|
"/api/tasks/",
|
||||||
|
{"name": "No Timer", "url": "https://example.com/channel"},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await router.tasks_add(request, repo, Encoder(), _Notify(), _Handler(matched=False))
|
||||||
|
|
||||||
|
assert response.status == web.HTTPBadRequest.status_code
|
||||||
|
assert b"requires a timer" in response.body
|
||||||
|
assert await repo.list() == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_all_or_nothing(repo) -> None:
|
||||||
|
request = _json_request(
|
||||||
|
"POST",
|
||||||
|
"/api/tasks/",
|
||||||
|
[
|
||||||
|
{"name": "First", "url": "https://example.com/first"},
|
||||||
|
{"url": "https://example.com/second"},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await router.tasks_add(
|
||||||
|
request,
|
||||||
|
repo,
|
||||||
|
Encoder(),
|
||||||
|
_Notify(),
|
||||||
|
_Handler({"https://example.com/first": True, "https://example.com/second": False}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status == web.HTTPBadRequest.status_code
|
||||||
|
assert b"requires a timer" in response.body
|
||||||
|
assert await repo.list() == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_allows_handler_only(repo) -> None:
|
||||||
|
request = _json_request(
|
||||||
|
"POST",
|
||||||
|
"/api/tasks/",
|
||||||
|
{"name": "Handler Only", "url": "https://example.com/feed"},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await router.tasks_add(request, repo, Encoder(), _Notify(), _Handler(matched=True))
|
||||||
|
|
||||||
|
assert response.status == web.HTTPOk.status_code
|
||||||
|
items = await repo.list()
|
||||||
|
assert len(items) == 1
|
||||||
|
assert items[0].name == "Handler Only"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_requires_timer_without_handler(repo) -> None:
|
||||||
|
item = await repo.create({"name": "Needs Timer", "url": "https://example.com/a", "timer": "0 0 * * *"})
|
||||||
|
|
||||||
|
request = _json_request(
|
||||||
|
"PUT",
|
||||||
|
f"/api/tasks/{item.id}",
|
||||||
|
{"name": item.name, "url": item.url, "timer": "", "preset": "", "folder": "", "template": "", "cli": ""},
|
||||||
|
match_info={"id": str(item.id)},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await router.tasks_update(request, repo, Encoder(), _Notify(), _Handler(matched=False))
|
||||||
|
|
||||||
|
assert response.status == web.HTTPBadRequest.status_code
|
||||||
|
refreshed = await repo.get(item.id)
|
||||||
|
assert refreshed is not None
|
||||||
|
assert refreshed.timer == "0 0 * * *"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_patch_requires_timer_without_handler(repo) -> None:
|
||||||
|
item = await repo.create({"name": "Patch Timer", "url": "https://example.com/b", "timer": "0 0 * * *"})
|
||||||
|
|
||||||
|
request = _json_request(
|
||||||
|
"PATCH",
|
||||||
|
f"/api/tasks/{item.id}",
|
||||||
|
{"timer": ""},
|
||||||
|
match_info={"id": str(item.id)},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await router.tasks_patch(request, repo, Encoder(), _Notify(), _Handler(matched=False))
|
||||||
|
|
||||||
|
assert response.status == web.HTTPBadRequest.status_code
|
||||||
|
refreshed = await repo.get(item.id)
|
||||||
|
assert refreshed is not None
|
||||||
|
assert refreshed.timer == "0 0 * * *"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_patch_requires_timer_when_handler_disabled(repo) -> None:
|
||||||
|
item = await repo.create({"name": "Disabled Handler", "url": "https://example.com/c", "timer": "0 0 * * *"})
|
||||||
|
|
||||||
|
request = _json_request(
|
||||||
|
"PATCH",
|
||||||
|
f"/api/tasks/{item.id}",
|
||||||
|
{"timer": "", "handler_enabled": False},
|
||||||
|
match_info={"id": str(item.id)},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await router.tasks_patch(request, repo, Encoder(), _Notify(), _Handler(matched=True))
|
||||||
|
|
||||||
|
assert response.status == web.HTTPBadRequest.status_code
|
||||||
|
assert b"handler is disabled" in response.body
|
||||||
|
|
@ -367,7 +367,9 @@
|
||||||
<UIcon name="i-lucide-rss" class="size-4 text-toned" />
|
<UIcon name="i-lucide-rss" class="size-4 text-toned" />
|
||||||
<p class="text-sm font-semibold text-default">Enable Handler</p>
|
<p class="text-sm font-semibold text-default">Enable Handler</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-toned">Handlers run regardless of task timer.</p>
|
<p class="text-xs text-toned">
|
||||||
|
Tasks without a supported handler must use a timer.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<USwitch v-model="form.handler_enabled" :disabled="addInProgress" />
|
<USwitch v-model="form.handler_enabled" :disabled="addInProgress" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -443,6 +445,10 @@
|
||||||
scheduled downloads to yt-dlp. Disable <code>Enable Handler</code> to disable RSS
|
scheduled downloads to yt-dlp. Disable <code>Enable Handler</code> to disable RSS
|
||||||
monitoring.
|
monitoring.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Timer Requirement:</strong> if no supported handler matches the URL, you must
|
||||||
|
set a CRON timer before saving.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
</UAlert>
|
</UAlert>
|
||||||
|
|
@ -523,6 +529,7 @@ const emitter = defineEmits<{
|
||||||
const toast = useNotification();
|
const toast = useNotification();
|
||||||
const config = useYtpConfig();
|
const config = useYtpConfig();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
|
const tasksComposable = useTasks();
|
||||||
const { findPreset, getPresetDefault, selectItems } = usePresetOptions();
|
const { findPreset, getPresetDefault, selectItems } = usePresetOptions();
|
||||||
const showImport = useStorage('showTaskImport', false);
|
const showImport = useStorage('showTaskImport', false);
|
||||||
|
|
||||||
|
|
@ -786,6 +793,13 @@ const checkInfo = async (): Promise<void> => {
|
||||||
form.cli = form.cli.trim();
|
form.cli = form.cli.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await requireTimerForTask(form);
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (urls.length === 1) {
|
if (urls.length === 1) {
|
||||||
emitter('submit', {
|
emitter('submit', {
|
||||||
reference: toRaw(props.reference),
|
reference: toRaw(props.reference),
|
||||||
|
|
@ -811,12 +825,32 @@ const checkInfo = async (): Promise<void> => {
|
||||||
} as Task;
|
} as Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { url } as Task;
|
return {
|
||||||
|
url,
|
||||||
|
preset: form.preset,
|
||||||
|
timer: form.timer,
|
||||||
|
handler_enabled: form.handler_enabled,
|
||||||
|
} as Task;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(tasks.map((item) => requireTimerForTask(item)));
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitTasks: Task[] = tasks.map((item, idx) => {
|
||||||
|
if (idx === 0) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { url: item.url } as Task;
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter('submit', {
|
emitter('submit', {
|
||||||
reference: toRaw(props.reference),
|
reference: toRaw(props.reference),
|
||||||
task: tasks,
|
task: submitTasks,
|
||||||
archive_all: archiveAllAfterAdd.value,
|
archive_all: archiveAllAfterAdd.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -943,6 +977,34 @@ const convertCurrentUrl = async (): Promise<void> => {
|
||||||
form.url = await convert_url(form.url);
|
form.url = await convert_url(form.url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const requireTimerForTask = async (
|
||||||
|
item: Pick<Task, 'url' | 'preset' | 'timer' | 'handler_enabled'>,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (item.timer?.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.handler_enabled === false) {
|
||||||
|
throw new Error('This task needs a CRON timer because the handler is disabled.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await tasksComposable.inspectTaskHandler({
|
||||||
|
url: item.url,
|
||||||
|
preset: item.preset,
|
||||||
|
static_only: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Failed to verify handler support. Set a CRON timer or try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.matched) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('This task needs a CRON timer because no supported handler matches the URL.');
|
||||||
|
};
|
||||||
|
|
||||||
const getDefault = (type: 'cookies' | 'cli' | 'template' | 'folder', ret: string = '') => {
|
const getDefault = (type: 'cookies' | 'cli' | 'template' | 'folder', ret: string = '') => {
|
||||||
if (false !== hasFormatInConfig.value || !form.preset) {
|
if (false !== hasFormatInConfig.value || !form.preset) {
|
||||||
return ret;
|
return ret;
|
||||||
|
|
|
||||||
|
|
@ -661,8 +661,8 @@
|
||||||
<code>Actions > Archive All</code> to mark existing items as already downloaded.
|
<code>Actions > Archive All</code> to mark existing items as already downloaded.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Custom Handlers:</strong> Leave timer empty for custom handler definitions. The
|
<strong>Timer Requirement:</strong> Tasks must have either a valid timer or a supported
|
||||||
handler runs hourly and doesn't require timer.
|
handler. If the URL does not match a handler, set a CRON timer.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Generate metadata:</strong> will attempt first to save metadata based on the
|
<strong>Generate metadata:</strong> will attempt first to save metadata based on the
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,29 @@ describe('useTasks', () => {
|
||||||
requestSpy.mockRestore()
|
requestSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('store_timer_required_error', async () => {
|
||||||
|
const requestSpy = spyOn(utils, 'request')
|
||||||
|
requestSpy.mockResolvedValueOnce(
|
||||||
|
createMockResponse({
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
jsonData: { error: 'Task requires a timer when no supported handler matches the URL.' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const tasks = useTasks()
|
||||||
|
const newTask = { ...mockTask }
|
||||||
|
delete (newTask as any).id
|
||||||
|
|
||||||
|
const result = await tasks.createTask(newTask)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
expect(tasks.lastError.value).toBe(
|
||||||
|
'Task requires a timer when no supported handler matches the URL.',
|
||||||
|
)
|
||||||
|
requestSpy.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('updateTask', () => {
|
describe('updateTask', () => {
|
||||||
|
|
@ -280,6 +303,26 @@ describe('useTasks', () => {
|
||||||
requestSpy.mockRestore()
|
requestSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('store_update_timer_error', async () => {
|
||||||
|
const requestSpy = spyOn(utils, 'request')
|
||||||
|
requestSpy.mockResolvedValueOnce(
|
||||||
|
createMockResponse({
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
jsonData: { error: 'Task requires a timer when the handler is disabled.' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const tasks = useTasks()
|
||||||
|
const updated = { ...mockTask, timer: '', handler_enabled: false }
|
||||||
|
|
||||||
|
const result = await tasks.updateTask(1, updated)
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
expect(tasks.lastError.value).toBe('Task requires a timer when the handler is disabled.')
|
||||||
|
requestSpy.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
it('strip_update_id', async () => {
|
it('strip_update_id', async () => {
|
||||||
const requestSpy = spyOn(utils, 'request')
|
const requestSpy = spyOn(utils, 'request')
|
||||||
requestSpy.mockResolvedValueOnce(
|
requestSpy.mockResolvedValueOnce(
|
||||||
|
|
@ -318,6 +361,26 @@ describe('useTasks', () => {
|
||||||
requestSpy.mockRestore()
|
requestSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('store_patch_timer_error', async () => {
|
||||||
|
const requestSpy = spyOn(utils, 'request')
|
||||||
|
requestSpy.mockResolvedValueOnce(
|
||||||
|
createMockResponse({
|
||||||
|
ok: false,
|
||||||
|
status: 400,
|
||||||
|
jsonData: { error: 'Task requires a timer when no supported handler matches the URL.' },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const tasks = useTasks()
|
||||||
|
const result = await tasks.patchTask(1, { timer: '' })
|
||||||
|
|
||||||
|
expect(result).toBeNull()
|
||||||
|
expect(tasks.lastError.value).toBe(
|
||||||
|
'Task requires a timer when no supported handler matches the URL.',
|
||||||
|
)
|
||||||
|
requestSpy.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deleteTask', () => {
|
describe('deleteTask', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue