mirror of
https://github.com/Comfy-Org/ComfyUI-Manager.git
synced 2026-05-09 08:32:30 +08:00
Defense-in-depth over GET→POST alone: reject the three CORS-safelisted simple-form Content-Types (x-www-form-urlencoded, multipart/form-data, text/plain) on 16 no-body POST handlers (glob + legacy) to block <form method=POST> CSRF that bypasses method-only gating. Move comfyui_switch_version to a JSON body so the preflight requirement applies. Split db_mode/policy/update/channel_url_list into GET(read) + POST(write). Tighten do_fix (high → high+) and gate three previously-ungated config setters at middle. Resynchronize openapi.yaml (27 paths, 30 operations, ComfyUISwitchVersionParams as a shared $ref component). Add E2E harness variants, Playwright config, CSRF/secgate suites, 39-endpoint coverage, and a CHANGELOG. Breaking: legacy per-op POST routes (install/uninstall/fix/disable/update/ reinstall/abort_current) are removed; callers already use queue/batch. Legacy /manager/notice (v1) is removed; /v2/manager/notice is retained. Reported-by: XlabAI Team of Tencent Xuanwu Lab CVSS: 8.1 (AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H)
455 lines
34 KiB
Markdown
455 lines
34 KiB
Markdown
# Report B — E2E Test Inventory + Coverage Mapping
|
||
|
||
**Generated**: 2026-04-18
|
||
**Source directories**:
|
||
- `tests/e2e/*.py` (pytest — HTTP + CLI E2E)
|
||
- `tests/playwright/*.spec.ts` (Playwright — legacy UI E2E)
|
||
|
||
## Summary
|
||
|
||
| Category | Files | Test Functions |
|
||
|---|---:|---:|
|
||
| pytest E2E (HTTP API) | 13 | 92 |
|
||
| pytest E2E (CLI — uv-compile) | 1 | 12 |
|
||
| Playwright (legacy UI) | 6 | 21 |
|
||
| Playwright (debug) | 1 | 1 |
|
||
| **TOTAL** | **21** | **126** |
|
||
|
||
> **Note**: +1 file / +4 functions vs prior counts reflect inclusion of `tests/e2e/test_e2e_csrf.py` (CSRF-mitigation contract suite, commit 99caef55). That suite's 4 test functions parametrize to 26 pytest invocations (13+2+11) after WI-HH removed 3 dual-purpose endpoints from the reject-GET fixture (they legitimately answer GET on the read-path and are covered only in the allow-GET class).
|
||
>
|
||
> **WI-Z Y2 sync (2026-04-19)**: Playwright legacy UI count 21 → 19 reflected Stage2 WI-F deletion of two `legacy-ui-navigation.spec.ts` tests (`API health check while dialogs are open`, `system endpoints accessible from browser context`) that were rewritten as direct-API violators; their coverage is now owned by `test_e2e_system_info.py`. TOTAL 119 → 117 was a downstream correction.
|
||
>
|
||
> **WI-AA sync (2026-04-19)**: Playwright legacy UI count 19 → 21 reflects addition of `tests/playwright/legacy-ui-install.spec.ts` (2 tests: LB1 Install button + LB2 Uninstall button) previously implemented but not inventoried. TOTAL 117 → 119. See dedicated subsection below.
|
||
>
|
||
> **WI-GG sync (2026-04-20)**: pytest E2E (HTTP API) file count 10 → 11 and function count 85 → 89 reflects addition of `tests/e2e/test_e2e_csrf_legacy.py` (4 new test functions / 26 parametrized invocations: 13 reject-GET + 2 POST-works + 11 allow-GET) from WI-FF. TOTAL 119 → 123 test functions. The legacy suite is the counterpart to `test_e2e_csrf.py` for the `--enable-manager-legacy-ui` server variant — required because `comfyui_manager/__init__.py` loads `glob.manager_server` XOR `legacy.manager_server`. See dedicated subsection below. (Accounting note: the higher-level audit `reports/e2e_verification_audit.md` Summary Matrix renders the 26 legacy invocations per-row, reaching TOTAL 143; both counts refer to the same underlying tests at different granularities — function-level here, invocation-level there.)
|
||
>
|
||
> **WI-LL sync (2026-04-20)**: pytest E2E (HTTP API) file count 11 → 13 and function count 89 → 92 reflects addition of two new SECGATE-coverage files (WI-KK deliverables, audit-integrated by WI-LL): `tests/e2e/test_e2e_secgate_strict.py` (strict-mode harness + SR4 PoC — 2 functions: `test_remove_returns_403` PASS + `test_post_works_at_default_after_restore` pytest.skip'd positive counterpart stub) + `tests/e2e/test_e2e_secgate_default.py` (default-mode demo + CV4 — 1 function: `test_switch_version_returns_403_at_default` PASS). TOTAL 123 → 126 test functions. These close 2 of the original 8 T2 SECGATE-PENDING Goals (SR4, CV4) and establish the strict-mode harness pattern (`start_comfyui_strict.sh` + config.ini backup/restore) for the remaining T2-pending-harness-ready Goals (SR6, V5, UA2). See the Classification policy block in `e2e_verification_audit.md` for the reclassification and propagation plan.
|
||
|
||
**Unique endpoints exercised**: 27 (glob v2) + 4 (legacy-only: queue/batch indirectly via UI)
|
||
|
||
---
|
||
|
||
# Section 1 — pytest E2E HTTP Tests
|
||
|
||
## tests/e2e/test_e2e_endpoint.py (7 tests)
|
||
|
||
Covers the main install/uninstall flow via `/v2/manager/queue/task` and `/v2/customnode/installed`.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestEndpointInstallUninstall::test_install_via_endpoint` | POST queue/task, POST queue/start | Install CNR pack (success) | pack dir exists + .tracking file present |
|
||
| `TestEndpointInstallUninstall::test_installed_list_shows_pack` | GET customnode/installed | Pack appears in installed list | cnr_id match in dict values |
|
||
| `TestEndpointInstallUninstall::test_uninstall_via_endpoint` | POST queue/task kind=uninstall | Uninstall success | pack dir removed from disk |
|
||
| `TestEndpointInstallUninstall::test_installed_list_after_uninstall` | GET customnode/installed | Post-uninstall state | cnr_id absent from installed list |
|
||
| `TestEndpointInstallUninstall::test_install_uninstall_cycle` | queue/task x2 | Full install→verify→uninstall cycle | All above assertions in one test |
|
||
| `TestEndpointStartup::test_comfyui_started` | GET /system_stats | Server health | 200 response |
|
||
| `TestEndpointStartup::test_startup_resolver_ran` | (log file) | UnifiedDepResolver ran at startup | log contains `[UnifiedDepResolver]` + "startup batch resolution succeeded" |
|
||
|
||
## tests/e2e/test_e2e_git_clone.py (3 tests)
|
||
|
||
Covers nightly (URL-based) install via `/v2/manager/queue/task` which triggers git_helper.py subprocess.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestNightlyInstallCycle::test_01_nightly_install` | POST queue/task selected_version=nightly | git clone via Manager API | pack dir exists + .git directory present |
|
||
| `TestNightlyInstallCycle::test_02_no_module_error` | (log file) | No ModuleNotFoundError regression | log does not contain "ModuleNotFoundError" |
|
||
| `TestNightlyInstallCycle::test_03_nightly_uninstall` | POST queue/task kind=uninstall | Uninstall nightly pack | pack dir removed |
|
||
|
||
## tests/cli/test_uv_compile.py — RELOCATED (WI-PP)
|
||
|
||
Previously tracked here as `tests/e2e/test_e2e_uv_compile.py`. Moved to
|
||
`tests/cli/` in WI-PP because every test in the suite drives cm-cli as a
|
||
subprocess; none of them exercise HTTP endpoints. The 8 tests (post
|
||
WI-MM/NN/OO consolidation) continue to cover install / reinstall (xfail-marked
|
||
for purge_node_state) / verbs-with-uv-compile (parametrized ×5) / uv-sync
|
||
no-packs-exits-zero / no-packs-emits-signal / with-packs / conflict
|
||
attribution with specs. CI runner updated in `.github/workflows/e2e.yml` to
|
||
point at the new path.
|
||
|
||
## tests/e2e/test_e2e_config_api.py (10 tests)
|
||
|
||
Covers GET/POST round-trip on configuration endpoints.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestConfigDbMode::test_read_db_mode` | GET db_mode | Read current value | text in {cache, channel, local, remote} |
|
||
| `TestConfigDbMode::test_set_and_restore_db_mode` | GET/POST db_mode | Set→read-back→restore | POST 200 + verify echo + restore verified |
|
||
| `TestConfigDbMode::test_set_db_mode_invalid_body` | POST db_mode | Malformed JSON | 400 |
|
||
| `TestConfigUpdatePolicy::test_read_update_policy` | GET policy/update | Read current policy | text in {stable, stable-comfyui, nightly, nightly-comfyui} |
|
||
| `TestConfigUpdatePolicy::test_set_and_restore_update_policy` | GET/POST policy/update | Set→read-back→restore | Round-trip verification |
|
||
| `TestConfigUpdatePolicy::test_set_policy_invalid_body` | POST policy/update | Malformed JSON | 400 |
|
||
| `TestConfigChannelUrlList::test_read_channel_url_list` | GET channel_url_list | Response shape | has `selected` (str) + `list` (array) |
|
||
| `TestConfigChannelUrlList::test_channel_list_entries_are_name_url_strings` | GET channel_url_list | Entry format | each entry is "name::url" string |
|
||
| `TestConfigChannelUrlList::test_set_and_restore_channel` | GET/POST channel_url_list | Switch channel + restore | Verify `selected` matches set value |
|
||
| `TestConfigChannelUrlList::test_set_channel_invalid_body` | POST channel_url_list | Malformed JSON | 400 |
|
||
|
||
## tests/e2e/test_e2e_customnode_info.py (11 tests)
|
||
|
||
Covers custom node info/mapping endpoints.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestCustomNodeMappings::test_getmappings_returns_dict` | GET customnode/getmappings?mode=local | Success response | 200 + dict |
|
||
| `TestCustomNodeMappings::test_getmappings_entries_have_node_lists` | GET getmappings | Entry structure | each value is `[node_list, metadata]` |
|
||
| `TestFetchUpdates::test_fetch_updates_returns_deprecated` | GET customnode/fetch_updates | Deprecated endpoint | 410 + `deprecated: true` |
|
||
| `TestInstalledPacks::test_installed_returns_dict` | GET customnode/installed | Success | 200 + dict |
|
||
| `TestInstalledPacks::test_installed_imported_mode` | GET installed?mode=imported | Startup snapshot | 200 + dict |
|
||
| `TestImportFailInfo::test_unknown_cnr_id_returns_400` | POST import_fail_info | Unknown pack | 400 |
|
||
| `TestImportFailInfo::test_missing_fields_returns_400` | POST import_fail_info | Missing cnr_id+url | 400 |
|
||
| `TestImportFailInfo::test_invalid_body_returns_error` | POST import_fail_info | Non-dict body | 400 |
|
||
| `TestImportFailInfoBulk::test_bulk_with_cnr_ids_returns_dict` | POST import_fail_info_bulk | cnr_ids list | 200 + null for unknown |
|
||
| `TestImportFailInfoBulk::test_bulk_empty_lists_returns_400` | POST import_fail_info_bulk | Empty cnr_ids+urls | 400 |
|
||
| `TestImportFailInfoBulk::test_bulk_with_urls_returns_dict` | POST import_fail_info_bulk | urls list | 200 + dict |
|
||
|
||
## tests/e2e/test_e2e_queue_lifecycle.py (9 tests)
|
||
|
||
Covers the queue management lifecycle.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestQueueLifecycle::test_reset_queue` | POST queue/reset | Empty the queue | 200 |
|
||
| `TestQueueLifecycle::test_status_after_reset` | GET queue/status | Post-reset state | all counts 0, is_processing bool |
|
||
| `TestQueueLifecycle::test_status_with_client_id_filter` | GET queue/status?client_id=X | Client filter | response echoes client_id |
|
||
| `TestQueueLifecycle::test_start_queue_already_idle` | POST queue/start | Idle worker start | status in {200, 201} |
|
||
| `TestQueueLifecycle::test_queue_task_and_history` | POST queue/task + queue/start + GET queue/status + GET queue/history | Full lifecycle | done_count>0 polled, history 200 or 400 |
|
||
| `TestQueueLifecycle::test_history_with_ui_id_filter` | GET queue/history?ui_id=X | Filter history | 200 or 400 (serialization-limit) |
|
||
| `TestQueueLifecycle::test_history_with_pagination` | GET queue/history?max_items=1&offset=0 | Pagination | 200 or 400 |
|
||
| `TestQueueLifecycle::test_history_list` | GET queue/history_list | List batch IDs | 200 + `ids` list |
|
||
| `TestQueueLifecycle::test_final_reset_and_clean_state` | POST queue/reset + GET queue/status | Cleanup | pending_count==0 |
|
||
|
||
## tests/e2e/test_e2e_snapshot_lifecycle.py (7 tests)
|
||
|
||
Covers snapshot save/list/remove cycle.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestSnapshotLifecycle::test_get_current_snapshot` | GET snapshot/get_current | Current state dict | 200 + dict |
|
||
| `TestSnapshotLifecycle::test_save_snapshot` | POST snapshot/save | Save new snapshot | 200 |
|
||
| `TestSnapshotLifecycle::test_getlist_after_save` | GET snapshot/getlist | List contains new snapshot | items.length > 0 |
|
||
| `TestSnapshotLifecycle::test_remove_snapshot` | POST snapshot/remove?target=X + GET getlist | Remove + verify | target absent + count decremented |
|
||
| `TestSnapshotLifecycle::test_remove_nonexistent_snapshot` | POST snapshot/remove | Nonexistent target | 200 (no-op) |
|
||
| `TestSnapshotLifecycle::test_remove_path_traversal_rejected` | POST snapshot/remove?target=../... | Path-traversal targets must be rejected | 400 + sentinel file outside snapshot dir preserved (SR3) |
|
||
| `TestSnapshotGetCurrentSchema::test_getlist_items_are_strings` | GET snapshot/getlist | Items shape | each item is string |
|
||
|
||
> Note: `POST /v2/snapshot/restore` intentionally NOT tested (destructive).
|
||
|
||
## tests/e2e/test_e2e_system_info.py (4 tests)
|
||
|
||
Covers system-level endpoints (version, legacy UI flag, reboot).
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestManagerVersion::test_version_returns_string` | GET manager/version | Non-empty string | 200 + len>0 |
|
||
| `TestManagerVersion::test_version_is_stable` | GET manager/version x2 | Idempotency | consecutive calls return same value |
|
||
| `TestIsLegacyManagerUI::test_returns_boolean_field` | GET is_legacy_manager_ui | Response shape | `{is_legacy_manager_ui: bool}` |
|
||
| `TestReboot::test_reboot_and_recovery` | POST manager/reboot + GET version | Restart + recovery | 200 or 403 (security); server polls healthy; version unchanged |
|
||
|
||
## tests/e2e/test_e2e_task_operations.py (16 tests)
|
||
|
||
Covers queue/task operations for kinds NOT tested in test_e2e_endpoint.py.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestDisableEnable::test_disable_pack` | POST queue/task kind=disable | Disable moves pack to .disabled/ | pack dir gone + .disabled/ entry present |
|
||
| `TestDisableEnable::test_enable_pack` | POST queue/task kind=enable | Enable restores pack | pack dir present + .disabled/ entry gone |
|
||
| `TestDisableEnable::test_disable_enable_cycle` | queue/task x2 | Full disable→enable | Both transitions verified |
|
||
| `TestUpdatePack::test_update_installed_pack` | POST queue/task kind=update | Update pack | pack still exists after update |
|
||
| `TestUpdatePack::test_update_history_recorded` | GET queue/history?ui_id=X | History has update entry | 200 or 400 (serialization-limit) |
|
||
| `TestFixPack::test_fix_installed_pack` | POST queue/task kind=fix | Fix pack | pack still exists |
|
||
| `TestFixPack::test_fix_history_recorded` | GET queue/history?ui_id=X | History has fix entry | 200 or 400 |
|
||
| `TestInstallModel::test_install_model_accepts_valid_request` | POST queue/install_model | Valid model request | 200 (reset queue after) |
|
||
| `TestInstallModel::test_install_model_missing_client_id` | POST queue/install_model | Missing client_id | 400 |
|
||
| `TestInstallModel::test_install_model_missing_ui_id` | POST queue/install_model | Missing ui_id | 400 |
|
||
| `TestInstallModel::test_install_model_invalid_body` | POST queue/install_model | Invalid metadata | 400 |
|
||
| `TestUpdateAll::test_update_all_queues_tasks` | POST queue/update_all | Queue all update tasks | 200/403 or tolerated ReadTimeout |
|
||
| `TestUpdateAll::test_update_all_missing_params` | POST queue/update_all | Missing params | 400 ValidationError |
|
||
| `TestUpdateComfyUI::test_update_comfyui_queues_task` | POST queue/update_comfyui | Queue task | 200 + total_count>=1 after |
|
||
| `TestUpdateComfyUI::test_update_comfyui_missing_params` | POST queue/update_comfyui | Missing params | 400 |
|
||
| `TestUpdateComfyUI::test_update_comfyui_with_stable_flag` | POST queue/update_comfyui?stable=true | Explicit stable flag | 200 |
|
||
|
||
## tests/e2e/test_e2e_version_mgmt.py (7 tests)
|
||
|
||
Covers comfyui_versions + comfyui_switch_version endpoints.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestComfyUIVersions::test_versions_endpoint` | GET comfyui_versions | Response shape | `{versions: list, current: str}` |
|
||
| `TestComfyUIVersions::test_versions_list_not_empty` | GET comfyui_versions | Non-empty list | len>0 |
|
||
| `TestComfyUIVersions::test_versions_items_are_strings` | GET comfyui_versions | Item type | each version is string |
|
||
| `TestComfyUIVersions::test_current_is_in_versions` | GET comfyui_versions | Current in list | current appears in versions |
|
||
| `TestSwitchVersionNegative::test_switch_version_missing_all_params` | POST comfyui_switch_version | No params | 400 or 403 |
|
||
| `TestSwitchVersionNegative::test_switch_version_missing_client_id` | POST comfyui_switch_version?ver=X | Partial params | 400 or 403 |
|
||
| `TestSwitchVersionNegative::test_switch_version_validation_error_body` | POST comfyui_switch_version | Error body shape | `error` field present (when 400 JSON) |
|
||
|
||
> Note: Actual version switching (destructive) intentionally NOT tested.
|
||
|
||
---
|
||
|
||
## tests/e2e/test_e2e_csrf.py (4 test functions / 26 parametrized invocations — post-WI-HH)
|
||
|
||
Covers the CSRF-mitigation contract from commit 99caef55 — state-changing
|
||
endpoints must reject HTTP GET so that `<img src>` / link-click /
|
||
redirect-based cross-origin triggers cannot mutate server state.
|
||
|
||
**Scope (per docstring)**: ONLY the GET-rejection contract. NOT covered
|
||
here: Origin/Referer validation (separate middleware), same-site cookies,
|
||
anti-CSRF tokens, cross-site form POST.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestStateChangingEndpointsRejectGet::test_get_is_rejected[path]` | 13 POST endpoints (queue/start, queue/reset, queue/update_all, queue/update_comfyui, queue/install_model, queue/task, snapshot/save, snapshot/remove, snapshot/restore, manager/reboot, comfyui_switch_version, import_fail_info, import_fail_info_bulk — WI-HH removed db_mode, policy/update, channel_url_list from this list since they legitimately answer GET on the read-path) | GET must reject | status_code in (400,403,404,405); explicit `not in 200-399` guard |
|
||
| `TestCsrfPostWorks::test_queue_reset_post_works` | POST queue/reset | POST counterpart works | status_code == 200 |
|
||
| `TestCsrfPostWorks::test_snapshot_save_post_works` | POST snapshot/save + cleanup via getlist+remove | POST counterpart works | status_code == 200; cleanup |
|
||
| `TestCsrfReadEndpointsStillAllowGet::test_get_read_endpoint_succeeds[path]` | 11 GET endpoints (version, db_mode, policy/update, channel_url_list, queue/status, queue/history_list, is_legacy_manager_ui, customnode/installed, snapshot/getlist, snapshot/get_current, comfyui_versions) | Negative control: read-only still works | status_code == 200 |
|
||
|
||
> Note: Three endpoints (`db_mode`, `policy/update`, `channel_url_list`) appear in BOTH reject-GET (POST path, write) and allow-GET (read path) lists — commit 99caef55 split each into a GET-read + POST-write pair; the POST path must reject GET while the GET path must continue to succeed.
|
||
|
||
---
|
||
|
||
## tests/e2e/test_e2e_csrf_legacy.py (4 test functions / 26 parametrized invocations)
|
||
|
||
Legacy-mode counterpart to `test_e2e_csrf.py`. Verifies the same CSRF
|
||
method-rejection contract but against the legacy server module loaded
|
||
via `--enable-manager-legacy-ui`. Added in WI-FF (commit following
|
||
99caef55) to close the legacy-side regression-guard gap. Audit-integrated
|
||
in WI-GG.
|
||
|
||
**Why a separate file** (per docstring L7–13): `comfyui_manager/__init__.py`
|
||
loads `glob.manager_server` XOR `legacy.manager_server` via mutex on the
|
||
`--enable-manager-legacy-ui` flag. One ComfyUI process exposes either the
|
||
glob or the legacy route table, never both — so verifying the legacy
|
||
CSRF contract requires its own module-scoped server lifecycle with the
|
||
legacy flag set (via `start_comfyui_legacy.sh`).
|
||
|
||
**Scope (per docstring L44–48)**: Same as `test_e2e_csrf.py` — ONLY the
|
||
method-reject layer. Origin/Referer, same-site cookies, anti-CSRF tokens,
|
||
and cross-site form POST are out of scope.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestLegacyStateChangingEndpointsRejectGet::test_get_is_rejected[path]` | 13 POST endpoints (queue/start, queue/reset, queue/update_all, queue/update_comfyui, queue/install_model, **queue/batch** (legacy; replaces queue/task), snapshot/save, snapshot/remove, snapshot/restore, manager/reboot, comfyui_switch_version, import_fail_info, import_fail_info_bulk) | GET must reject under legacy server | status_code in (400,403,404,405); explicit `not in 200-399` guard |
|
||
| `TestLegacyCsrfPostWorks::test_queue_reset_post_works` | POST queue/reset (legacy) | POST counterpart works under legacy server | status_code == 200 |
|
||
| `TestLegacyCsrfPostWorks::test_snapshot_save_post_works` | POST snapshot/save + cleanup via getlist+remove (legacy) | POST counterpart works + cleanup | status_code == 200; cleanup |
|
||
| `TestLegacyCsrfReadEndpointsStillAllowGet::test_get_read_endpoint_succeeds[path]` | 11 GET endpoints (version, db_mode, policy/update, channel_url_list, queue/status, queue/history_list, is_legacy_manager_ui, customnode/installed, snapshot/getlist, snapshot/get_current, comfyui_versions) | Negative control: legacy read-only still works | status_code == 200 |
|
||
|
||
> **Endpoint-list deltas vs glob** (per docstring L23–36):
|
||
> - `queue/task` → dropped (glob-only); `queue/batch` → added (legacy task-enqueue equivalent)
|
||
> - `db_mode`, `policy/update`, `channel_url_list` → dropped from reject-GET (CSRF contract applies only to the POST write-path; legacy splits these into `@routes.get` read + `@routes.post` write, identical to glob). These 3 remain in the ALLOW-GET class above. (The glob `test_e2e_csrf.py` lists them in BOTH classes; WI-HH tracks the glob-side correction.)
|
||
|
||
---
|
||
|
||
## tests/e2e/test_e2e_secgate_strict.py (1 test active + 1 skipped; WI-KK PoC, WI-LL audit-integrated)
|
||
|
||
Strict-mode security-gate PoC. Covers the middle/middle+ gate 403 contract for
|
||
Goals that require elevating `security_level=strong`. Launches via
|
||
`start_comfyui_strict.sh` (which patches `user/__manager/config.ini` to
|
||
`security_level=strong`, leaves a `.before-strict` backup, and starts the server
|
||
on the E2E port) and restores the original config in the fixture teardown.
|
||
|
||
**Scope (per docstring L3–9)**: strict-mode 403 path for the middle/middle+
|
||
gates. The default E2E config (`security_level=normal`, `is_local_mode=True`)
|
||
puts NORMAL inside the allowed set for both gates per
|
||
`comfyui_manager/glob/utils/security_utils.py` L32–38, so this harness is the
|
||
only way to exercise the 403 side. This is the first of 4 planned Goals
|
||
(SR4 ← here; SR6, V5, UA2 ← mechanical additions using the same fixture).
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestSecurityGate403_SR4::test_remove_returns_403` | POST `/v2/snapshot/remove?target=…` (under security_level=strong) | Goal SR4 — snapshot/remove below `middle` | (a) `status_code == 403`; (b) the seeded snapshot file on disk is NOT deleted (negative-check per `verification_design.md` §7.3 Security Boundary Template). |
|
||
| `TestSecurityGate403_SR4::test_post_works_at_default_after_restore` | (none — pytest.skip) | Positive counterpart of SR4 at default config | pytest.skip'd: deferred to `test_e2e_secgate_default.py` follow-up to avoid double-startup cost. Documents both halves of the gate contract. |
|
||
|
||
**Harness notes**:
|
||
- **Teardown ordering** is contract-critical: stop server FIRST, then restore config (the server holds the config-file lock; restoring before stopping causes a re-snapshot race). Documented in the fixture's `finally` block.
|
||
- Subsequent test modules continue to see `security_level=normal` because the backup restore happens deterministically in teardown.
|
||
|
||
## tests/e2e/test_e2e_secgate_default.py (1 test; WI-KK demo, WI-LL audit-integrated)
|
||
|
||
Default-mode security-gate demonstration. Covers the CV4 Goal (comfyui_switch_version
|
||
`high+` gate 403 contract) without any harness, leveraging the WI-KK research
|
||
finding that default `security_level=normal` + `is_local_mode=True` already
|
||
triggers 403 for high+ operations at the HTTP handler. This is the cleanest of
|
||
the 4 originally-classified-T2 high+ Goals to demonstrate the no-harness-needed
|
||
insight.
|
||
|
||
**Scope (per docstring L1–18)**: only the CV4 Goal. The other 3 originally-T2
|
||
high+ Goals are deferred with reclassification notes:
|
||
- **IM4** → **T2-TASKLEVEL**: non-safetensors check lives deep in the install pipeline (worker + `get_risky_level`), not at the HTTP handler. POST `/v2/manager/queue/install_model` accepts the request and queues a task; rejection only surfaces at task execution. Requires a queue-observation pattern, not a simple HTTP 403 check.
|
||
- **LGU2**, **LPP2** → **NORMAL-legacy**: registered ONLY in `legacy/manager_server.py` (L1502, L1522). Testing needs `start_comfyui_legacy.sh` fixture — follow-up `test_e2e_secgate_legacy_default.py` is the natural home.
|
||
|
||
| Test | Endpoint(s) | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `TestSecurityGate403_CV4::test_switch_version_returns_403_at_default` | POST `/v2/comfyui_manager/comfyui_switch_version` with `ver`, `client_id`, `ui_id` (at default security_level) | Goal CV4 — comfyui_switch_version below `high+` | `status_code == 403`. The `ver` query is syntactically valid so the request WOULD reach the Pydantic validation step IF the gate were broken; since the gate is the FIRST check in the handler, 403 must precede any 400-from-validation outcome. |
|
||
|
||
---
|
||
|
||
# Section 2 — Playwright UI Tests
|
||
|
||
All Playwright tests require ComfyUI running with `--enable-manager-legacy-ui` on PORT (default 8199).
|
||
|
||
## tests/playwright/legacy-ui-manager-menu.spec.ts (5 tests)
|
||
|
||
Covers the Manager Menu dialog and its settings dropdowns.
|
||
|
||
| Test | Endpoint(s) exercised | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `Manager Menu Dialog > opens via Manager button and shows 3-column layout` | (indirect: initial page + legacy UI detection) | Menu dialog opens | `#cm-manager-dialog` visible; "Custom Nodes Manager", "Model Manager", "Restart" buttons present |
|
||
| `> shows settings dropdowns (DB, Channel, Policy)` | (UI) | DB + Policy combos render | Both `<select>` elements visible |
|
||
| `> DB mode dropdown round-trips via API` | GET/POST db_mode | UI dropdown change → backend persists | selectOption → verify via GET → restore |
|
||
| `> Update Policy dropdown round-trips via API` | GET/POST policy/update | Policy change via UI | selectOption → verify GET → restore |
|
||
| `> closes and reopens without duplicating` | (UI only) | Dialog lifecycle | No duplicate dialog instances |
|
||
|
||
## tests/playwright/legacy-ui-custom-nodes.spec.ts (5 tests)
|
||
|
||
Covers the Custom Nodes Manager dialog (TurboGrid-based list).
|
||
|
||
| Test | Endpoint(s) exercised | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `Custom Nodes Manager > opens from Manager menu and renders grid` | GET customnode/getlist (legacy), customnode/getmappings | Grid render | `#cn-manager-dialog` + `.tg-body` visible |
|
||
| `> loads custom node list (non-empty)` | GET customnode/getlist | Data load | rows > 0 after polling |
|
||
| `> filter dropdown changes displayed nodes` | (client-side filter) | "Installed" filter | filtered count ≤ initial count |
|
||
| `> search input filters the grid` | (client-side filter) | Search term | filtered count ≤ initial |
|
||
| `> footer buttons are present` | (UI) | Install via Git URL / Restart buttons | At least one present |
|
||
|
||
## tests/playwright/legacy-ui-model-manager.spec.ts (4 tests)
|
||
|
||
Covers Model Manager dialog.
|
||
|
||
| Test | Endpoint(s) exercised | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `Model Manager > opens from Manager menu and renders grid` | GET externalmodel/getlist (legacy) | Grid render | `#cmm-manager-dialog` + grid visible |
|
||
| `> loads model list (non-empty)` | GET externalmodel/getlist | Data load | rows > 0 |
|
||
| `> search input filters the model grid` | (client-side filter) | Search | filtered ≤ initial |
|
||
| `> filter dropdown is present with expected options` | (UI) | Filter options | options.length > 0 |
|
||
|
||
## tests/playwright/legacy-ui-snapshot.spec.ts (3 tests)
|
||
|
||
Covers Snapshot Manager dialog.
|
||
|
||
| Test | Endpoint(s) exercised | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `Snapshot Manager > opens snapshot manager from Manager menu` | (UI) | Dialog opens | `#snapshot-manager-dialog` present |
|
||
| `> lists existing snapshots` | GET snapshot/getlist | List loads | resp.ok + `items` property |
|
||
| `> save snapshot via API and verify in list` | POST snapshot/save + GET snapshot/getlist + POST snapshot/remove | Save→verify→cleanup | items.length > 0 after save; remove cleanup |
|
||
|
||
## tests/playwright/legacy-ui-navigation.spec.ts (2 tests)
|
||
|
||
Covers dialog navigation lifecycle. Stage2 WI-F deleted two prior tests (`API health check while dialogs are open`, `system endpoints accessible from browser context`) because they exercised `page.request.*` direct API calls with no real UI interaction — coverage is now owned by `test_e2e_system_info.py::test_version_*` and related pytest suites.
|
||
|
||
| Test | Endpoint(s) exercised | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `Dialog Navigation > Manager menu → Custom Nodes → close → Manager still visible` | (UI) | Nested dialog navigation | Manager menu reopens after child close |
|
||
| `> Manager menu → Model Manager → close → reopen` | (UI) | Close and reopen Model Manager | Dialog reappears |
|
||
|
||
## tests/playwright/legacy-ui-install.spec.ts (2 tests)
|
||
|
||
Covers UI-driven install/uninstall effect verification against the test pack `ComfyUI_SigmoidOffsetScheduler`. Primary action is always a UI button click; `page.request` is used only for setup (queue/reset baseline, optional API pre-install in LB2) and effect-observation (queue/status polling, installed-list lookup) — consistent with the hybrid UI-action + backend-effect pattern in `legacy-ui-snapshot.spec.ts::SS1`.
|
||
|
||
| Test | Endpoint(s) exercised | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `UI-driven install/uninstall > LB1 Install button triggers install effect` | (UI) Custom Nodes Manager dialog → filter "Not Installed" → search pack → row Install button → version "Select" button; GET /v2/manager/queue/status (effect polling), GET /v2/customnode/installed (effect verification) | User initiates install from Custom Nodes dialog | `isPackInstalled === true` after queue drains via `waitForAllDone` |
|
||
| `UI-driven install/uninstall > LB2 Uninstall button triggers uninstall effect` | (UI) Custom Nodes Manager dialog → filter "Installed" → search pack → row Uninstall button → optional confirm dialog; GET /v2/manager/queue/status, GET /v2/customnode/installed | User initiates uninstall from Custom Nodes dialog (preconditioned by API install if pack absent) | `isPackInstalled === false` after queue drains |
|
||
|
||
## tests/playwright/debug-install-flow.spec.ts (1 test)
|
||
|
||
Debug/instrumentation test — captures the install API flow for documentation.
|
||
|
||
| Test | Endpoint(s) exercised | Scenario | Assertion semantics |
|
||
|---|---|---|---|
|
||
| `capture install button API flow` | GET customnode/getlist, POST queue/batch (legacy), GET customnode/versions/{id}, WebSocket cm-queue-status | End-to-end install UI flow capture | No assertions — logs API sequence + WebSocket frames for manual review |
|
||
|
||
---
|
||
|
||
# Section 3 — Endpoint Coverage Summary
|
||
|
||
## Glob v2 endpoints covered (27/30)
|
||
|
||
| Endpoint | Covered by |
|
||
|---|---|
|
||
| POST queue/task (install) | test_e2e_endpoint, test_e2e_git_clone, test_e2e_task_operations |
|
||
| POST queue/task (update/fix/disable/enable/uninstall) | test_e2e_endpoint, test_e2e_task_operations |
|
||
| GET queue/history_list | test_e2e_queue_lifecycle |
|
||
| GET queue/history | test_e2e_queue_lifecycle, test_e2e_task_operations |
|
||
| GET customnode/getmappings | test_e2e_customnode_info |
|
||
| GET customnode/fetch_updates | test_e2e_customnode_info (deprecated 410) |
|
||
| POST queue/update_all | test_e2e_task_operations |
|
||
| GET is_legacy_manager_ui | test_e2e_system_info, playwright legacy-ui-navigation |
|
||
| GET customnode/installed | test_e2e_endpoint, test_e2e_customnode_info |
|
||
| GET snapshot/getlist | test_e2e_snapshot_lifecycle, playwright legacy-ui-snapshot |
|
||
| POST snapshot/remove | test_e2e_snapshot_lifecycle |
|
||
| GET snapshot/get_current | test_e2e_snapshot_lifecycle |
|
||
| POST snapshot/save | test_e2e_snapshot_lifecycle, playwright legacy-ui-snapshot |
|
||
| POST customnode/import_fail_info | test_e2e_customnode_info |
|
||
| POST customnode/import_fail_info_bulk | test_e2e_customnode_info |
|
||
| POST queue/reset | test_e2e_queue_lifecycle, test_e2e_task_operations |
|
||
| GET queue/status | test_e2e_queue_lifecycle, test_e2e_task_operations |
|
||
| POST queue/start | test_e2e_endpoint, test_e2e_task_operations |
|
||
| POST queue/update_comfyui | test_e2e_task_operations |
|
||
| GET comfyui_versions | test_e2e_version_mgmt |
|
||
| POST comfyui_switch_version | test_e2e_version_mgmt (negative only) |
|
||
| POST queue/install_model | test_e2e_task_operations |
|
||
| GET/POST db_mode | test_e2e_config_api, playwright legacy-ui-manager-menu |
|
||
| GET/POST policy/update | test_e2e_config_api, playwright legacy-ui-manager-menu |
|
||
| GET/POST channel_url_list | test_e2e_config_api |
|
||
| POST manager/reboot | test_e2e_system_info |
|
||
| GET manager/version | test_e2e_system_info, playwright legacy-ui-navigation |
|
||
|
||
## CSRF Method-Reject Contract
|
||
|
||
Separate from the positive-path coverage above, the 16 state-changing POST
|
||
endpoints (glob) + 13 (legacy, with queue/batch substitution) plus 11
|
||
read-only GET endpoints per server are independently verified for their
|
||
CSRF-mitigation contract (commit 99caef55, CVSS 8.1). Coverage is split
|
||
across two files because server loading is mutex on `--enable-manager-legacy-ui`:
|
||
|
||
**Glob server** — `tests/e2e/test_e2e_csrf.py`:
|
||
|
||
| Contract | Tests | Coverage |
|
||
|---|---|---|
|
||
| 13 POST endpoints must reject HTTP GET (glob; post-WI-HH) | TestStateChangingEndpointsRejectGet (parametrized ×13) | ✓ full |
|
||
| POST counterparts must work (glob sanity) | TestCsrfPostWorks (queue/reset, snapshot/save) | ~ spot-check |
|
||
| 11 read-only GET endpoints must still allow GET (glob negative control) | TestCsrfReadEndpointsStillAllowGet (parametrized ×11) | ✓ full |
|
||
|
||
**Legacy server** (WI-FF) — `tests/e2e/test_e2e_csrf_legacy.py`:
|
||
|
||
| Contract | Tests | Coverage |
|
||
|---|---|---|
|
||
| 13 POST endpoints must reject HTTP GET (legacy; queue/task→queue/batch; dual-purpose endpoints scoped to ALLOW-GET only) | TestLegacyStateChangingEndpointsRejectGet (parametrized ×13) | ✓ full |
|
||
| POST counterparts must work (legacy sanity) | TestLegacyCsrfPostWorks (queue/reset, snapshot/save) | ~ spot-check |
|
||
| 11 read-only GET endpoints must still allow GET (legacy negative control) | TestLegacyCsrfReadEndpointsStillAllowGet (parametrized ×11) | ✓ full |
|
||
|
||
Note: this contract is NEGATIVE-assertion (must-reject) + negative-control.
|
||
Do NOT interpret CSRF-suite PASS as "CSRF fully solved" — both suites
|
||
explicitly scope themselves to the method-conversion layer only. The
|
||
legacy suite closes the gap where a reverted `@routes.post` → `@routes.get`
|
||
in `legacy/manager_server.py` would have slipped past CI.
|
||
|
||
## Glob v2 endpoints NOT covered
|
||
|
||
| Endpoint | Reason |
|
||
|---|---|
|
||
| POST snapshot/restore | Intentionally skipped (destructive — alters node state) |
|
||
| POST comfyui_switch_version (positive) | Intentionally skipped (destructive — alters ComfyUI version) |
|
||
| (none otherwise missing) | — |
|
||
|
||
## Legacy-only endpoints covered
|
||
|
||
| Endpoint | Covered by |
|
||
|---|---|
|
||
| POST queue/batch | playwright debug-install-flow (indirect — triggered via Install UI) |
|
||
| GET customnode/getlist | playwright legacy-ui-custom-nodes (indirect) |
|
||
| GET externalmodel/getlist | playwright legacy-ui-model-manager (indirect) |
|
||
|
||
## Legacy-only endpoints NOT covered
|
||
|
||
| Endpoint | Reason |
|
||
|---|---|
|
||
| GET /customnode/alternatives | Not invoked by legacy UI flows tested |
|
||
| GET customnode/versions/{node_name} | Tested indirectly via install version dialog (debug-install-flow) but no direct assertion |
|
||
| GET customnode/disabled_versions/{node_name} | No direct test |
|
||
| POST customnode/install/git_url | High+ security, destructive; not in UI flow |
|
||
| POST customnode/install/pip | High+ security, destructive |
|
||
| GET manager/notice | Removed in recent work; legacy only |
|
||
|
||
---
|
||
*End of Report B*
|