"""
Multi-User Workflow Definitions for OAuth Load Testing.
Defines coordinated workflows that span multiple users, simulating realistic
collaborative scenarios like note sharing, file collaboration, and permission management.
"""
import json
import logging
import random
import time
import uuid
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Awaitable, Callable
import anyio
from tests.load.oauth_pool import UserSessionWrapper
logger = logging.getLogger(__name__)
@dataclass
class WorkflowStepResult:
"""Result of a single workflow step."""
step_name: str
user: str
success: bool
duration: float
error: str | None = None
data: dict[str, Any] = field(default_factory=dict)
@dataclass
class WorkflowResult:
"""Result of a complete workflow execution."""
workflow_name: str
success: bool
total_duration: float
steps: list[WorkflowStepResult]
participants: list[str]
error: str | None = None
@property
def steps_completed(self) -> int:
"""Count of successfully completed steps."""
return sum(1 for step in self.steps if step.success)
@property
def step_latencies(self) -> dict[str, float]:
"""Map of step names to their durations."""
return {step.step_name: step.duration for step in self.steps}
class Workflow(ABC):
"""
Base class for multi-user workflows.
A workflow represents a coordinated sequence of operations across multiple users,
such as creating and sharing a note, collaborative editing, or permission management.
"""
def __init__(self, name: str):
self.name = name
self.steps: list[WorkflowStepResult] = []
self.start_time: float | None = None
@abstractmethod
async def execute(self, users: list[UserSessionWrapper]) -> WorkflowResult:
"""
Execute the workflow with the given users.
Args:
users: List of UserSessionWrapper instances to use in the workflow
Returns:
WorkflowResult with execution details
"""
pass
async def _execute_step(
self,
step_name: str,
user: UserSessionWrapper,
operation: Callable[..., Awaitable[Any]],
**kwargs,
) -> WorkflowStepResult:
"""
Execute a single workflow step with timing and error handling.
Args:
step_name: Name of the step for reporting
user: User executing the step
operation: Async callable to execute
**kwargs: Arguments to pass to the operation
Returns:
WorkflowStepResult
"""
start = time.time()
try:
result = await operation(**kwargs)
duration = time.time() - start
step_result = WorkflowStepResult(
step_name=step_name,
user=user.username,
success=True,
duration=duration,
data={"result": result} if result else {},
)
self.steps.append(step_result)
return step_result
except Exception as e:
duration = time.time() - start
logger.error(f"Step {step_name} failed for user {user.username}: {e}")
step_result = WorkflowStepResult(
step_name=step_name,
user=user.username,
success=False,
duration=duration,
error=str(e),
)
self.steps.append(step_result)
return step_result
def _finish(self, success: bool, error: str | None = None) -> WorkflowResult:
"""
Finalize workflow and create result.
Args:
success: Whether the overall workflow succeeded
error: Optional error message
Returns:
WorkflowResult
"""
duration = time.time() - self.start_time if self.start_time else 0.0
participants = list(set(step.user for step in self.steps))
return WorkflowResult(
workflow_name=self.name,
success=success,
total_duration=duration,
steps=self.steps,
participants=participants,
error=error,
)
class NoteShareWorkflow(Workflow):
"""
Workflow: User A creates a note and shares it with User B, who then reads it.
Steps:
1. User A creates a note
2. User A shares the note with User B (read-only)
3. User B lists their shared notes (verify propagation)
4. User B reads the shared note
"""
def __init__(self):
super().__init__("note_share")
async def execute(self, users: list[UserSessionWrapper]) -> WorkflowResult:
"""Execute note sharing workflow."""
self.start_time = time.time()
if len(users) < 2:
return self._finish(False, error="Requires at least 2 users")
user_a, user_b = users[0], users[1]
unique_id = uuid.uuid4().hex[:8]
try:
# Step 1: User A creates note
create_result = await self._execute_step(
"create_note",
user_a,
lambda: user_a.call_tool(
"nc_notes_create_note",
{
"title": f"Shared Note {unique_id}",
"content": f"Content for workflow test {unique_id}",
"category": "Workflows",
},
),
)
if not create_result.success:
return self._finish(False, error="Failed to create note")
# Extract note ID
note_data = json.loads(create_result.data["result"].content[0].text)
note_id = note_data["id"]
# Step 2: User A shares note with User B
# Note: Sharing files/notes requires using WebDAV path
# Create a file first, then share it
share_result = await self._execute_step(
"share_note",
user_a,
lambda: user_a.call_tool(
"nc_share_create",
{
"path": f"/Notes/{note_data['category']}/{note_data['title']}.txt",
"share_with": user_b.username,
"share_type": 0, # User share
"permissions": 1, # Read-only
},
),
)
if not share_result.success:
logger.warning("Share creation failed, continuing anyway")
# Step 3: User B lists shares (measure propagation)
await self._execute_step(
"list_shared_with_me",
user_b,
lambda: user_b.call_tool("nc_share_list", {"shared_with_me": True}),
)
# Step 4: User B reads the note
await self._execute_step(
"read_shared_note",
user_b,
lambda: user_b.call_tool("nc_notes_get_note", {"note_id": note_id}),
)
# Cleanup: Delete the note
await user_a.call_tool("nc_notes_delete_note", {"note_id": note_id})
return self._finish(success=True)
except Exception as e:
logger.error(f"Note share workflow failed: {e}")
return self._finish(False, error=str(e))
class CollaborativeEditWorkflow(Workflow):
"""
Workflow: Multiple users edit the same note concurrently.
Steps:
1. User A creates a note
2. User A shares note with Users B, C (edit permissions)
3. All users read the note simultaneously
4. All users update the note simultaneously (test concurrent edits)
5. User A verifies final state
"""
def __init__(self):
super().__init__("collaborative_edit")
async def execute(self, users: list[UserSessionWrapper]) -> WorkflowResult:
"""Execute collaborative editing workflow."""
self.start_time = time.time()
if len(users) < 2:
return self._finish(False, error="Requires at least 2 users")
owner = users[0]
collaborators = users[1:]
unique_id = uuid.uuid4().hex[:8]
try:
# Step 1: Owner creates note
create_result = await self._execute_step(
"create_note",
owner,
lambda: owner.call_tool(
"nc_notes_create_note",
{
"title": f"Collab Note {unique_id}",
"content": f"Initial content {unique_id}",
"category": "Collaboration",
},
),
)
if not create_result.success:
return self._finish(False, error="Failed to create note")
note_data = json.loads(create_result.data["result"].content[0].text)
note_id = note_data["id"]
# Step 2: Read note concurrently by all users
read_tasks = []
for i, user in enumerate(users):
read_tasks.append(
self._execute_step(
f"concurrent_read_{i}",
user,
lambda uid=note_id: user.call_tool(
"nc_notes_get_note", {"note_id": uid}
),
)
)
async with anyio.create_task_group() as tg:
for task in read_tasks:
tg.start_soon(task)
# Step 3: Append content concurrently by all collaborators
append_tasks = []
for i, user in enumerate(collaborators):
append_tasks.append(
self._execute_step(
f"concurrent_append_{i}",
user,
lambda _=i, u=user: u.call_tool(
"nc_notes_append_content",
{
"note_id": note_id,
"content": f"Addition from {u.username} at {time.time()}",
},
),
)
)
async with anyio.create_task_group() as tg:
for task in append_tasks:
tg.start_soon(task)
# Step 4: Owner verifies final state
await self._execute_step(
"verify_final_state",
owner,
lambda: owner.call_tool("nc_notes_get_note", {"note_id": note_id}),
)
# Cleanup
await owner.call_tool("nc_notes_delete_note", {"note_id": note_id})
return self._finish(success=True)
except Exception as e:
logger.error(f"Collaborative edit workflow failed: {e}")
return self._finish(False, error=str(e))
class FileShareAndDownloadWorkflow(Workflow):
"""
Workflow: User A uploads a file, shares it with User B, who then downloads it.
Steps:
1. User A creates a file via WebDAV
2. User A shares the file with User B (read-only)
3. User B lists their shares
4. User B reads/downloads the file
"""
def __init__(self):
super().__init__("file_share_download")
async def execute(self, users: list[UserSessionWrapper]) -> WorkflowResult:
"""Execute file sharing workflow."""
self.start_time = time.time()
if len(users) < 2:
return self._finish(False, error="Requires at least 2 users")
user_a, user_b = users[0], users[1]
unique_id = uuid.uuid4().hex[:8]
file_path = f"/LoadTest_{unique_id}.txt"
try:
# Step 1: User A creates a file
content = f"Test file content {unique_id}\nCreated for workflow testing"
create_result = await self._execute_step(
"create_file",
user_a,
lambda: user_a.call_tool(
"nc_webdav_put_file",
{
"path": file_path,
"content": content,
"content_type": "text/plain",
},
),
)
if not create_result.success:
return self._finish(False, error="Failed to create file")
# Step 2: User A shares file with User B
share_result = await self._execute_step(
"share_file",
user_a,
lambda: user_a.call_tool(
"nc_share_create",
{
"path": file_path,
"share_with": user_b.username,
"share_type": 0,
"permissions": 1, # Read-only
},
),
)
if not share_result.success:
logger.warning("File share failed, continuing")
# Step 3: User B lists shared files
_ = await self._execute_step(
"list_shares",
user_b,
lambda: user_b.call_tool("nc_share_list", {"shared_with_me": True}),
)
# Step 4: User B downloads the file
_ = await self._execute_step(
"download_file",
user_b,
lambda: user_b.call_tool("nc_webdav_get_file", {"path": file_path}),
)
# Cleanup
await user_a.call_tool("nc_webdav_delete", {"path": file_path})
return self._finish(success=True)
except Exception as e:
logger.error(f"File share workflow failed: {e}")
return self._finish(False, error=str(e))
class MixedOAuthWorkload:
"""
Mixed workload combining baseline operations and coordinated workflows.
Distribution:
- 50% Baseline operations (individual user CRUD)
- 30% Note sharing workflows
- 15% Collaborative editing workflows
- 5% File sharing workflows
"""
def __init__(self, users: list[UserSessionWrapper]):
self.users = users
self.workflows = {
"note_share": NoteShareWorkflow(),
"collaborative_edit": CollaborativeEditWorkflow(),
"file_share": FileShareAndDownloadWorkflow(),
}
async def run_operation(self) -> WorkflowResult | dict[str, Any]:
"""
Execute one random operation (baseline or workflow).
Returns:
WorkflowResult for workflows, dict for baseline operations
"""
rand = random.random()
# 50% baseline operations (single-user)
if rand < 0.50:
return await self._run_baseline_operation()
# 30% note sharing
elif rand < 0.80:
users = random.sample(self.users, min(2, len(self.users)))
return await self.workflows["note_share"].execute(users)
# 15% collaborative editing
elif rand < 0.95:
users = random.sample(self.users, min(len(self.users), 3))
return await self.workflows["collaborative_edit"].execute(users)
# 5% file sharing
else:
users = random.sample(self.users, min(2, len(self.users)))
return await self.workflows["file_share"].execute(users)
async def _run_baseline_operation(self) -> dict[str, Any]:
"""Run a baseline single-user operation."""
user = random.choice(self.users)
operations = [
(
"search_notes",
lambda: user.call_tool("nc_notes_search_notes", {"query": ""}),
),
("list_files", lambda: user.call_tool("nc_webdav_list", {"path": "/"})),
("get_capabilities", lambda: user.read_resource("nc://capabilities")),
]
op_name, operation = random.choice(operations)
start = time.time()
try:
await operation()
duration = time.time() - start
return {
"type": "baseline",
"operation": op_name,
"user": user.username,
"success": True,
"duration": duration,
}
except Exception as e:
duration = time.time() - start
return {
"type": "baseline",
"operation": op_name,
"user": user.username,
"success": False,
"duration": duration,
"error": str(e),
}