08868e1e55
Add Behave BDD scenarios covering: - ServerLifecycle.start() with mocked uvicorn (full lifecycle) - run_server() convenience function with Settings defaults and overrides - Signal handler installation and non-main-thread edge case - TeamCollaborationService validation edge cases (empty args) - SessionRegistry validation edge cases and get_active_sessions - VersionConflictError negative version validation - Untracked resource version stamp creation path These tests cover previously uncovered validation branches and the server lifecycle start/shutdown paths to maintain >=97% coverage.
1040 lines
36 KiB
Python
1040 lines
36 KiB
Python
"""Step definitions for team collaboration feature tests.
|
|
|
|
Covers team roles, permissions, user sessions, session registry,
|
|
version stamps, conflict resolution, and the TeamCollaborationService.
|
|
|
|
Based on issue #863 — feat(server): team collaboration features.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from time import sleep
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
from pydantic import ValidationError
|
|
|
|
from cleveragents.application.services.team_collab_service import (
|
|
TeamCollaborationService,
|
|
)
|
|
from cleveragents.domain.models.core.team_collab import (
|
|
TEAM_ROLE_PERMISSIONS,
|
|
ConflictDetectionResult,
|
|
ConflictResolutionStrategy,
|
|
SessionRegistry,
|
|
TeamCollaborationError,
|
|
TeamMember,
|
|
TeamMemberExistsError,
|
|
TeamMemberNotFoundError,
|
|
TeamPermission,
|
|
TeamRole,
|
|
UserSession,
|
|
VersionConflictError,
|
|
VersionStamp,
|
|
)
|
|
|
|
ULID_RE = re.compile(r"^[0-9A-HJKMNP-TV-Z]{26}$")
|
|
|
|
|
|
# ── TeamRole enum ──────────────────────────────────────────────────
|
|
|
|
|
|
@given("the TeamRole enum")
|
|
def step_given_team_role_enum(context: Context) -> None:
|
|
context.enum_cls = TeamRole
|
|
|
|
|
|
@then('the TeamRole enum should contain "{value}"')
|
|
def step_then_team_role_contains(context: Context, value: str) -> None:
|
|
members = [m.value for m in TeamRole]
|
|
assert value in members, f"{value} not in TeamRole: {members}"
|
|
|
|
|
|
# ── TeamPermission enum ────────────────────────────────────────────
|
|
|
|
|
|
@given("the TeamPermission enum")
|
|
def step_given_team_permission_enum(context: Context) -> None:
|
|
context.enum_cls = TeamPermission
|
|
|
|
|
|
@then('the TeamPermission enum should contain "{value}"')
|
|
def step_then_team_permission_contains(context: Context, value: str) -> None:
|
|
members = [m.value for m in TeamPermission]
|
|
assert value in members, f"{value} not in TeamPermission: {members}"
|
|
|
|
|
|
# ── ConflictResolutionStrategy enum ────────────────────────────────
|
|
|
|
|
|
@given("the ConflictResolutionStrategy enum")
|
|
def step_given_conflict_strategy_enum(context: Context) -> None:
|
|
context.enum_cls = ConflictResolutionStrategy
|
|
|
|
|
|
@then('the ConflictResolutionStrategy enum should contain "{value}"')
|
|
def step_then_conflict_strategy_contains(context: Context, value: str) -> None:
|
|
members = [m.value for m in ConflictResolutionStrategy]
|
|
assert value in members, f"{value} not in ConflictResolutionStrategy: {members}"
|
|
|
|
|
|
# ── TEAM_ROLE_PERMISSIONS constant ─────────────────────────────────
|
|
|
|
|
|
@given("the TEAM_ROLE_PERMISSIONS constant")
|
|
def step_given_team_role_permissions(context: Context) -> None:
|
|
context.role_permissions = TEAM_ROLE_PERMISSIONS
|
|
|
|
|
|
@then('the "{role}" team role should have {count:d} allowed permissions')
|
|
def step_then_team_role_has_n_permissions(
|
|
context: Context, role: str, count: int
|
|
) -> None:
|
|
team_role = TeamRole(role)
|
|
perms = TEAM_ROLE_PERMISSIONS[team_role]
|
|
assert len(perms) == count, f"{role} has {len(perms)} permissions, expected {count}"
|
|
|
|
|
|
# ── TeamMember model ───────────────────────────────────────────────
|
|
|
|
|
|
@given('a team member "{user_id}" with role "{role}" and email "{email}"')
|
|
def step_given_team_member(
|
|
context: Context, user_id: str, role: str, email: str
|
|
) -> None:
|
|
context.team_member = TeamMember(
|
|
user_id=user_id,
|
|
display_name=user_id.title(),
|
|
email=email,
|
|
role=TeamRole(role),
|
|
)
|
|
|
|
|
|
@then('the team member user_id should be "{expected}"')
|
|
def step_then_member_user_id(context: Context, expected: str) -> None:
|
|
assert context.team_member.user_id == expected
|
|
|
|
|
|
@then('the team member role should be "{expected}"')
|
|
def step_then_member_role(context: Context, expected: str) -> None:
|
|
assert context.team_member.role == TeamRole(expected)
|
|
|
|
|
|
@then('the team member email should be "{expected}"')
|
|
def step_then_member_email(context: Context, expected: str) -> None:
|
|
assert context.team_member.email == expected
|
|
|
|
|
|
@when('I try to create a team member with email "{email}"')
|
|
def step_when_create_member_bad_email(context: Context, email: str) -> None:
|
|
context.validation_error = None
|
|
try:
|
|
TeamMember(
|
|
user_id="test",
|
|
display_name="Test",
|
|
email=email,
|
|
role=TeamRole.MEMBER,
|
|
)
|
|
except ValidationError as exc:
|
|
context.validation_error = exc
|
|
|
|
|
|
@then("a team member validation error should be raised")
|
|
def step_then_team_member_validation_error_raised(context: Context) -> None:
|
|
assert context.validation_error is not None, "Expected ValidationError"
|
|
|
|
|
|
@then('the team member should have "{permission}" permission')
|
|
def step_then_member_has_permission(context: Context, permission: str) -> None:
|
|
perm = TeamPermission(permission)
|
|
assert context.team_member.has_permission(perm), (
|
|
f"Expected member to have {permission}"
|
|
)
|
|
|
|
|
|
@then('the team member should not have "{permission}" permission')
|
|
def step_then_member_no_permission(context: Context, permission: str) -> None:
|
|
perm = TeamPermission(permission)
|
|
assert not context.team_member.has_permission(perm), (
|
|
f"Expected member NOT to have {permission}"
|
|
)
|
|
|
|
|
|
# ── UserSession model ──────────────────────────────────────────────
|
|
|
|
|
|
@given('a user session for user "{user_id}" on project "{project_id}"')
|
|
def step_given_user_session(context: Context, user_id: str, project_id: str) -> None:
|
|
context.user_session = UserSession(
|
|
session_id=str(SessionRegistry().generate_session_id()),
|
|
user_id=user_id,
|
|
display_name=user_id.title(),
|
|
project_id=project_id,
|
|
)
|
|
|
|
|
|
@then('the user session user_id should be "{expected}"')
|
|
def step_then_session_user_id(context: Context, expected: str) -> None:
|
|
assert context.user_session.user_id == expected
|
|
|
|
|
|
@then('the user session project_id should be "{expected}"')
|
|
def step_then_session_project_id(context: Context, expected: str) -> None:
|
|
assert context.user_session.project_id == expected
|
|
|
|
|
|
@then("the user session should be active")
|
|
def step_then_session_active(context: Context) -> None:
|
|
assert context.user_session.is_active is True
|
|
|
|
|
|
@when("the session is deactivated")
|
|
def step_when_session_deactivated(context: Context) -> None:
|
|
context.user_session.deactivate()
|
|
|
|
|
|
@then("the user session should not be active")
|
|
def step_then_session_not_active(context: Context) -> None:
|
|
assert context.user_session.is_active is False
|
|
|
|
|
|
@when("the session is touched")
|
|
def step_when_session_touched(context: Context) -> None:
|
|
context.old_last_active = context.user_session.last_active
|
|
sleep(0.01)
|
|
context.user_session.touch()
|
|
|
|
|
|
@then("the last_active timestamp should be updated")
|
|
def step_then_last_active_updated(context: Context) -> None:
|
|
assert context.user_session.last_active >= context.old_last_active
|
|
|
|
|
|
# ── VersionStamp model ─────────────────────────────────────────────
|
|
|
|
|
|
@given('a version stamp for resource "{resource_id}" by user "{user_id}"')
|
|
def step_given_version_stamp(context: Context, resource_id: str, user_id: str) -> None:
|
|
context.version_stamp = VersionStamp(
|
|
resource_id=resource_id,
|
|
updated_by=user_id,
|
|
)
|
|
|
|
|
|
@then("the version stamp version should be {expected:d}")
|
|
def step_then_stamp_version(context: Context, expected: int) -> None:
|
|
assert context.version_stamp.version == expected
|
|
|
|
|
|
@then('the version stamp updated_by should be "{expected}"')
|
|
def step_then_stamp_updated_by(context: Context, expected: str) -> None:
|
|
assert context.version_stamp.updated_by == expected
|
|
|
|
|
|
@when('the version stamp is incremented by "{user_id}"')
|
|
def step_when_stamp_incremented(context: Context, user_id: str) -> None:
|
|
context.new_version_stamp = context.version_stamp.increment(user_id)
|
|
|
|
|
|
@then("the new version stamp version should be {expected:d}")
|
|
def step_then_new_stamp_version(context: Context, expected: int) -> None:
|
|
assert context.new_version_stamp.version == expected
|
|
|
|
|
|
@then('the new version stamp updated_by should be "{expected}"')
|
|
def step_then_new_stamp_updated_by(context: Context, expected: str) -> None:
|
|
assert context.new_version_stamp.updated_by == expected
|
|
|
|
|
|
@then("incrementing with empty user should raise ValueError")
|
|
def step_then_increment_empty_raises(context: Context) -> None:
|
|
raised = False
|
|
try:
|
|
context.version_stamp.increment("")
|
|
except ValueError:
|
|
raised = True
|
|
assert raised, "Expected ValueError"
|
|
|
|
|
|
# ── ConflictDetectionResult ────────────────────────────────────────
|
|
|
|
|
|
@given('a conflict detection result with no conflict for "{resource_id}"')
|
|
def step_given_no_conflict_result(context: Context, resource_id: str) -> None:
|
|
context.conflict_result = ConflictDetectionResult(
|
|
detected=False,
|
|
resource_id=resource_id,
|
|
resolved=True,
|
|
details="No conflict",
|
|
)
|
|
|
|
|
|
@then("the result detected should be False")
|
|
def step_then_result_not_detected(context: Context) -> None:
|
|
assert context.conflict_result.detected is False
|
|
|
|
|
|
@then('the result resource_id should be "{expected}"')
|
|
def step_then_result_resource_id(context: Context, expected: str) -> None:
|
|
assert context.conflict_result.resource_id == expected
|
|
|
|
|
|
# ── VersionConflictError ───────────────────────────────────────────
|
|
|
|
|
|
@when(
|
|
'a VersionConflictError is raised for "{resource_id}" '
|
|
"expected {expected:d} actual {actual:d}"
|
|
)
|
|
def step_when_version_conflict_error(
|
|
context: Context, resource_id: str, expected: int, actual: int
|
|
) -> None:
|
|
context.conflict_error = VersionConflictError(
|
|
resource_id=resource_id,
|
|
expected_version=expected,
|
|
actual_version=actual,
|
|
)
|
|
|
|
|
|
@then('the error resource_id should be "{expected}"')
|
|
def step_then_error_resource_id(context: Context, expected: str) -> None:
|
|
assert context.conflict_error.resource_id == expected
|
|
|
|
|
|
@then("the error expected_version should be {expected:d}")
|
|
def step_then_error_expected_version(context: Context, expected: int) -> None:
|
|
assert context.conflict_error.expected_version == expected
|
|
|
|
|
|
@then("the error actual_version should be {expected:d}")
|
|
def step_then_error_actual_version(context: Context, expected: int) -> None:
|
|
assert context.conflict_error.actual_version == expected
|
|
|
|
|
|
@then("creating VersionConflictError with empty resource_id raises ValueError")
|
|
def step_then_conflict_error_empty_raises(context: Context) -> None:
|
|
raised = False
|
|
try:
|
|
VersionConflictError(resource_id="", expected_version=1, actual_version=2)
|
|
except ValueError:
|
|
raised = True
|
|
assert raised, "Expected ValueError"
|
|
|
|
|
|
# ── SessionRegistry ────────────────────────────────────────────────
|
|
|
|
|
|
@given("a session registry")
|
|
def step_given_session_registry(context: Context) -> None:
|
|
context.session_registry = SessionRegistry()
|
|
context.sessions = {}
|
|
|
|
|
|
@given('a user session "{session_id}" for user "{user_id}" on project "{project_id}"')
|
|
def step_given_named_session(
|
|
context: Context, session_id: str, user_id: str, project_id: str
|
|
) -> None:
|
|
session = UserSession(
|
|
session_id=session_id,
|
|
user_id=user_id,
|
|
display_name=user_id.title(),
|
|
project_id=project_id,
|
|
)
|
|
if not hasattr(context, "sessions") or context.sessions is None:
|
|
context.sessions = {}
|
|
context.sessions[session_id] = session
|
|
|
|
|
|
@when("the session is registered")
|
|
def step_when_session_registered(context: Context) -> None:
|
|
for session in context.sessions.values():
|
|
context.session_registry.register(session)
|
|
|
|
|
|
@when("all sessions are registered")
|
|
def step_when_all_sessions_registered(context: Context) -> None:
|
|
for session in context.sessions.values():
|
|
context.session_registry.register(session)
|
|
|
|
|
|
@then('the registry should contain the session "{session_id}"')
|
|
def step_then_registry_contains(context: Context, session_id: str) -> None:
|
|
result = context.session_registry.get(session_id)
|
|
assert result is not None, f"Session '{session_id}' not found"
|
|
|
|
|
|
@then('the registry should not contain the session "{session_id}"')
|
|
def step_then_registry_not_contains(context: Context, session_id: str) -> None:
|
|
result = context.session_registry.get(session_id)
|
|
assert result is None, f"Session '{session_id}' should not be found"
|
|
|
|
|
|
@then("the registry active count should be {expected:d}")
|
|
def step_then_registry_active_count(context: Context, expected: int) -> None:
|
|
assert context.session_registry.active_count == expected, (
|
|
f"Expected {expected}, got {context.session_registry.active_count}"
|
|
)
|
|
|
|
|
|
@when('the session "{session_id}" is unregistered')
|
|
def step_when_session_unregistered(context: Context, session_id: str) -> None:
|
|
context.session_registry.unregister(session_id)
|
|
|
|
|
|
@then('the registry should have {count:d} sessions for user "{user_id}"')
|
|
def step_then_registry_user_sessions(
|
|
context: Context, count: int, user_id: str
|
|
) -> None:
|
|
sessions = context.session_registry.get_sessions_for_user(user_id)
|
|
assert len(sessions) == count, (
|
|
f"Expected {count} sessions for user '{user_id}', got {len(sessions)}"
|
|
)
|
|
|
|
|
|
@then('the registry should have {count:d} sessions for project "{project_id}"')
|
|
def step_then_registry_project_sessions(
|
|
context: Context, count: int, project_id: str
|
|
) -> None:
|
|
sessions = context.session_registry.get_sessions_for_project(project_id)
|
|
assert len(sessions) == count, (
|
|
f"Expected {count} sessions for project '{project_id}', got {len(sessions)}"
|
|
)
|
|
|
|
|
|
@when("the registry is cleared")
|
|
def step_when_registry_cleared(context: Context) -> None:
|
|
context.session_registry.clear()
|
|
|
|
|
|
@then("registering the same session again raises ValueError")
|
|
def step_then_duplicate_registration_raises(context: Context) -> None:
|
|
session = next(iter(context.sessions.values()))
|
|
raised = False
|
|
try:
|
|
context.session_registry.register(session)
|
|
except ValueError:
|
|
raised = True
|
|
assert raised, "Expected ValueError for duplicate registration"
|
|
|
|
|
|
@then("the generated session ID should be a valid ULID")
|
|
def step_then_generated_id_valid(context: Context) -> None:
|
|
sid = context.session_registry.generate_session_id()
|
|
assert ULID_RE.match(sid), f"'{sid}' is not a valid ULID"
|
|
|
|
|
|
# ── TeamCollaborationService — member management ───────────────────
|
|
|
|
|
|
@given("a team collaboration service")
|
|
def step_given_collab_service(context: Context) -> None:
|
|
context.collab_service = TeamCollaborationService()
|
|
context.last_session = None
|
|
context.last_conflict_result = None
|
|
|
|
|
|
@when('I add member "{user_id}" with role "{role}" and email "{email}"')
|
|
def step_when_add_member(context: Context, user_id: str, role: str, email: str) -> None:
|
|
context.collab_service.add_member(
|
|
user_id=user_id,
|
|
display_name=user_id.title(),
|
|
email=email,
|
|
role=TeamRole(role),
|
|
)
|
|
|
|
|
|
@then("the service should have {count:d} members")
|
|
def step_then_service_member_count(context: Context, count: int) -> None:
|
|
members = context.collab_service.list_members()
|
|
assert len(members) == count, f"Expected {count} members, got {len(members)}"
|
|
|
|
|
|
@then('getting member "{user_id}" should return the member')
|
|
def step_then_get_member(context: Context, user_id: str) -> None:
|
|
member = context.collab_service.get_member(user_id)
|
|
assert member.user_id == user_id
|
|
|
|
|
|
@then('adding member "{user_id}" again raises TeamMemberExistsError')
|
|
def step_then_add_duplicate_raises(context: Context, user_id: str) -> None:
|
|
raised = False
|
|
try:
|
|
context.collab_service.add_member(
|
|
user_id=user_id,
|
|
display_name="Dup",
|
|
email="dup@example.com",
|
|
role=TeamRole.MEMBER,
|
|
)
|
|
except TeamMemberExistsError:
|
|
raised = True
|
|
assert raised, "Expected TeamMemberExistsError"
|
|
|
|
|
|
@when('I remove member "{user_id}"')
|
|
def step_when_remove_member(context: Context, user_id: str) -> None:
|
|
context.collab_service.remove_member(user_id)
|
|
|
|
|
|
@then('removing member "{user_id}" raises TeamMemberNotFoundError')
|
|
def step_then_remove_not_found_raises(context: Context, user_id: str) -> None:
|
|
raised = False
|
|
try:
|
|
context.collab_service.remove_member(user_id)
|
|
except TeamMemberNotFoundError:
|
|
raised = True
|
|
assert raised, "Expected TeamMemberNotFoundError"
|
|
|
|
|
|
@when('I update member "{user_id}" role to "{role}"')
|
|
def step_when_update_role(context: Context, user_id: str, role: str) -> None:
|
|
context.collab_service.update_member_role(user_id, TeamRole(role))
|
|
|
|
|
|
@then('member "{user_id}" should have role "{role}"')
|
|
def step_then_member_has_role(context: Context, user_id: str, role: str) -> None:
|
|
member = context.collab_service.get_member(user_id)
|
|
assert member.role == TeamRole(role), f"Expected role {role}, got {member.role}"
|
|
|
|
|
|
# ── TeamCollaborationService — permission checks ──────────────────
|
|
|
|
|
|
@then('member "{user_id}" should have "{permission}" team permission')
|
|
def step_then_service_member_has_perm(
|
|
context: Context, user_id: str, permission: str
|
|
) -> None:
|
|
result = context.collab_service.check_permission(
|
|
user_id, TeamPermission(permission)
|
|
)
|
|
assert result is True, f"Expected {user_id} to have {permission}"
|
|
|
|
|
|
@then('member "{user_id}" should not have "{permission}" team permission')
|
|
def step_then_service_member_no_perm(
|
|
context: Context, user_id: str, permission: str
|
|
) -> None:
|
|
result = context.collab_service.check_permission(
|
|
user_id, TeamPermission(permission)
|
|
)
|
|
assert result is False, f"Expected {user_id} NOT to have {permission}"
|
|
|
|
|
|
@then('enforcing "{permission}" on "{user_id}" raises PermissionError')
|
|
def step_then_enforce_raises(context: Context, permission: str, user_id: str) -> None:
|
|
raised = False
|
|
try:
|
|
context.collab_service.enforce_permission(user_id, TeamPermission(permission))
|
|
except PermissionError:
|
|
raised = True
|
|
assert raised, "Expected PermissionError"
|
|
|
|
|
|
# ── TeamCollaborationService — session management ─────────────────
|
|
|
|
|
|
@when('I register a session for "{user_id}" on project "{project_id}"')
|
|
def step_when_register_service_session(
|
|
context: Context, user_id: str, project_id: str
|
|
) -> None:
|
|
session = context.collab_service.register_session(
|
|
user_id=user_id,
|
|
project_id=project_id,
|
|
)
|
|
context.last_session = session
|
|
|
|
|
|
@then('the project "{project_id}" should have {count:d} active sessions')
|
|
def step_then_project_active_sessions(
|
|
context: Context, project_id: str, count: int
|
|
) -> None:
|
|
sessions = context.collab_service.get_active_sessions_for_project(project_id)
|
|
assert len(sessions) == count, (
|
|
f"Expected {count} active sessions for '{project_id}', got {len(sessions)}"
|
|
)
|
|
|
|
|
|
@when("I unregister the last session")
|
|
def step_when_unregister_last_session(context: Context) -> None:
|
|
context.collab_service.unregister_session(context.last_session.session_id)
|
|
|
|
|
|
# ── TeamCollaborationService — conflict resolution ─────────────────
|
|
|
|
|
|
@when('I create a version stamp for "{resource_id}" by "{user_id}"')
|
|
def step_when_create_stamp(context: Context, resource_id: str, user_id: str) -> None:
|
|
context.collab_service.create_version_stamp(resource_id, user_id)
|
|
|
|
|
|
@then('the version stamp for "{resource_id}" should have version {version:d}')
|
|
def step_then_stamp_has_version(
|
|
context: Context, resource_id: str, version: int
|
|
) -> None:
|
|
stamp = context.collab_service.get_version_stamp(resource_id)
|
|
assert stamp is not None, f"No stamp for '{resource_id}'"
|
|
assert stamp.version == version, f"Expected version {version}, got {stamp.version}"
|
|
|
|
|
|
@when('I update version for "{resource_id}" expecting {version:d} by "{user_id}"')
|
|
def step_when_update_version(
|
|
context: Context, resource_id: str, version: int, user_id: str
|
|
) -> None:
|
|
context.last_conflict_result = context.collab_service.update_version(
|
|
resource_id=resource_id,
|
|
expected_version=version,
|
|
user_id=user_id,
|
|
)
|
|
|
|
|
|
@then(
|
|
'updating "{resource_id}" expecting {version:d} by "{user_id}" '
|
|
"with reject raises VersionConflictError"
|
|
)
|
|
def step_then_update_reject_raises(
|
|
context: Context, resource_id: str, version: int, user_id: str
|
|
) -> None:
|
|
raised = False
|
|
try:
|
|
context.collab_service.update_version(
|
|
resource_id=resource_id,
|
|
expected_version=version,
|
|
user_id=user_id,
|
|
strategy=ConflictResolutionStrategy.REJECT,
|
|
)
|
|
except VersionConflictError:
|
|
raised = True
|
|
assert raised, "Expected VersionConflictError"
|
|
|
|
|
|
@when(
|
|
'I update "{resource_id}" expecting {version:d} by "{user_id}" '
|
|
"with last_writer_wins"
|
|
)
|
|
def step_when_update_lww(
|
|
context: Context, resource_id: str, version: int, user_id: str
|
|
) -> None:
|
|
context.last_conflict_result = context.collab_service.update_version(
|
|
resource_id=resource_id,
|
|
expected_version=version,
|
|
user_id=user_id,
|
|
strategy=ConflictResolutionStrategy.LAST_WRITER_WINS,
|
|
)
|
|
|
|
|
|
@when('I update "{resource_id}" expecting {version:d} by "{user_id}" with merge')
|
|
def step_when_update_merge(
|
|
context: Context, resource_id: str, version: int, user_id: str
|
|
) -> None:
|
|
context.last_conflict_result = context.collab_service.update_version(
|
|
resource_id=resource_id,
|
|
expected_version=version,
|
|
user_id=user_id,
|
|
strategy=ConflictResolutionStrategy.MERGE,
|
|
)
|
|
|
|
|
|
@then("the conflict result should be detected and resolved")
|
|
def step_then_conflict_detected_resolved(context: Context) -> None:
|
|
result = context.last_conflict_result
|
|
assert result.detected is True, "Expected conflict detected"
|
|
assert result.resolved is True, "Expected conflict resolved"
|
|
|
|
|
|
@then('detecting conflict on "{resource_id}" expecting {version:d} should return True')
|
|
def step_then_detect_conflict_true(
|
|
context: Context, resource_id: str, version: int
|
|
) -> None:
|
|
result = context.collab_service.detect_conflict(resource_id, version)
|
|
assert result is True, "Expected conflict detected"
|
|
|
|
|
|
@then('detecting conflict on "{resource_id}" expecting {version:d} should return False')
|
|
def step_then_detect_conflict_false(
|
|
context: Context, resource_id: str, version: int
|
|
) -> None:
|
|
result = context.collab_service.detect_conflict(resource_id, version)
|
|
assert result is False, "Expected no conflict"
|
|
|
|
|
|
# ── Error hierarchy ────────────────────────────────────────────────
|
|
|
|
|
|
@then("TeamMemberNotFoundError should be a subclass of TeamCollaborationError")
|
|
def step_then_not_found_is_subclass(context: Context) -> None:
|
|
assert issubclass(TeamMemberNotFoundError, TeamCollaborationError)
|
|
|
|
|
|
@then("TeamMemberExistsError should be a subclass of TeamCollaborationError")
|
|
def step_then_exists_is_subclass(context: Context) -> None:
|
|
assert issubclass(TeamMemberExistsError, TeamCollaborationError)
|
|
|
|
|
|
@then("VersionConflictError should be a subclass of TeamCollaborationError")
|
|
def step_then_conflict_is_subclass(context: Context) -> None:
|
|
assert issubclass(VersionConflictError, TeamCollaborationError)
|
|
|
|
|
|
# ── Service edge-case validations ──────────────────────────────────
|
|
|
|
|
|
@given("a fresh TeamCollaborationService")
|
|
def step_fresh_service(context: Context) -> None:
|
|
context.service = TeamCollaborationService()
|
|
|
|
|
|
@then("the service session_registry should be a SessionRegistry instance")
|
|
def step_check_session_registry_type(context: Context) -> None:
|
|
assert isinstance(context.service.session_registry, SessionRegistry)
|
|
|
|
|
|
@when("I try to add a member with empty user_id via service")
|
|
def step_add_member_empty_uid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.add_member(
|
|
user_id="", display_name="X", email="x@x.com", role=TeamRole.MEMBER
|
|
)
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to add a member with empty display_name via service")
|
|
def step_add_member_empty_name(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.add_member(
|
|
user_id="u1", display_name="", email="x@x.com", role=TeamRole.MEMBER
|
|
)
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to add a member with empty email via service")
|
|
def step_add_member_empty_email(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.add_member(
|
|
user_id="u1", display_name="X", email="", role=TeamRole.MEMBER
|
|
)
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@then('a collab ValueError should be raised mentioning "{fragment}"')
|
|
def step_check_value_error_msg(context: Context, fragment: str) -> None:
|
|
assert context.caught_exception is not None, "Expected ValueError"
|
|
assert isinstance(context.caught_exception, ValueError)
|
|
assert fragment in str(context.caught_exception), (
|
|
f"Expected '{fragment}' in '{context.caught_exception}'"
|
|
)
|
|
|
|
|
|
@when("I try to remove a member with empty user_id via service")
|
|
def step_remove_member_empty_uid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.remove_member(user_id="")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to get a member with empty user_id via service")
|
|
def step_get_member_empty_uid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.get_member(user_id="")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when('I try to get member "{uid}" via service')
|
|
def step_get_member_missing(context: Context, uid: str) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.get_member(user_id=uid)
|
|
except TeamMemberNotFoundError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@then("a TeamMemberNotFoundError should be raised")
|
|
def step_check_not_found_error(context: Context) -> None:
|
|
assert context.caught_exception is not None, "Expected TeamMemberNotFoundError"
|
|
assert isinstance(context.caught_exception, TeamMemberNotFoundError)
|
|
|
|
|
|
@when("I try to update role with empty user_id via service")
|
|
def step_update_role_empty_uid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.update_member_role(user_id="", new_role=TeamRole.ADMIN)
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to register session with empty user_id via service")
|
|
def step_register_session_empty_uid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.register_session(user_id="", project_id="p1")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@given('a team member "{uid}" with role "{role}"')
|
|
def step_add_team_member(context: Context, uid: str, role: str) -> None:
|
|
context.service.add_member(
|
|
user_id=uid,
|
|
display_name=f"User {uid}",
|
|
email=f"{uid}@test.com",
|
|
role=TeamRole(role),
|
|
)
|
|
|
|
|
|
@when('I try to register session with empty project_id for user "{uid}"')
|
|
def step_register_session_empty_pid(context: Context, uid: str) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.register_session(user_id=uid, project_id="")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to unregister session with empty session_id")
|
|
def step_unregister_session_empty_sid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.unregister_session(session_id="")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to get sessions for empty project_id")
|
|
def step_get_sessions_empty_pid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.get_active_sessions_for_project(project_id="")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to create version stamp with empty resource_id")
|
|
def step_create_stamp_empty_rid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.create_version_stamp(resource_id="", user_id="u1")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to create version stamp with empty user_id")
|
|
def step_create_stamp_empty_uid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.create_version_stamp(resource_id="r1", user_id="")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to get version stamp with empty resource_id")
|
|
def step_get_stamp_empty_rid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.get_version_stamp(resource_id="")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to update version with empty resource_id")
|
|
def step_update_version_empty_rid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.update_version(resource_id="", expected_version=1, user_id="u1")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to update version with empty user_id")
|
|
def step_update_version_empty_uid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.update_version(resource_id="r1", expected_version=1, user_id="")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to update version with expected_version 0")
|
|
def step_update_version_zero(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.update_version(
|
|
resource_id="r1", expected_version=0, user_id="u1"
|
|
)
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when(
|
|
'I update version for untracked resource "{rid}" by user "{uid}" expecting version {ver:d}'
|
|
)
|
|
def step_update_version_untracked(
|
|
context: Context, rid: str, uid: str, ver: int
|
|
) -> None:
|
|
context.conflict_result = context.service.update_version(
|
|
resource_id=rid, expected_version=ver, user_id=uid
|
|
)
|
|
|
|
|
|
@then("the result should indicate no conflict detected")
|
|
def step_check_no_conflict(context: Context) -> None:
|
|
assert not context.conflict_result.detected, "Expected no conflict"
|
|
|
|
|
|
@then('the result details should contain "{text}"')
|
|
def step_check_result_details(context: Context, text: str) -> None:
|
|
assert text in (context.conflict_result.details or ""), (
|
|
f"Expected '{text}' in '{context.conflict_result.details}'"
|
|
)
|
|
|
|
|
|
@when("I try to detect conflict with empty resource_id")
|
|
def step_detect_conflict_empty_rid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.detect_conflict(resource_id="", expected_version=1)
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to detect conflict with expected_version 0")
|
|
def step_detect_conflict_zero(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.service.detect_conflict(resource_id="r1", expected_version=0)
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when('I detect conflict for untracked resource "{rid}" expecting version {ver:d}')
|
|
def step_detect_conflict_untracked(context: Context, rid: str, ver: int) -> None:
|
|
context.conflict_bool = context.service.detect_conflict(
|
|
resource_id=rid, expected_version=ver
|
|
)
|
|
|
|
|
|
@then("the conflict result should be False")
|
|
def step_check_conflict_false(context: Context) -> None:
|
|
assert context.conflict_bool is False
|
|
|
|
|
|
# ── Domain model edge-case validations ─────────────────────────────
|
|
|
|
|
|
@when("I try to create VersionConflictError with expected_version -1")
|
|
def step_vce_negative_expected(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
VersionConflictError(resource_id="r1", expected_version=-1, actual_version=1)
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to create VersionConflictError with actual_version -1")
|
|
def step_vce_negative_actual(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
VersionConflictError(resource_id="r1", expected_version=1, actual_version=-1)
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@given("a fresh SessionRegistry")
|
|
def step_fresh_registry(context: Context) -> None:
|
|
context.registry = SessionRegistry()
|
|
|
|
|
|
@when("I try to register a session with empty session_id")
|
|
def step_register_empty_sid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
session = UserSession(
|
|
session_id="",
|
|
user_id="u1",
|
|
display_name="User 1",
|
|
project_id="p1",
|
|
)
|
|
context.registry.register(session)
|
|
except (ValueError, ValidationError) as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to unregister a session with empty session_id")
|
|
def step_unregister_empty_sid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.registry.unregister("")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when('I unregister session "{sid}"')
|
|
def step_unregister_unknown(context: Context, sid: str) -> None:
|
|
context.unregister_result = context.registry.unregister(sid)
|
|
|
|
|
|
@then("the unregister result should be False")
|
|
def step_check_unregister_false(context: Context) -> None:
|
|
assert context.unregister_result is False
|
|
|
|
|
|
@when("I try to get session with empty session_id")
|
|
def step_get_session_empty_sid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.registry.get("")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@given('a registered session "{sid}" for user "{uid}" in project "{pid}"')
|
|
def step_register_session_in_registry(
|
|
context: Context, sid: str, uid: str, pid: str
|
|
) -> None:
|
|
session = UserSession(
|
|
session_id=sid,
|
|
user_id=uid,
|
|
display_name=f"User {uid}",
|
|
project_id=pid,
|
|
)
|
|
context.registry.register(session)
|
|
|
|
|
|
@when('I deactivate session "{sid}" in the registry')
|
|
def step_deactivate_session(context: Context, sid: str) -> None:
|
|
session = context.registry.get(sid)
|
|
session.deactivate()
|
|
|
|
|
|
@then("get_active_sessions should return {count:d} session")
|
|
def step_check_active_count(context: Context, count: int) -> None:
|
|
active = context.registry.get_active_sessions()
|
|
assert len(active) == count, f"Expected {count} active sessions, got {len(active)}"
|
|
|
|
|
|
@when("I try to get sessions for empty user_id")
|
|
def step_get_sessions_empty_uid(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.registry.get_sessions_for_user("")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|
|
|
|
|
|
@when("I try to get sessions for empty project_id via registry")
|
|
def step_get_sessions_empty_pid_registry(context: Context) -> None:
|
|
context.caught_exception = None
|
|
try:
|
|
context.registry.get_sessions_for_project("")
|
|
except ValueError as exc:
|
|
context.caught_exception = exc
|