mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-09 04:40:57 +00:00
Merge remote-tracking branch 'origin/master' into dev/4.11.x
# Conflicts: # pyproject.toml # src/langbot/pkg/pipeline/controller.py # uv.lock # web/src/app/home/bots/components/bot-form/BotForm.tsx # web/src/app/home/components/home-sidebar/HomeSidebar.tsx # web/src/app/home/components/home-sidebar/SidebarDataContext.tsx # web/src/app/home/plugin-pages/page.tsx # web/src/app/infra/entities/api/index.ts
This commit is contained in:
@@ -9,6 +9,8 @@ Run: uv run pytest tests/integration/api/test_smoke.py -q
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, Mock
|
||||
|
||||
@@ -304,12 +306,34 @@ class TestUserInitEndpoint:
|
||||
data = await response.get_json()
|
||||
assert data['data'] == {
|
||||
'initialized': True,
|
||||
'authenticated_invitation_acceptance_enabled': False,
|
||||
'password_login_enabled': True,
|
||||
'space_login_enabled': False,
|
||||
}
|
||||
fake_api_app.user_service.get_login_capabilities.assert_awaited_once_with()
|
||||
fake_api_app.user_service.get_first_user.assert_not_awaited()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_account_info_enables_authenticated_invitation_acceptance_in_cloud(
|
||||
self, quart_test_client, fake_api_app
|
||||
):
|
||||
fake_api_app.deployment = SimpleNamespace(mode='cloud')
|
||||
fake_api_app.user_service.is_initialized.return_value = True
|
||||
fake_api_app.user_service.get_login_capabilities = AsyncMock(
|
||||
return_value={'password_login_enabled': True, 'space_login_enabled': True}
|
||||
)
|
||||
|
||||
response = await quart_test_client.get('/api/v1/user/account-info')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = await response.get_json()
|
||||
assert data['data'] == {
|
||||
'initialized': True,
|
||||
'authenticated_invitation_acceptance_enabled': True,
|
||||
'password_login_enabled': False,
|
||||
'space_login_enabled': True,
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_recovery_key_resets_any_existing_account(self, quart_test_client, fake_api_app, monkeypatch):
|
||||
fake_api_app.user_service.is_initialized.return_value = True
|
||||
|
||||
@@ -333,7 +333,6 @@ async def test_support_admin_request_context_has_actor_owner_and_no_membership(s
|
||||
assert Permission.RESOURCE_MANAGE.value in permissions
|
||||
assert not permissions.intersection(
|
||||
{
|
||||
Permission.OWNER_TRANSFER.value,
|
||||
Permission.MEMBER_VIEW.value,
|
||||
Permission.MEMBER_INVITE.value,
|
||||
Permission.MEMBER_UPDATE_ROLE.value,
|
||||
|
||||
@@ -272,9 +272,10 @@ async def test_space_credits_are_resolved_from_workspace_owner(space_oauth_api):
|
||||
'/api/v1/user/space-credits',
|
||||
headers={'Authorization': 'Bearer account-token', 'X-Workspace-Id': WORKSPACE_UUID},
|
||||
)
|
||||
payload = await response.get_json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert (await response.get_json())['data'] == {
|
||||
assert payload['data'] == {
|
||||
'credits': 25000,
|
||||
'owner_space_bound': True,
|
||||
'is_workspace_owner': True,
|
||||
@@ -282,6 +283,31 @@ async def test_space_credits_are_resolved_from_workspace_owner(space_oauth_api):
|
||||
application.space_service.get_credits.assert_awaited_once_with('owner@example.com')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_workspace_owner_is_always_space_bound_after_login(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
application.deployment.mode = 'cloud'
|
||||
application.user_service.get_workspace_owner = AsyncMock(return_value=None)
|
||||
application.space_service.get_credits = AsyncMock()
|
||||
application.cloud_model_catalog_service = SimpleNamespace(
|
||||
get_workspace_credits=lambda workspace_uuid: 25000 if workspace_uuid == WORKSPACE_UUID else None
|
||||
)
|
||||
|
||||
response = await client.get(
|
||||
'/api/v1/user/space-credits',
|
||||
headers={'Authorization': 'Bearer account-token', 'X-Workspace-Id': WORKSPACE_UUID},
|
||||
)
|
||||
payload = await response.get_json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert payload['data'] == {
|
||||
'credits': 25000,
|
||||
'owner_space_bound': True,
|
||||
'is_workspace_owner': True,
|
||||
}
|
||||
application.space_service.get_credits.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bind_callback_uses_opaque_state_and_never_treats_it_as_jwt(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
|
||||
@@ -188,6 +188,7 @@ async def test_owner_invites_second_account_and_secret_is_not_persisted(workspac
|
||||
workspace_uuid = current['workspace']['uuid']
|
||||
assert current['membership']['role'] == 'owner'
|
||||
assert 'member.invite' in current['permissions']
|
||||
assert 'owner.transfer' not in current['permissions']
|
||||
|
||||
invite_response = await client.post(
|
||||
f'/api/v1/workspaces/{workspace_uuid}/invitations',
|
||||
@@ -263,6 +264,14 @@ async def test_owner_invites_second_account_and_secret_is_not_persisted(workspac
|
||||
assert member_current['membership']['role'] == 'viewer'
|
||||
assert 'member.invite' not in member_current['permissions']
|
||||
|
||||
transfer_response = await client.patch(
|
||||
f'/api/v1/workspaces/{workspace_uuid}/members/{member_current["membership"]["account_uuid"]}',
|
||||
headers=_auth(owner_token, workspace_uuid),
|
||||
json={'role': 'owner'},
|
||||
)
|
||||
assert transfer_response.status_code == 403
|
||||
assert (await transfer_response.get_json())['code'] == 'permission_denied'
|
||||
|
||||
forbidden_invite = await client.post(
|
||||
f'/api/v1/workspaces/{workspace_uuid}/invitations',
|
||||
headers=_auth(member_token, workspace_uuid),
|
||||
|
||||
@@ -95,6 +95,30 @@ class TestSQLiteMigrationBaseline:
|
||||
class TestSQLiteMigrationUpgrade:
|
||||
"""Tests for upgrade to head workflow."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upgrade_from_published_space_launch_head_to_merged_head(self, sqlite_engine):
|
||||
"""A database released at the production-only 0016 head must remain upgradable."""
|
||||
async with sqlite_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
await run_alembic_stamp(sqlite_engine, '0016_space_launch_replay')
|
||||
await run_alembic_upgrade(sqlite_engine, 'head')
|
||||
|
||||
assert await get_alembic_current(sqlite_engine) == _get_script_head()
|
||||
assert _get_script_head() == '0020_merge_agent_cloud_heads'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upgrade_from_development_workspace_head_to_merged_head(self, sqlite_engine):
|
||||
"""A database at the 4.11 development head must absorb later master migrations."""
|
||||
async with sqlite_engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
await run_alembic_stamp(sqlite_engine, '0018_merge_workspace_heads')
|
||||
await run_alembic_upgrade(sqlite_engine, 'head')
|
||||
|
||||
assert await get_alembic_current(sqlite_engine) == _get_script_head()
|
||||
assert _get_script_head() == '0020_merge_agent_cloud_heads'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upgrade_from_baseline_to_head(self, sqlite_engine):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
from langbot.pkg.entity.persistence.base import Base
|
||||
from langbot.pkg.entity.persistence.user import User
|
||||
from langbot.pkg.entity.persistence.workspace import Workspace, WorkspaceMembership
|
||||
from langbot.pkg.persistence.alembic_runner import run_alembic_stamp, run_alembic_upgrade
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_owner_migration_demotes_historical_extra_owner_and_installs_unique_index(tmp_path):
|
||||
engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "single-owner.db"}')
|
||||
try:
|
||||
async with engine.begin() as connection:
|
||||
await connection.run_sync(Base.metadata.create_all)
|
||||
await connection.execute(sa.text('DROP INDEX uq_workspace_memberships_one_active_owner'))
|
||||
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
workspace_uuid = '00000000-0000-4000-8000-000000000001'
|
||||
creator_uuid = '00000000-0000-4000-8000-000000000010'
|
||||
promoted_uuid = '00000000-0000-4000-8000-000000000020'
|
||||
async with session_factory() as session:
|
||||
session.add_all(
|
||||
[
|
||||
User(
|
||||
uuid=creator_uuid,
|
||||
user='creator@example.test',
|
||||
normalized_email='creator@example.test',
|
||||
password='hash',
|
||||
account_type='local',
|
||||
),
|
||||
User(
|
||||
uuid=promoted_uuid,
|
||||
user='promoted@example.test',
|
||||
normalized_email='promoted@example.test',
|
||||
password='hash',
|
||||
account_type='local',
|
||||
),
|
||||
Workspace(
|
||||
uuid=workspace_uuid,
|
||||
instance_uuid='instance-test',
|
||||
name='Workspace',
|
||||
slug='workspace',
|
||||
type='team',
|
||||
status='active',
|
||||
source='local',
|
||||
created_by_account_uuid=creator_uuid,
|
||||
),
|
||||
WorkspaceMembership(
|
||||
uuid='00000000-0000-4000-8000-000000000100',
|
||||
workspace_uuid=workspace_uuid,
|
||||
account_uuid=creator_uuid,
|
||||
role='owner',
|
||||
status='active',
|
||||
),
|
||||
WorkspaceMembership(
|
||||
uuid='00000000-0000-4000-8000-000000000200',
|
||||
workspace_uuid=workspace_uuid,
|
||||
account_uuid=promoted_uuid,
|
||||
role='owner',
|
||||
status='active',
|
||||
),
|
||||
]
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
await run_alembic_stamp(engine, '0018_merge_launch_replay')
|
||||
await run_alembic_upgrade(engine, 'head')
|
||||
|
||||
async with engine.connect() as connection:
|
||||
roles = dict(
|
||||
(
|
||||
await connection.execute(
|
||||
sa.text(
|
||||
'SELECT account_uuid, role FROM workspace_memberships '
|
||||
'WHERE workspace_uuid = :workspace_uuid ORDER BY account_uuid'
|
||||
),
|
||||
{'workspace_uuid': workspace_uuid},
|
||||
)
|
||||
).all()
|
||||
)
|
||||
assert roles == {creator_uuid: 'owner', promoted_uuid: 'admin'}
|
||||
indexes = await connection.run_sync(
|
||||
lambda sync_connection: {
|
||||
index['name'] for index in sa.inspect(sync_connection).get_indexes('workspace_memberships')
|
||||
}
|
||||
)
|
||||
assert 'uq_workspace_memberships_one_active_owner' in indexes
|
||||
|
||||
with pytest.raises(sa.exc.IntegrityError):
|
||||
async with engine.begin() as connection:
|
||||
await connection.execute(
|
||||
sa.text("UPDATE workspace_memberships SET role = 'owner' WHERE account_uuid = :account_uuid"),
|
||||
{'account_uuid': promoted_uuid},
|
||||
)
|
||||
finally:
|
||||
await engine.dispose()
|
||||
@@ -180,7 +180,7 @@ async def test_workspace_upgrade_repairs_ownerless_existing_local_workspace(lega
|
||||
assert membership['status'] == 'active'
|
||||
|
||||
|
||||
async def test_published_owner_repair_revision_upgrades_to_merged_head(legacy_engine):
|
||||
async def test_development_owner_repair_revision_upgrades_to_merged_head(legacy_engine):
|
||||
await run_alembic_upgrade(legacy_engine, '0017_local_owner_repair')
|
||||
assert await get_alembic_current(legacy_engine) == '0017_local_owner_repair'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user