refactor(api): extract init validation into layered application service (#40046)

This commit is contained in:
Byron.wang
2026-08-12 08:42:09 +00:00
committed by GitHub
parent 6d185b6f40
commit 51a007e84e
17 changed files with 547 additions and 114 deletions
@@ -97,6 +97,31 @@ def test_dify_config(monkeypatch: pytest.MonkeyPatch):
assert Version(config.project.version) >= Version("1.0.0")
@pytest.mark.parametrize(
("environment_value", "expected"),
[
pytest.param(None, "", id="unset"),
pytest.param("", "", id="empty"),
pytest.param("expected", "expected", id="ascii"),
pytest.param("pässwörd-🔐", "pässwörd-🔐", id="unicode"),
],
)
def test_init_password_defaults_to_empty_and_preserves_environment_value(
monkeypatch: pytest.MonkeyPatch,
environment_value: str | None,
expected: str,
) -> None:
_set_basic_config_env(monkeypatch)
if environment_value is None:
monkeypatch.delenv("INIT_PASSWORD", raising=False)
else:
monkeypatch.setenv("INIT_PASSWORD", environment_value)
config = DifyConfig(_env_file=None)
assert expected == config.INIT_PASSWORD
@pytest.mark.parametrize("edition", list(DeploymentEdition))
def test_deployment_edition_is_loaded_from_environment(
monkeypatch: pytest.MonkeyPatch,
@@ -40,8 +40,7 @@ def _patch_console_guards(monkeypatch: pytest.MonkeyPatch, account: Account, app
monkeypatch.setattr(login_lib, "check_csrf_token", lambda *_, **__: None)
monkeypatch.setattr(console_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(app_wraps, "current_account_with_tenant", lambda: (account, account.current_tenant_id))
monkeypatch.setattr(console_wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.delenv("INIT_PASSWORD", raising=False)
monkeypatch.setattr(console_wraps.dify_config, "INIT_PASSWORD", "")
# Avoid hitting the database when resolving the app model
monkeypatch.setattr(app_wraps, "_load_app_model_from_scoped_session", lambda _app_id: app_model)
@@ -64,8 +64,8 @@ def _mock_console_guards(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
account.is_authenticated = True
monkeypatch.setattr(wraps_module.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
monkeypatch.setattr(wraps_module.dify_config, "INIT_PASSWORD", "")
monkeypatch.setattr("libs.login.dify_config.LOGIN_DISABLED", True)
monkeypatch.delenv("INIT_PASSWORD", raising=False)
monkeypatch.setattr(wraps_module, "current_account_with_tenant", lambda: (account, "tenant-123"))
# The login_required decorator consults the shared LocalProxy in libs.login.
@@ -0,0 +1,149 @@
"""HTTP contract tests for the FastOpenAPI initialization routes."""
from types import SimpleNamespace
from unittest.mock import Mock, create_autospec
import pytest
from controllers.console import init_validate
from dify_app import DifyApp
from enums import DeploymentEdition
from extensions import ext_fastopenapi
from services.init_validation_service import (
AlreadyInitializedError,
InitValidationService,
InvalidInitializationPasswordError,
)
@pytest.fixture
def init_validation(monkeypatch: pytest.MonkeyPatch) -> Mock:
service = create_autospec(InitValidationService, instance=True, spec_set=True)
services = SimpleNamespace(init_validation=service)
monkeypatch.setattr(init_validate, "application_services", lambda: services)
return service
@pytest.fixture
def app() -> DifyApp:
app = DifyApp(__name__)
app.config.update(TESTING=True, SECRET_KEY="test-secret")
ext_fastopenapi.init_app(app)
return app
@pytest.mark.parametrize(
("validated", "expected_status"),
[
pytest.param(True, "finished", id="finished"),
pytest.param(False, "not_started", id="not-started"),
],
)
def test_get_init_status(
app: DifyApp,
init_validation: Mock,
validated: bool,
expected_status: str,
) -> None:
init_validation.is_validated.return_value = validated
response = app.test_client().get("/console/api/init")
assert response.status_code == 200
assert response.get_json() == {"status": expected_status}
init_validation.is_validated.assert_called_once_with(session_validated=False)
def test_validate_init_password_success(
app: DifyApp,
init_validation: Mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"controllers.console.wraps.dify_config.DEPLOYMENT_EDITION",
DeploymentEdition.COMMUNITY,
)
client = app.test_client()
response = client.post("/console/api/init", json={"password": "expected"})
assert response.status_code == 201
assert response.get_json() == {"result": "success"}
init_validation.validate_password.assert_called_once_with("expected")
with client.session_transaction() as browser_session:
assert browser_session["is_init_validated"] is True
def test_validate_init_password_rejects_a_mismatch(
app: DifyApp,
init_validation: Mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"controllers.console.wraps.dify_config.DEPLOYMENT_EDITION",
DeploymentEdition.COMMUNITY,
)
init_validation.validate_password.side_effect = InvalidInitializationPasswordError
client = app.test_client()
response = client.post("/console/api/init", json={"password": "wrong"})
assert response.status_code == 401
with client.session_transaction() as browser_session:
assert browser_session["is_init_validated"] is False
def test_validate_init_password_rejects_an_initialized_installation(
app: DifyApp,
init_validation: Mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"controllers.console.wraps.dify_config.DEPLOYMENT_EDITION",
DeploymentEdition.COMMUNITY,
)
init_validation.validate_password.side_effect = AlreadyInitializedError
response = app.test_client().post("/console/api/init", json={"password": "expected"})
assert response.status_code == 403
@pytest.mark.parametrize(
"payload",
[
pytest.param({}, id="missing-password"),
pytest.param({"password": "x" * 31}, id="password-too-long"),
],
)
def test_validate_init_password_rejects_an_invalid_payload(
app: DifyApp,
init_validation: Mock,
monkeypatch: pytest.MonkeyPatch,
payload: dict[str, str],
) -> None:
monkeypatch.setattr(
"controllers.console.wraps.dify_config.DEPLOYMENT_EDITION",
DeploymentEdition.COMMUNITY,
)
response = app.test_client().post("/console/api/init", json=payload)
assert response.status_code == 422
init_validation.validate_password.assert_not_called()
def test_validate_init_password_is_not_available_in_cloud(
app: DifyApp,
init_validation: Mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
"controllers.console.wraps.dify_config.DEPLOYMENT_EDITION",
DeploymentEdition.CLOUD,
)
response = app.test_client().post("/console/api/init", json={"password": "expected"})
assert response.status_code == 404
init_validation.validate_password.assert_not_called()
@@ -80,7 +80,7 @@ def test_console_setup_fastopenapi_post_success(
deployment_edition: DeploymentEdition,
) -> None:
monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", deployment_edition)
monkeypatch.setattr(setup_controller, "get_init_validate_status", lambda: True)
monkeypatch.setattr(setup_controller, "is_init_validated", lambda: True)
mark_setup_completed = Mock()
monkeypatch.setattr(setup_controller, "mark_setup_completed", mark_setup_completed)
payload = {
@@ -197,7 +197,7 @@ def test_console_setup_translates_service_errors_to_controller_errors(
expected_controller_error: type[Exception],
) -> None:
monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(setup_controller, "get_init_validate_status", lambda: False)
monkeypatch.setattr(setup_controller, "is_init_validated", lambda: False)
mark_setup_completed = Mock()
monkeypatch.setattr(setup_controller, "mark_setup_completed", mark_setup_completed)
setup_service.initialize.side_effect = service_error
@@ -228,7 +228,7 @@ def test_console_setup_fastopenapi_does_not_mark_setup_completed_when_service_fa
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(setup_controller, "get_init_validate_status", lambda: True)
monkeypatch.setattr(setup_controller, "is_init_validated", lambda: True)
mark_setup_completed = Mock()
monkeypatch.setattr(setup_controller, "mark_setup_completed", mark_setup_completed)
setup_service.initialize.side_effect = RuntimeError("provision failed")
@@ -17,6 +17,7 @@ from services.entities.feature_entities import (
)
from services.explore_banner_query_service import ExploreBannerQueryService
from services.feature_query_service import FeatureQueryService
from services.init_validation_service import InitValidationService
from services.schema_definition_service import SchemaDefinitionService
from services.setup_service import SetupService
from services.workspace_member_query_service import WorkspaceMemberQueryService
@@ -39,6 +40,7 @@ def _install_application_services(mocker: MockerFixture):
schema_definitions=create_autospec(SchemaDefinitionService, instance=True, spec_set=True),
setup=create_autospec(SetupService, instance=True, spec_set=True),
feature_queries=feature_queries,
init_validation=create_autospec(InitValidationService, instance=True, spec_set=True),
workspace_queries=create_autospec(WorkspaceQueryService, instance=True, spec_set=True),
workspace_member_queries=create_autospec(WorkspaceMemberQueryService, instance=True, spec_set=True),
)
@@ -1,34 +1,56 @@
"""Initialization validation tests with real setup-state persistence in SQLite."""
from __future__ import annotations
"""Tests for the Flask adapter around initialization validation."""
from types import SimpleNamespace
from unittest.mock import Mock, create_autospec
import pytest
from flask import Flask
from sqlalchemy.orm import Session
from controllers.console import init_validate
from controllers.console import init_validate, wraps
from controllers.console.error import AlreadySetupError, InitValidateFailedError
from enums import DeploymentEdition
from models.model import DifySetup
from services.init_validation_service import (
AlreadyInitializedError,
InitValidationService,
InvalidInitializationPasswordError,
)
def test_get_init_status_finished(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(init_validate, "get_init_validate_status", lambda: True)
result = init_validate.get_init_status()
assert result.status == "finished"
@pytest.fixture
def init_validation(monkeypatch: pytest.MonkeyPatch) -> Mock:
service = create_autospec(InitValidationService, instance=True, spec_set=True)
application_services = SimpleNamespace(init_validation=service)
monkeypatch.setattr(init_validate, "application_services", lambda: application_services)
return service
def test_get_init_status_not_started(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(init_validate, "get_init_validate_status", lambda: False)
result = init_validate.get_init_status()
assert result.status == "not_started"
def test_get_init_status_finished(app: Flask, init_validation: Mock) -> None:
init_validation.is_validated.return_value = True
app.secret_key = "test-secret"
with app.test_request_context("/console/api/init", method="GET"):
result = init_validate.get_init_status()
assert result.status == "finished"
def test_validate_init_password_already_setup(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(init_validate.TenantService, "get_tenant_count", lambda *, session: 1)
def test_get_init_status_not_started(app: Flask, init_validation: Mock) -> None:
init_validation.is_validated.return_value = False
app.secret_key = "test-secret"
with app.test_request_context("/console/api/init", method="GET"):
result = init_validate.get_init_status()
assert result.status == "not_started"
def test_validate_init_password_already_setup(
app: Flask,
init_validation: Mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
init_validation.validate_password.side_effect = AlreadyInitializedError
app.secret_key = "test-secret"
with app.test_request_context("/console/api/init", method="POST"):
@@ -36,10 +58,13 @@ def test_validate_init_password_already_setup(app: Flask, monkeypatch: pytest.Mo
init_validate.validate_init_password(init_validate.InitValidatePayload(password="pw"))
def test_validate_init_password_wrong_password(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(init_validate.TenantService, "get_tenant_count", lambda *, session: 0)
monkeypatch.setenv("INIT_PASSWORD", "expected")
def test_validate_init_password_wrong_password(
app: Flask,
init_validation: Mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
init_validation.validate_password.side_effect = InvalidInitializationPasswordError
app.secret_key = "test-secret"
with app.test_request_context("/console/api/init", method="POST"):
@@ -48,58 +73,42 @@ def test_validate_init_password_wrong_password(app: Flask, monkeypatch: pytest.M
assert init_validate.session.get("is_init_validated") is False
def test_validate_init_password_success(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setattr(init_validate.TenantService, "get_tenant_count", lambda *, session: 0)
monkeypatch.setenv("INIT_PASSWORD", "expected")
def test_validate_init_password_success(
app: Flask,
init_validation: Mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(wraps.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
app.secret_key = "test-secret"
with app.test_request_context("/console/api/init", method="POST"):
result = init_validate.validate_init_password(init_validate.InitValidatePayload(password="expected"))
assert result.result == "success"
assert init_validate.session.get("is_init_validated") is True
init_validation.validate_password.assert_called_once_with("expected")
def test_get_init_validate_status_not_self_hosted(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.CLOUD)
assert init_validate.get_init_validate_status() is True
def test_get_init_validate_status_validated_session(app: Flask, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setenv("INIT_PASSWORD", "expected")
app.secret_key = "test-secret"
with app.test_request_context("/console/api/init", method="GET"):
init_validate.session["is_init_validated"] = True
assert init_validate.get_init_validate_status() is True
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_get_init_validate_status_setup_exists(
app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
@pytest.mark.parametrize(
("session_value", "expected"),
[
pytest.param(None, False, id="missing"),
pytest.param(False, False, id="not-validated"),
pytest.param(True, True, id="validated"),
],
)
def test_is_init_validated_passes_session_state(
app: Flask,
init_validation: Mock,
session_value: bool | None,
expected: bool,
) -> None:
monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setenv("INIT_PASSWORD", "expected")
monkeypatch.setattr(init_validate, "db", SimpleNamespace(engine=sqlite_session.get_bind()))
sqlite_session.add(DifySetup(version="test-version"))
sqlite_session.commit()
init_validation.is_validated.return_value = True
app.secret_key = "test-secret"
with app.test_request_context("/console/api/init", method="GET"):
init_validate.session.pop("is_init_validated", None)
assert init_validate.get_init_validate_status() is True
if session_value is not None:
init_validate.session["is_init_validated"] = session_value
@pytest.mark.parametrize("sqlite_session", [(DifySetup,)], indirect=True)
def test_get_init_validate_status_not_validated(
app: Flask, monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
) -> None:
monkeypatch.setattr(init_validate.dify_config, "DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY)
monkeypatch.setenv("INIT_PASSWORD", "expected")
monkeypatch.setattr(init_validate, "db", SimpleNamespace(engine=sqlite_session.get_bind()))
app.secret_key = "test-secret"
with app.test_request_context("/console/api/init", method="GET"):
init_validate.session.pop("is_init_validated", None)
assert init_validate.get_init_validate_status() is False
assert init_validate.is_init_validated() is True
init_validation.is_validated.assert_called_once_with(session_validated=expected)
@@ -925,17 +925,18 @@ class TestSystemSetup:
assert mock_db.session.scalar.call_count == 1
@patch("controllers.console.wraps.db")
@patch("controllers.console.wraps.os.environ.get")
def test_should_not_cache_missing_setup(self, mock_environ_get, mock_db):
def test_should_not_cache_missing_setup(self, mock_db):
"""Test that first-time bootstrap completion can be observed later in the same process"""
mock_db.session.scalar.side_effect = [None, MagicMock()]
mock_environ_get.return_value = None
@setup_required
def admin_view():
return "admin_success"
with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
with (
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("controllers.console.wraps.dify_config.INIT_PASSWORD", ""),
):
with pytest.raises(NotSetupError):
admin_view()
assert admin_view() == "admin_success"
@@ -943,36 +944,38 @@ class TestSystemSetup:
assert mock_db.session.scalar.call_count == 2
@patch("controllers.console.wraps.db")
@patch("controllers.console.wraps.os.environ.get")
def test_should_raise_not_init_validate_error_with_init_password(self, mock_environ_get, mock_db: MagicMock):
def test_should_raise_not_init_validate_error_with_init_password(self, mock_db: MagicMock):
"""Test NotInitValidateError when INIT_PASSWORD is set but setup not complete"""
# Arrange
mock_db.session.scalar.return_value = None # No setup
mock_environ_get.return_value = "some_password"
@setup_required
def admin_view():
return "admin_success"
# Act & Assert
with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
with (
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("controllers.console.wraps.dify_config.INIT_PASSWORD", "some_password"),
):
with pytest.raises(NotInitValidateError):
admin_view()
@patch("controllers.console.wraps.db")
@patch("controllers.console.wraps.os.environ.get")
def test_should_raise_not_setup_error_without_init_password(self, mock_environ_get, mock_db: MagicMock):
def test_should_raise_not_setup_error_without_init_password(self, mock_db: MagicMock):
"""Test NotSetupError when no INIT_PASSWORD and setup not complete"""
# Arrange
mock_db.session.scalar.return_value = None # No setup
mock_environ_get.return_value = None # No INIT_PASSWORD
@setup_required
def admin_view():
return "admin_success"
# Act & Assert
with patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY):
with (
patch("controllers.console.wraps.dify_config.DEPLOYMENT_EDITION", DeploymentEdition.COMMUNITY),
patch("controllers.console.wraps.dify_config.INIT_PASSWORD", ""),
):
with pytest.raises(NotSetupError):
admin_view()
@@ -1,11 +1,86 @@
"""Tests for application-service dependency wiring."""
from unittest.mock import MagicMock, patch
import pytest
from flask import Flask
from sqlalchemy.orm import Session, sessionmaker
from enums import DeploymentEdition
from extensions.ext_application_services import build_application_services
from extensions import ext_application_services
from extensions.ext_redis import RedisClientWrapper
from models.model import DifySetup
from services.init_validation_service import InvalidInitializationPasswordError
@pytest.mark.parametrize(
("deployment_edition", "initialization_password", "session_validated", "setup_exists", "expected"),
[
pytest.param(DeploymentEdition.CLOUD, "expected", False, False, True, id="cloud"),
pytest.param(DeploymentEdition.COMMUNITY, "", False, False, True, id="no-password"),
pytest.param(DeploymentEdition.COMMUNITY, "expected", False, False, False, id="not-validated"),
pytest.param(DeploymentEdition.ENTERPRISE, "expected", False, False, False, id="enterprise"),
pytest.param(DeploymentEdition.COMMUNITY, "expected", True, False, True, id="browser-session"),
pytest.param(DeploymentEdition.COMMUNITY, "expected", False, True, True, id="setup-record"),
],
)
def test_build_application_services_configures_init_validation(
sqlite_session: Session,
sqlite_session_factory: sessionmaker[Session],
deployment_edition: DeploymentEdition,
initialization_password: str,
session_validated: bool,
setup_exists: bool,
expected: bool,
) -> None:
if setup_exists:
sqlite_session.add(DifySetup(version="test-version"))
sqlite_session.commit()
services = ext_application_services.build_application_services(
database_client=sqlite_session_factory,
deployment_edition=deployment_edition,
initialization_password=initialization_password,
redis=MagicMock(spec=RedisClientWrapper),
)
assert services.init_validation.is_validated(session_validated=session_validated) is expected
def test_build_application_services_passes_the_expected_password(
sqlite_session_factory: sessionmaker[Session],
) -> None:
services = ext_application_services.build_application_services(
database_client=sqlite_session_factory,
deployment_edition=DeploymentEdition.COMMUNITY,
initialization_password="expected",
redis=MagicMock(spec=RedisClientWrapper),
)
services.init_validation.validate_password("expected")
with pytest.raises(InvalidInitializationPasswordError):
services.init_validation.validate_password("wrong")
def test_init_app_registers_services_for_the_current_app(
monkeypatch: pytest.MonkeyPatch,
sqlite_session_factory: sessionmaker[Session],
) -> None:
app = Flask(__name__)
monkeypatch.setattr(ext_application_services, "get_session_maker", lambda: sqlite_session_factory)
monkeypatch.setattr(
ext_application_services.dify_config,
"DEPLOYMENT_EDITION",
DeploymentEdition.COMMUNITY,
)
monkeypatch.setattr(ext_application_services.dify_config, "INIT_PASSWORD", "expected")
ext_application_services.init_app(app)
with app.app_context():
services = ext_application_services.application_services()
assert services is app.extensions["application_services"]
assert services.init_validation.is_validated(session_validated=False) is False
@pytest.mark.parametrize(
@@ -21,9 +96,10 @@ def test_build_application_services_configures_setup_policy(
deployment_edition: DeploymentEdition,
setup_completed: bool,
) -> None:
services = build_application_services(
services = ext_application_services.build_application_services(
database_client=sqlite_session_factory,
deployment_edition=deployment_edition,
initialization_password="",
redis=MagicMock(spec=RedisClientWrapper),
)
@@ -33,9 +109,10 @@ def test_build_application_services_configures_setup_policy(
def test_build_application_services_wires_builtin_schema_definitions(
sqlite_session_factory: sessionmaker[Session],
) -> None:
services = build_application_services(
services = ext_application_services.build_application_services(
database_client=sqlite_session_factory,
deployment_edition=DeploymentEdition.COMMUNITY,
initialization_password="",
redis=MagicMock(spec=RedisClientWrapper),
)
@@ -49,9 +126,10 @@ def test_build_application_services_does_not_construct_schema_manager(
sqlite_session_factory: sessionmaker[Session],
) -> None:
with patch("extensions.ext_application_services.SchemaManager") as schema_manager:
build_application_services(
ext_application_services.build_application_services(
database_client=sqlite_session_factory,
deployment_edition=DeploymentEdition.COMMUNITY,
initialization_password="",
redis=MagicMock(spec=RedisClientWrapper),
)
@@ -0,0 +1,101 @@
"""Tests for initialization validation policy without Flask or persistence."""
from unittest.mock import Mock, create_autospec
import pytest
from services.init_validation_service import (
AlreadyInitializedError,
InitValidationService,
InitValidationState,
InvalidInitializationPasswordError,
)
@pytest.fixture
def state() -> Mock:
return create_autospec(InitValidationState, instance=True, spec_set=True)
def test_status_is_valid_when_validation_is_not_required(state: Mock) -> None:
service = InitValidationService(state=state, validation_required=False, expected_password="")
assert service.is_validated(session_validated=False) is True
state.is_setup.assert_not_called()
def test_status_is_valid_when_browser_session_was_validated(state: Mock) -> None:
service = InitValidationService(state=state, validation_required=True, expected_password="expected")
assert service.is_validated(session_validated=True) is True
state.is_setup.assert_not_called()
@pytest.mark.parametrize("setup_exists", [False, True])
def test_status_falls_back_to_persisted_setup_state(state: Mock, setup_exists: bool) -> None:
state.is_setup.return_value = setup_exists
service = InitValidationService(state=state, validation_required=True, expected_password="expected")
assert service.is_validated(session_validated=False) is setup_exists
state.is_setup.assert_called_once_with()
def test_password_validation_rejects_an_initialized_installation(state: Mock) -> None:
state.has_tenants.return_value = True
service = InitValidationService(state=state, validation_required=True, expected_password="expected")
with pytest.raises(AlreadyInitializedError):
service.validate_password("expected")
def test_initialized_installation_takes_precedence_over_a_password_mismatch(state: Mock) -> None:
state.has_tenants.return_value = True
service = InitValidationService(state=state, validation_required=True, expected_password="expected")
with pytest.raises(AlreadyInitializedError):
service.validate_password("wrong")
def test_password_validation_rejects_a_mismatch(state: Mock) -> None:
state.has_tenants.return_value = False
service = InitValidationService(state=state, validation_required=True, expected_password="expected")
with pytest.raises(InvalidInitializationPasswordError):
service.validate_password("wrong")
def test_password_validation_accepts_a_match(state: Mock) -> None:
state.has_tenants.return_value = False
service = InitValidationService(state=state, validation_required=True, expected_password="expected")
service.validate_password("expected")
state.has_tenants.assert_called_once_with()
@pytest.mark.parametrize("expected_password", ["", "expected"])
def test_password_validation_rejects_an_empty_password(state: Mock, expected_password: str) -> None:
state.has_tenants.return_value = False
service = InitValidationService(
state=state,
validation_required=bool(expected_password),
expected_password=expected_password,
)
with pytest.raises(InvalidInitializationPasswordError):
service.validate_password("")
def test_password_validation_rejects_a_password_when_no_password_is_configured(state: Mock) -> None:
state.has_tenants.return_value = False
service = InitValidationService(state=state, validation_required=False, expected_password="")
with pytest.raises(InvalidInitializationPasswordError):
service.validate_password("unexpected")
def test_password_validation_accepts_a_unicode_password(state: Mock) -> None:
state.has_tenants.return_value = False
service = InitValidationService(state=state, validation_required=True, expected_password="pässwörd-🔐")
service.validate_password("pässwörd-🔐")