feat(cloud): complete secure invitation experience

This commit is contained in:
dadachann
2026-07-26 13:54:42 +08:00
parent ff068564ab
commit e90a1546de
18 changed files with 214 additions and 46 deletions
@@ -86,3 +86,20 @@ async def test_environment_mapping_enables_provider_without_leaking_secret(monke
'https://env.langbot.example/invitations/accept#token=lbi_secret'
)
assert service.capability() == {'enabled': True, 'provider': 'smtp'}
async def test_cloud_invitation_email_has_branded_html_plain_fallback_and_expiry_copy():
service = InvitationDeliveryService(_app({}))
link = 'https://cloud.langbot.app/invitations/accept#token=lbi_secret&next=<unsafe>'
text = service._plain_text('Research & Development', link)
html = service._html('Research & Development', link)
assert 'LangBot Cloud' in text
assert 'Research & Development' in text
assert '7 days' in text
assert link in text
assert 'Accept invitation' in html
assert 'Research &amp; Development' in html
assert 'expires in 7 days' in html
assert 'lbi_secret&amp;next=&lt;unsafe&gt;' in html
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import datetime
import uuid
from contextlib import asynccontextmanager
from types import SimpleNamespace
@@ -19,6 +20,7 @@ from langbot.pkg.entity.persistence.workspace import (
)
from langbot.pkg.workspace.collaboration import (
InvitationEmailMismatchError,
InvitationExpiredError,
InvitationRoleError,
InvitationUsedError,
LastOwnerError,
@@ -159,6 +161,23 @@ async def test_invitation_rejects_owner_role_and_email_mismatch(collaboration_co
await service.accept_invitation(created.token, wrong_account.uuid)
async def test_expired_invitation_is_inspectable_before_periodic_cleanup_deletes_it(collaboration_context):
service, _, session_factory, _, workspace, owner_membership = collaboration_context
created = await service.create_invitation(
workspace.uuid, owner_membership, 'expired@example.com', 'viewer'
)
async with session_factory.begin() as session:
invitation = await session.get(WorkspaceInvitation, created.invitation.uuid)
invitation.expires_at = service._utcnow() - datetime.timedelta(minutes=1)
with pytest.raises(InvitationExpiredError):
await service.inspect_invitation(created.token)
assert await service.cleanup_expired_invitations(retention=datetime.timedelta(0)) == 1
async with session_factory() as session:
assert await session.get(WorkspaceInvitation, created.invitation.uuid) is None
async def test_last_owner_cannot_be_demoted(collaboration_context):
service, _, session_factory, _, workspace, owner_membership = collaboration_context
created = await service.create_invitation(