Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

200 lines
8.1 KiB
Python

import logging
from unittest.mock import MagicMock
import pytest
from fastapi import status
from fastapi.testclient import TestClient
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api_app import app
from invokeai.app.services.auth.token_service import set_jwt_secret
from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
from invokeai.app.services.boards.boards_default import BoardService
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.image_moves.image_moves_default import ImageMoveJobAlreadyRunning, ImageMoveQueueActive
from invokeai.app.services.image_records.image_records_sqlite import SqliteImageRecordStorage
from invokeai.app.services.images.images_default import ImageService
from invokeai.app.services.invocation_cache.invocation_cache_memory import MemoryInvocationCache
from invokeai.app.services.invocation_services import InvocationServices
from invokeai.app.services.invocation_stats.invocation_stats_default import InvocationStatsService
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.users.users_common import UserCreateRequest
from invokeai.app.services.users.users_default import UserService
from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
from invokeai.backend.util.logging import InvokeAILogger
from tests.fixtures.sqlite_database import create_mock_sqlite_database
from tests.test_nodes import TestEventService
class MockApiDependencies(ApiDependencies):
invoker: Invoker
def __init__(self, invoker: Invoker) -> None:
self.invoker = invoker
@pytest.fixture
def client() -> TestClient:
return TestClient(app)
@pytest.fixture
def mock_services() -> InvocationServices:
configuration = InvokeAIAppConfig(use_memory_db=True, node_cache_size=0)
logger = InvokeAILogger.get_logger()
db = create_mock_sqlite_database(configuration, logger)
image_moves = MagicMock()
return InvocationServices(
board_image_records=SqliteBoardImageRecordStorage(db=db),
board_images=None, # type: ignore
board_records=SqliteBoardRecordStorage(db=db),
boards=BoardService(),
bulk_download=BulkDownloadService(),
configuration=configuration,
events=TestEventService(),
image_files=None, # type: ignore
image_records=SqliteImageRecordStorage(db=db),
images=ImageService(),
invocation_cache=MemoryInvocationCache(max_cache_size=0),
logger=logging, # type: ignore
model_images=None, # type: ignore
model_manager=None, # type: ignore
download_queue=None, # type: ignore
external_generation=None, # type: ignore
names=None, # type: ignore
performance_statistics=InvocationStatsService(),
session_processor=None, # type: ignore
session_queue=None, # type: ignore
urls=None, # type: ignore
workflow_records=SqliteWorkflowRecordsStorage(db=db),
tensors=None, # type: ignore
conditioning=None, # type: ignore
style_preset_records=None, # type: ignore
style_preset_image_files=None, # type: ignore
workflow_thumbnails=None, # type: ignore
model_relationship_records=None, # type: ignore
model_relationships=None, # type: ignore
client_state_persistence=ClientStatePersistenceSqlite(db=db),
users=UserService(db),
image_moves=image_moves,
)
@pytest.fixture
def mock_invoker(mock_services: InvocationServices, monkeypatch: pytest.MonkeyPatch) -> Invoker:
invoker = Invoker(services=mock_services)
mock_deps = MockApiDependencies(invoker)
monkeypatch.setattr("invokeai.app.api.routers.image_moves.ApiDependencies", mock_deps)
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", mock_deps)
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", mock_deps)
return invoker
def _status_payload(is_running: bool = True, operation: str = "move_all") -> dict:
return {
"is_running": is_running,
"operation": operation,
"active_job_id": None,
"latest_job": None,
"last_error": None,
"needs_move_count": 0,
}
def _create_user(invoker: Invoker, email: str, is_admin: bool) -> None:
invoker.services.users.create(
UserCreateRequest(
email=email,
display_name=email,
password="TestPass123",
is_admin=is_admin,
)
)
def _login(client: TestClient, email: str) -> str:
response = client.post("/api/v1/auth/login", json={"email": email, "password": "TestPass123"})
assert response.status_code == status.HTTP_200_OK
return response.json()["token"]
def test_start_image_move_returns_accepted_without_running_job_inline(
client: TestClient, mock_invoker: Invoker
) -> None:
image_moves = mock_invoker.services.image_moves
image_moves.start_background_move_all.return_value = _status_payload()
response = client.post("/api/v1/image_moves/start")
assert response.status_code == status.HTTP_202_ACCEPTED
assert response.json()["is_running"] is True
image_moves.start_background_move_all.assert_called_once_with()
image_moves.move_all_images.assert_not_called()
def test_start_image_move_rejects_overlapping_background_job(client: TestClient, mock_invoker: Invoker) -> None:
image_moves = mock_invoker.services.image_moves
image_moves.start_background_move_all.side_effect = ImageMoveJobAlreadyRunning("already running")
response = client.post("/api/v1/image_moves/start")
assert response.status_code == status.HTTP_409_CONFLICT
assert response.json()["detail"] == "already running"
def test_start_image_move_rejects_active_queue_work(client: TestClient, mock_invoker: Invoker) -> None:
image_moves = mock_invoker.services.image_moves
image_moves.start_background_move_all.side_effect = ImageMoveQueueActive("queue work is active")
response = client.post("/api/v1/image_moves/start")
assert response.status_code == status.HTTP_409_CONFLICT
assert response.json()["detail"] == "queue work is active"
def test_force_recovery_returns_accepted(client: TestClient, mock_invoker: Invoker) -> None:
image_moves = mock_invoker.services.image_moves
image_moves.start_background_recovery.return_value = _status_payload(operation="recovery")
response = client.post("/api/v1/image_moves/recover")
assert response.status_code == status.HTTP_202_ACCEPTED
assert response.json()["operation"] == "recovery"
image_moves.start_background_recovery.assert_called_once_with()
def test_image_move_status_uses_service_status(client: TestClient, mock_invoker: Invoker) -> None:
image_moves = mock_invoker.services.image_moves
image_moves.get_background_status.return_value = _status_payload(is_running=False)
response = client.get("/api/v1/image_moves/status")
assert response.status_code == status.HTTP_200_OK
assert response.json()["is_running"] is False
assert response.json()["needs_move_count"] == 0
image_moves.get_background_status.assert_called_once_with()
@pytest.mark.parametrize(
("method", "path"),
[
("post", "/api/v1/image_moves/start"),
("post", "/api/v1/image_moves/recover"),
("get", "/api/v1/image_moves/status"),
],
)
def test_image_move_endpoints_require_admin_in_multiuser_mode(
client: TestClient, mock_invoker: Invoker, method: str, path: str
) -> None:
set_jwt_secret("test-secret")
mock_invoker.services.configuration.multiuser = True
_create_user(mock_invoker, "user@test.com", is_admin=False)
token = _login(client, "user@test.com")
response = getattr(client, method)(path, headers={"Authorization": f"Bearer {token}"})
assert response.status_code == status.HTTP_403_FORBIDDEN