mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
121 lines
4.6 KiB
Python
121 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import uuid
|
|
from collections.abc import Callable
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from faker import Faker
|
|
from flask import Flask
|
|
from sqlalchemy.orm import Session
|
|
|
|
from controllers.openapi.auth.context import Context
|
|
from controllers.openapi.auth.loaders import PathParam, load_app
|
|
from controllers.openapi.auth.requirements import ResolveCaller
|
|
from controllers.openapi.auth.subjects import subject_from_auth
|
|
from libs.oauth_bearer import AuthContext, TokenType
|
|
from models import Account, Tenant
|
|
from services.account_service import AccountService, TenantService
|
|
from tests.test_containers_integration_tests.helpers import generate_valid_password
|
|
|
|
_CLIENT_ID = "integration-cli"
|
|
|
|
|
|
@pytest.fixture
|
|
def app(flask_app_with_containers: Flask) -> Flask:
|
|
return flask_app_with_containers
|
|
|
|
|
|
@pytest.fixture
|
|
def make_account(db_session_with_containers: Session) -> Callable[..., Account]:
|
|
"""Factory that registers a real Account and gives it an owner workspace.
|
|
|
|
System feature gates are stubbed (registration / workspace creation
|
|
allowed) exactly like the AppDslService integration tests, so this stays a
|
|
pure account+tenant setup helper.
|
|
"""
|
|
|
|
# Depend on db_session_with_containers so the app context / DB session is
|
|
# active for the real AccountService/TenantService calls below.
|
|
assert db_session_with_containers is not None
|
|
|
|
def _make(*, with_owner_tenant: bool = True) -> Account:
|
|
fake = Faker()
|
|
with patch("services.account_service.SystemFeatureService") as mock_feature_service:
|
|
mock_feature_service.is_registration_allowed.return_value = True
|
|
account = AccountService.create_account(
|
|
email=fake.email(),
|
|
name=fake.name(),
|
|
interface_language="en-US",
|
|
password=generate_valid_password(fake),
|
|
session=db_session_with_containers,
|
|
)
|
|
if with_owner_tenant:
|
|
TenantService.create_owner_tenant_if_not_exist(
|
|
account, name=fake.company(), session=db_session_with_containers
|
|
)
|
|
return account
|
|
|
|
return _make
|
|
|
|
|
|
def add_tenant_for_account(
|
|
account: Account, *, session: Session, role: str = "normal", name: str = "Second WS"
|
|
) -> Tenant:
|
|
"""Create an additional tenant and join ``account`` to it (real service calls)."""
|
|
with patch("services.account_service.SystemFeatureService") as mock_feature_service:
|
|
mock_feature_service.is_workspace_creation_allowed.return_value = True
|
|
tenant = TenantService.create_tenant(name=name, session=session)
|
|
TenantService.create_tenant_member(tenant, account, session, role=role)
|
|
return tenant
|
|
|
|
|
|
def _account_auth(
|
|
account: Account,
|
|
*,
|
|
token_id: uuid.UUID | None = None,
|
|
client_id: str = _CLIENT_ID,
|
|
) -> AuthContext:
|
|
"""The ``AuthContext`` a live ``dfoa_`` token for ``account`` would carry."""
|
|
return AuthContext(
|
|
subject_email=account.email,
|
|
subject_issuer=None,
|
|
account_id=uuid.UUID(str(account.id)),
|
|
client_id=client_id,
|
|
token_id=token_id or uuid.uuid4(),
|
|
token_type=TokenType.OAUTH_ACCOUNT,
|
|
expires_at=None,
|
|
)
|
|
|
|
|
|
def context_for(
|
|
account: Account,
|
|
*,
|
|
session: Session,
|
|
view_args: dict[str, str] | None = None,
|
|
token_id: uuid.UUID | None = None,
|
|
) -> Context:
|
|
"""Build the ``Context`` a handler is given after the pipeline ran.
|
|
|
|
The subject comes from a real ``AuthContext`` through ``subject_from_auth``,
|
|
so the helper walks the same resolution path the router does. ``view_args``
|
|
is the route's path params — the app and the workspace are loaded from it,
|
|
so a route carrying ``<app_id>`` needs ``{"app_id": ...}`` here.
|
|
``token_id`` only matters to the ``/account/sessions*`` family, which reads
|
|
it back off the subject.
|
|
|
|
It runs ``ResolveCaller`` itself — the requirement every pipeline fixes
|
|
last — rather than the wider set a route's own requirements would ask for,
|
|
so a handler sees exactly what the thinnest pipeline would give it. On a
|
|
route carrying ``<app_id>`` that thinnest pipeline is ``CheckAppApiEnabled``,
|
|
which loads the app; handlers read ``ctx.app`` and never load it themselves,
|
|
so the helper loads it through the same loader. Running the real pieces is
|
|
what keeps this CI-only helper from drifting away from the pipeline it
|
|
stands in for.
|
|
"""
|
|
ctx = Context(subject_from_auth(_account_auth(account, token_id=token_id)), session, view_args or {})
|
|
if PathParam.APP_ID in ctx.view_args:
|
|
load_app(ctx)
|
|
ResolveCaller().run(ctx.subject, ctx, session)
|
|
return ctx
|