项目文件夹

文件
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

445 行
17 KiB
Python

"""Tests for multiuser client state functionality."""
from typing import Any
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.invoker import Invoker
from invokeai.app.services.users.users_common import UserCreateRequest
@pytest.fixture
def client():
"""Create a test client."""
return TestClient(app)
class MockApiDependencies(ApiDependencies):
"""Mock API dependencies for testing."""
invoker: Invoker
def __init__(self, invoker: Invoker) -> None:
self.invoker = invoker
def setup_test_user(
mock_invoker: Invoker, email: str, display_name: str, password: str = "TestPass123", is_admin: bool = False
) -> str:
"""Helper to create a test user and return user_id."""
user_service = mock_invoker.services.users
user_data = UserCreateRequest(
email=email,
display_name=display_name,
password=password,
is_admin=is_admin,
)
user = user_service.create(user_data)
return user.user_id
def get_user_token(client: TestClient, email: str, password: str = "TestPass123") -> str:
"""Helper to login and get a user token."""
response = client.post(
"/api/v1/auth/login",
json={
"email": email,
"password": password,
"remember_me": False,
},
)
assert response.status_code == 200
return response.json()["token"]
@pytest.fixture
def admin_token(monkeypatch: Any, mock_invoker: Invoker, client: TestClient):
"""Get an admin token for testing."""
# Enable multiuser mode for auth endpoints
mock_invoker.services.configuration.multiuser = True
# Mock ApiDependencies for auth and client_state routers
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.routers.client_state.ApiDependencies", MockApiDependencies(mock_invoker))
# Create admin user
setup_test_user(mock_invoker, "admin@test.com", "Admin User", is_admin=True)
return get_user_token(client, "admin@test.com")
@pytest.fixture
def user1_token(monkeypatch: Any, mock_invoker: Invoker, client: TestClient, admin_token: str):
"""Get a token for test user 1."""
# Create a regular user
setup_test_user(mock_invoker, "user1@test.com", "User One", is_admin=False)
return get_user_token(client, "user1@test.com")
@pytest.fixture
def user2_token(monkeypatch: Any, mock_invoker: Invoker, client: TestClient, admin_token: str):
"""Get a token for test user 2."""
# Create another regular user
setup_test_user(mock_invoker, "user2@test.com", "User Two", is_admin=False)
return get_user_token(client, "user2@test.com")
def test_get_client_state_without_auth_uses_system_user(client: TestClient, monkeypatch, mock_invoker: Invoker):
"""Test that getting client state without authentication uses the system user."""
# Mock ApiDependencies
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.routers.client_state.ApiDependencies", MockApiDependencies(mock_invoker))
# Set a value for the system user directly
mock_invoker.services.client_state_persistence.set_by_key("system", "test_key", "system_value")
# Get without authentication - should return system user's value
response = client.get("/api/v1/client_state/default/get_by_key?key=test_key")
assert response.status_code == status.HTTP_200_OK
assert response.json() == "system_value"
def test_set_client_state_without_auth_uses_system_user(client: TestClient, monkeypatch, mock_invoker: Invoker):
"""Test that setting client state without authentication uses the system user."""
# Mock ApiDependencies
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.routers.client_state.ApiDependencies", MockApiDependencies(mock_invoker))
# Set without authentication - should set for system user
response = client.post(
"/api/v1/client_state/default/set_by_key?key=test_key",
json="unauthenticated_value",
)
assert response.status_code == status.HTTP_200_OK
# Verify it was set for system user
value = mock_invoker.services.client_state_persistence.get_by_key("system", "test_key")
assert value == "unauthenticated_value"
def test_delete_client_state_without_auth_uses_system_user(client: TestClient, monkeypatch, mock_invoker: Invoker):
"""Test that deleting client state without authentication uses the system user."""
# Mock ApiDependencies
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.routers.client_state.ApiDependencies", MockApiDependencies(mock_invoker))
# Set a value for system user
mock_invoker.services.client_state_persistence.set_by_key("system", "test_key", "system_value")
# Delete without authentication - should delete system user's data
response = client.post("/api/v1/client_state/default/delete")
assert response.status_code == status.HTTP_200_OK
# Verify it was deleted for system user
value = mock_invoker.services.client_state_persistence.get_by_key("system", "test_key")
assert value is None
def test_set_and_get_client_state(client: TestClient, admin_token: str):
"""Test that authenticated users can set and get their client state."""
# Set a value
set_response = client.post(
"/api/v1/client_state/default/set_by_key?key=test_key",
json="test_value",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert set_response.status_code == status.HTTP_200_OK
assert set_response.json() == "test_value"
# Get the value back
get_response = client.get(
"/api/v1/client_state/default/get_by_key?key=test_key",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert get_response.status_code == status.HTTP_200_OK
assert get_response.json() == "test_value"
def test_client_state_isolation_between_users(client: TestClient, user1_token: str, user2_token: str):
"""Test that client state is isolated between different users."""
# User 1 sets a value
user1_set_response = client.post(
"/api/v1/client_state/default/set_by_key?key=shared_key",
json="user1_value",
headers={"Authorization": f"Bearer {user1_token}"},
)
assert user1_set_response.status_code == status.HTTP_200_OK
# User 2 sets a different value for the same key
user2_set_response = client.post(
"/api/v1/client_state/default/set_by_key?key=shared_key",
json="user2_value",
headers={"Authorization": f"Bearer {user2_token}"},
)
assert user2_set_response.status_code == status.HTTP_200_OK
# User 1 should still see their own value
user1_get_response = client.get(
"/api/v1/client_state/default/get_by_key?key=shared_key",
headers={"Authorization": f"Bearer {user1_token}"},
)
assert user1_get_response.status_code == status.HTTP_200_OK
assert user1_get_response.json() == "user1_value"
# User 2 should see their own value
user2_get_response = client.get(
"/api/v1/client_state/default/get_by_key?key=shared_key",
headers={"Authorization": f"Bearer {user2_token}"},
)
assert user2_get_response.status_code == status.HTTP_200_OK
assert user2_get_response.json() == "user2_value"
def test_get_nonexistent_key_returns_null(client: TestClient, admin_token: str):
"""Test that getting a nonexistent key returns null."""
response = client.get(
"/api/v1/client_state/default/get_by_key?key=nonexistent_key",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == status.HTTP_200_OK
assert response.json() is None
def test_delete_client_state(client: TestClient, admin_token: str):
"""Test that users can delete their own client state."""
# Set some values
client.post(
"/api/v1/client_state/default/set_by_key?key=key1",
json="value1",
headers={"Authorization": f"Bearer {admin_token}"},
)
client.post(
"/api/v1/client_state/default/set_by_key?key=key2",
json="value2",
headers={"Authorization": f"Bearer {admin_token}"},
)
# Verify values exist
get_response = client.get(
"/api/v1/client_state/default/get_by_key?key=key1",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert get_response.json() == "value1"
# Delete all client state
delete_response = client.post(
"/api/v1/client_state/default/delete",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert delete_response.status_code == status.HTTP_200_OK
# Verify values are gone
get_response = client.get(
"/api/v1/client_state/default/get_by_key?key=key1",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert get_response.json() is None
get_response = client.get(
"/api/v1/client_state/default/get_by_key?key=key2",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert get_response.json() is None
def test_update_existing_key(client: TestClient, admin_token: str):
"""Test that updating an existing key works correctly."""
# Set initial value
client.post(
"/api/v1/client_state/default/set_by_key?key=update_key",
json="initial_value",
headers={"Authorization": f"Bearer {admin_token}"},
)
# Update the value
update_response = client.post(
"/api/v1/client_state/default/set_by_key?key=update_key",
json="updated_value",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert update_response.status_code == status.HTTP_200_OK
# Verify the updated value
get_response = client.get(
"/api/v1/client_state/default/get_by_key?key=update_key",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert get_response.status_code == status.HTTP_200_OK
assert get_response.json() == "updated_value"
def test_complex_json_values(client: TestClient, admin_token: str):
"""Test that complex JSON values can be stored and retrieved."""
import json
complex_dict = {"params": {"model": "test-model", "steps": 50}, "prompt": "a beautiful landscape"}
complex_value = json.dumps(complex_dict)
# Set complex value
set_response = client.post(
"/api/v1/client_state/default/set_by_key?key=complex_key",
json=complex_value,
headers={"Authorization": f"Bearer {admin_token}"},
)
assert set_response.status_code == status.HTTP_200_OK
# Get it back
get_response = client.get(
"/api/v1/client_state/default/get_by_key?key=complex_key",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert get_response.status_code == status.HTTP_200_OK
assert get_response.json() == complex_value
def test_get_keys_by_prefix_without_auth(client: TestClient, monkeypatch, mock_invoker: Invoker):
"""Test that keys can be retrieved by prefix without authentication."""
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.routers.client_state.ApiDependencies", MockApiDependencies(mock_invoker))
# Set several keys with a common prefix directly
for i in range(3):
mock_invoker.services.client_state_persistence.set_by_key("system", f"canvas_snapshot:snap{i}", f"value{i}")
mock_invoker.services.client_state_persistence.set_by_key("system", "other_key", "other_value")
# Get keys by prefix
response = client.get("/api/v1/client_state/default/get_keys_by_prefix?prefix=canvas_snapshot:")
assert response.status_code == status.HTTP_200_OK
keys = response.json()
assert len(keys) == 3
assert "canvas_snapshot:snap0" in keys
assert "canvas_snapshot:snap1" in keys
assert "canvas_snapshot:snap2" in keys
assert "other_key" not in keys
def test_get_keys_by_prefix_empty_without_auth(client: TestClient, monkeypatch, mock_invoker: Invoker):
"""Test that an empty list is returned when no keys match the prefix."""
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.routers.client_state.ApiDependencies", MockApiDependencies(mock_invoker))
response = client.get("/api/v1/client_state/default/get_keys_by_prefix?prefix=nonexistent_prefix:")
assert response.status_code == status.HTTP_200_OK
assert response.json() == []
def test_delete_by_key_without_auth(client: TestClient, monkeypatch, mock_invoker: Invoker):
"""Test that a specific key can be deleted without affecting other keys."""
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.routers.client_state.ApiDependencies", MockApiDependencies(mock_invoker))
# Set two keys directly
mock_invoker.services.client_state_persistence.set_by_key("system", "keep_key", "keep_value")
mock_invoker.services.client_state_persistence.set_by_key("system", "delete_key", "delete_value")
# Delete only one key via endpoint
delete_response = client.post("/api/v1/client_state/default/delete_by_key?key=delete_key")
assert delete_response.status_code == status.HTTP_200_OK
# Verify deleted key is gone
value = mock_invoker.services.client_state_persistence.get_by_key("system", "delete_key")
assert value is None
# Verify other key still exists
value = mock_invoker.services.client_state_persistence.get_by_key("system", "keep_key")
assert value == "keep_value"
def test_get_keys_by_prefix(client: TestClient, admin_token: str):
"""Test that keys can be retrieved by prefix with authentication."""
# Set several keys with a common prefix
for i in range(3):
client.post(
f"/api/v1/client_state/default/set_by_key?key=canvas_snapshot:snap{i}",
json=f"value{i}",
headers={"Authorization": f"Bearer {admin_token}"},
)
# Set a key without the prefix
client.post(
"/api/v1/client_state/default/set_by_key?key=other_key",
json="other_value",
headers={"Authorization": f"Bearer {admin_token}"},
)
# Get keys by prefix
response = client.get(
"/api/v1/client_state/default/get_keys_by_prefix?prefix=canvas_snapshot:",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == status.HTTP_200_OK
keys = response.json()
assert len(keys) == 3
assert "canvas_snapshot:snap0" in keys
assert "canvas_snapshot:snap1" in keys
assert "canvas_snapshot:snap2" in keys
assert "other_key" not in keys
def test_delete_by_key(client: TestClient, admin_token: str):
"""Test that a specific key can be deleted without affecting other keys."""
# Set two keys
client.post(
"/api/v1/client_state/default/set_by_key?key=keep_key",
json="keep_value",
headers={"Authorization": f"Bearer {admin_token}"},
)
client.post(
"/api/v1/client_state/default/set_by_key?key=delete_key",
json="delete_value",
headers={"Authorization": f"Bearer {admin_token}"},
)
# Delete only one key
delete_response = client.post(
"/api/v1/client_state/default/delete_by_key?key=delete_key",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert delete_response.status_code == status.HTTP_200_OK
# Verify deleted key is gone
get_response = client.get(
"/api/v1/client_state/default/get_by_key?key=delete_key",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert get_response.json() is None
# Verify other key still exists
get_response = client.get(
"/api/v1/client_state/default/get_by_key?key=keep_key",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert get_response.json() == "keep_value"
def test_get_keys_by_prefix_isolation_between_users(client: TestClient, user1_token: str, user2_token: str):
"""Test that get_keys_by_prefix is isolated between users."""
# User 1 sets keys
client.post(
"/api/v1/client_state/default/set_by_key?key=snapshot:u1",
json="user1_data",
headers={"Authorization": f"Bearer {user1_token}"},
)
# User 2 sets keys
client.post(
"/api/v1/client_state/default/set_by_key?key=snapshot:u2",
json="user2_data",
headers={"Authorization": f"Bearer {user2_token}"},
)
# User 1 should only see their own keys
response = client.get(
"/api/v1/client_state/default/get_keys_by_prefix?prefix=snapshot:",
headers={"Authorization": f"Bearer {user1_token}"},
)
keys = response.json()
assert "snapshot:u1" in keys
assert "snapshot:u2" not in keys