Security fixes: P0-P2 complete (22 fixes)
P0 (7): Auth-bypass removed, migrations fixed, plugin-upload disabled, RLS FORCE+WITH CHECK, plugin double-registration fixed, persistent volume, domain removed P1 (11): User/tenant model, Redis centralized, worker separated, transactional outbox, XSS fixed, DMS chunked streaming, permissions unified, password reset, metrics secured, config/docs fixed, cross-tenant FK P2 (4): Contact model normalized, cross-imports reduced 94%, commands+state machines for contacts/dms/mail/calendar, SPA path-traversal 8 new migrations, 99 unit tests, 13 commands, 8 contracts, 72 files changed
This commit is contained in:
@@ -0,0 +1,628 @@
|
||||
"""Tests for the Command pattern — Create, Update, Delete, Merge contact commands.
|
||||
|
||||
Tests cover:
|
||||
- Permission checks (viewer denied, admin allowed)
|
||||
- Business logic execution
|
||||
- Audit log entry creation
|
||||
- Outbox event enqueuing
|
||||
- State machine validation
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import uuid as uuid_mod
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.commands.base import CommandResult
|
||||
from app.commands.contact_commands import (
|
||||
CreateContactCommand,
|
||||
UpdateContactCommand,
|
||||
DeleteContactCommand,
|
||||
MergeContactsCommand,
|
||||
)
|
||||
from app.core.state_machine import (
|
||||
StateMachine,
|
||||
StateMachineError,
|
||||
contact_state_machine,
|
||||
workflow_state_machine,
|
||||
)
|
||||
from app.models.audit import AuditLog
|
||||
from app.models.contact import Contact
|
||||
from app.models.outbox import EventOutbox
|
||||
|
||||
from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users
|
||||
|
||||
|
||||
# ── State Machine Unit Tests ──
|
||||
|
||||
|
||||
class TestStateMachine:
|
||||
"""Unit tests for the StateMachine class."""
|
||||
|
||||
def test_can_transition_allowed(self):
|
||||
"""Allowed transitions return True."""
|
||||
assert contact_state_machine.can_transition("lead", "qualified") is True
|
||||
assert contact_state_machine.can_transition("qualified", "customer") is True
|
||||
assert contact_state_machine.can_transition("customer", "inactive") is True
|
||||
assert contact_state_machine.can_transition("inactive", "lead") is True
|
||||
|
||||
def test_can_transition_disallowed(self):
|
||||
"""Disallowed transitions return False."""
|
||||
assert contact_state_machine.can_transition("customer", "lead") is False
|
||||
assert contact_state_machine.can_transition("inactive", "customer") is False
|
||||
|
||||
def test_transition_success(self):
|
||||
"""Valid transition returns the new state."""
|
||||
assert contact_state_machine.transition("lead", "qualified") == "qualified"
|
||||
assert workflow_state_machine.transition("active", "paused") == "paused"
|
||||
|
||||
def test_transition_invalid_raises(self):
|
||||
"""Invalid transition raises StateMachineError."""
|
||||
with pytest.raises(StateMachineError, match="Invalid state transition"):
|
||||
contact_state_machine.transition("customer", "lead")
|
||||
|
||||
def test_transition_unknown_state_raises(self):
|
||||
"""Transition from unknown state raises StateMachineError."""
|
||||
with pytest.raises(StateMachineError, match="Invalid state transition"):
|
||||
contact_state_machine.transition("nonexistent", "lead")
|
||||
|
||||
def test_workflow_state_machine_transitions(self):
|
||||
"""Workflow state machine has correct transitions."""
|
||||
assert workflow_state_machine.can_transition("draft", "active") is True
|
||||
assert workflow_state_machine.can_transition("active", "paused") is True
|
||||
assert workflow_state_machine.can_transition("paused", "active") is True
|
||||
assert workflow_state_machine.can_transition("active", "completed") is True
|
||||
assert workflow_state_machine.can_transition("active", "cancelled") is True
|
||||
assert workflow_state_machine.can_transition("paused", "cancelled") is True
|
||||
assert workflow_state_machine.can_transition("draft", "cancelled") is True
|
||||
assert workflow_state_machine.can_transition("completed", "active") is False
|
||||
assert workflow_state_machine.can_transition("cancelled", "active") is False
|
||||
|
||||
def test_custom_state_machine(self):
|
||||
"""Custom state machine with own transitions."""
|
||||
sm = StateMachine({"a": ["b"], "b": ["c"], "c": []})
|
||||
assert sm.can_transition("a", "b") is True
|
||||
assert sm.can_transition("b", "c") is True
|
||||
assert sm.can_transition("c", "a") is False
|
||||
assert sm.transition("a", "b") == "b"
|
||||
with pytest.raises(StateMachineError):
|
||||
sm.transition("c", "a")
|
||||
|
||||
|
||||
# ── CommandResult Tests ──
|
||||
|
||||
|
||||
class TestCommandResult:
|
||||
"""Unit tests for CommandResult."""
|
||||
|
||||
def test_ok_result(self):
|
||||
"""ok() creates a successful result."""
|
||||
result = CommandResult.ok(data={"id": "123"})
|
||||
assert result.success is True
|
||||
assert result.data == {"id": "123"}
|
||||
assert result.error is None
|
||||
assert result.events == []
|
||||
|
||||
def test_ok_with_events(self):
|
||||
"""ok() with events creates a successful result with events."""
|
||||
events = [{"event": "contact.created"}]
|
||||
result = CommandResult.ok(data={}, events=events)
|
||||
assert result.events == events
|
||||
|
||||
def test_fail_result(self):
|
||||
"""fail() creates a failed result."""
|
||||
result = CommandResult.fail("Something went wrong")
|
||||
assert result.success is False
|
||||
assert result.data is None
|
||||
assert result.error == "Something went wrong"
|
||||
assert result.events == []
|
||||
|
||||
|
||||
# ── CreateContactCommand Tests ──
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestCreateContactCommand:
|
||||
"""Tests for CreateContactCommand."""
|
||||
|
||||
async def test_create_contact_admin_success(
|
||||
self, client, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Admin can create a contact, audit log is created, outbox event enqueued."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
|
||||
# Use command directly with db_session and redis_client
|
||||
current_user = {
|
||||
"user_id": str(seed["admin_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "admin",
|
||||
"is_system_admin": True,
|
||||
"permissions": ["*: *"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
cmd = CreateContactCommand(data={
|
||||
"type": "company",
|
||||
"name": "Test Company",
|
||||
"email_1": "test@example.com",
|
||||
})
|
||||
result = await cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert result.success is True
|
||||
assert result.data is not None
|
||||
assert result.data["name"] == "Test Company"
|
||||
assert result.data["displayname"] == "Test Company"
|
||||
assert len(result.events) >= 1
|
||||
assert result.events[0]["event"] == "contact.created"
|
||||
|
||||
# Verify audit log was created
|
||||
await db_session.flush()
|
||||
audit_q = select(AuditLog).where(
|
||||
AuditLog.entity_type == "contact",
|
||||
AuditLog.action == "create",
|
||||
AuditLog.user_id == seed["admin_a"].id,
|
||||
)
|
||||
audit_result = await db_session.execute(audit_q)
|
||||
audit_entries = audit_result.scalars().all()
|
||||
assert len(audit_entries) >= 1
|
||||
assert audit_entries[0].entity_id == uuid_mod.UUID(result.data["id"])
|
||||
|
||||
# Verify outbox event was enqueued
|
||||
outbox_q = select(EventOutbox).where(
|
||||
EventOutbox.event_name == "contact.created"
|
||||
)
|
||||
outbox_result = await db_session.execute(outbox_q)
|
||||
outbox_entries = outbox_result.scalars().all()
|
||||
assert len(outbox_entries) >= 1
|
||||
|
||||
async def test_create_contact_viewer_denied(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Viewer cannot create a contact (permission denied)."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["viewer_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "viewer",
|
||||
"is_system_admin": False,
|
||||
"permissions": ["contacts:read"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
cmd = CreateContactCommand(data={
|
||||
"type": "company",
|
||||
"name": "Denied Company",
|
||||
})
|
||||
result = await cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert result.success is False
|
||||
assert "Permission denied" in result.error
|
||||
|
||||
async def test_create_contact_with_invalid_status(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Creating a contact with invalid status fails."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["admin_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "admin",
|
||||
"is_system_admin": True,
|
||||
"permissions": ["*: *"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
cmd = CreateContactCommand(data={
|
||||
"type": "company",
|
||||
"name": "Bad Status Corp",
|
||||
"status": "nonexistent",
|
||||
})
|
||||
result = await cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert result.success is False
|
||||
assert "Invalid contact status" in result.error
|
||||
|
||||
|
||||
# ── UpdateContactCommand Tests ──
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestUpdateContactCommand:
|
||||
"""Tests for UpdateContactCommand."""
|
||||
|
||||
async def test_update_contact_admin_success(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Admin can update a contact, audit log created, outbox event enqueued."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["admin_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "admin",
|
||||
"is_system_admin": True,
|
||||
"permissions": ["*: *"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
# First create a contact
|
||||
create_cmd = CreateContactCommand(data={
|
||||
"type": "company",
|
||||
"name": "Original Name",
|
||||
})
|
||||
create_result = await create_cmd.execute(db_session, redis_client, current_user)
|
||||
assert create_result.success is True
|
||||
contact_id = create_result.data["id"]
|
||||
|
||||
# Update the contact
|
||||
update_cmd = UpdateContactCommand(
|
||||
contact_id=contact_id,
|
||||
data={"name": "Updated Name", "email_1": "updated@example.com"},
|
||||
)
|
||||
update_result = await update_cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert update_result.success is True
|
||||
assert update_result.data["name"] == "Updated Name"
|
||||
assert update_result.data["email_1"] == "updated@example.com"
|
||||
assert len(update_result.events) >= 1
|
||||
assert update_result.events[0]["event"] == "contact.updated"
|
||||
|
||||
# Verify audit log
|
||||
await db_session.flush()
|
||||
audit_q = select(AuditLog).where(
|
||||
AuditLog.entity_type == "contact",
|
||||
AuditLog.action == "update",
|
||||
AuditLog.entity_id == uuid_mod.UUID(contact_id),
|
||||
)
|
||||
audit_result = await db_session.execute(audit_q)
|
||||
audit_entries = audit_result.scalars().all()
|
||||
assert len(audit_entries) >= 1
|
||||
|
||||
async def test_update_contact_not_found(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Updating a non-existent contact fails gracefully."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["admin_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "admin",
|
||||
"is_system_admin": True,
|
||||
"permissions": ["*: *"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
random_id = str(uuid_mod.uuid4())
|
||||
update_cmd = UpdateContactCommand(
|
||||
contact_id=random_id,
|
||||
data={"name": "New Name"},
|
||||
)
|
||||
result = await update_cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert result.success is False
|
||||
assert "not found" in result.error.lower()
|
||||
|
||||
async def test_update_contact_viewer_denied(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Viewer cannot update a contact."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["viewer_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "viewer",
|
||||
"is_system_admin": False,
|
||||
"permissions": ["contacts:read"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
cmd = UpdateContactCommand(
|
||||
contact_id=str(seed["company_a"].id),
|
||||
data={"name": "Hacked"},
|
||||
)
|
||||
result = await cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert result.success is False
|
||||
assert "Permission denied" in result.error
|
||||
|
||||
|
||||
# ── DeleteContactCommand Tests ──
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDeleteContactCommand:
|
||||
"""Tests for DeleteContactCommand."""
|
||||
|
||||
async def test_soft_delete_contact_admin_success(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Admin can soft-delete a contact."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["admin_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "admin",
|
||||
"is_system_admin": True,
|
||||
"permissions": ["*: *"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
# Create a contact to delete
|
||||
create_cmd = CreateContactCommand(data={
|
||||
"type": "company",
|
||||
"name": "To Delete",
|
||||
})
|
||||
create_result = await create_cmd.execute(db_session, redis_client, current_user)
|
||||
assert create_result.success is True
|
||||
contact_id = create_result.data["id"]
|
||||
|
||||
# Soft-delete it
|
||||
delete_cmd = DeleteContactCommand(contact_id=contact_id, hard=False)
|
||||
delete_result = await delete_cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert delete_result.success is True
|
||||
assert delete_result.data is None
|
||||
assert len(delete_result.events) >= 1
|
||||
assert delete_result.events[0]["event"] == "contact.deleted"
|
||||
|
||||
# Verify audit log
|
||||
await db_session.flush()
|
||||
audit_q = select(AuditLog).where(
|
||||
AuditLog.entity_type == "contact",
|
||||
AuditLog.action == "delete",
|
||||
AuditLog.entity_id == uuid_mod.UUID(contact_id),
|
||||
)
|
||||
audit_result = await db_session.execute(audit_q)
|
||||
audit_entries = audit_result.scalars().all()
|
||||
assert len(audit_entries) >= 1
|
||||
|
||||
async def test_hard_delete_contact_admin_success(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Admin can hard-delete a contact."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["admin_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "admin",
|
||||
"is_system_admin": True,
|
||||
"permissions": ["*: *"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
# Create a contact to hard-delete
|
||||
create_cmd = CreateContactCommand(data={
|
||||
"type": "company",
|
||||
"name": "Hard Delete Me",
|
||||
})
|
||||
create_result = await create_cmd.execute(db_session, redis_client, current_user)
|
||||
assert create_result.success is True
|
||||
contact_id = create_result.data["id"]
|
||||
|
||||
# Hard-delete it
|
||||
delete_cmd = DeleteContactCommand(contact_id=contact_id, hard=True)
|
||||
delete_result = await delete_cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert delete_result.success is True
|
||||
assert len(delete_result.events) >= 1
|
||||
assert delete_result.events[0]["event"] == "contact.hard_deleted"
|
||||
|
||||
async def test_delete_contact_not_found(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Deleting a non-existent contact fails gracefully."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["admin_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "admin",
|
||||
"is_system_admin": True,
|
||||
"permissions": ["*: *"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
random_id = str(uuid_mod.uuid4())
|
||||
delete_cmd = DeleteContactCommand(contact_id=random_id)
|
||||
result = await delete_cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert result.success is False
|
||||
assert "not found" in result.error.lower()
|
||||
|
||||
async def test_delete_contact_viewer_denied(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Viewer cannot delete a contact."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["viewer_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "viewer",
|
||||
"is_system_admin": False,
|
||||
"permissions": ["contacts:read"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
cmd = DeleteContactCommand(contact_id=str(seed["company_a"].id))
|
||||
result = await cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert result.success is False
|
||||
assert "Permission denied" in result.error
|
||||
|
||||
|
||||
# ── MergeContactsCommand Tests ──
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestMergeContactsCommand:
|
||||
"""Tests for MergeContactsCommand."""
|
||||
|
||||
async def test_merge_contacts_admin_success(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Admin can merge two contacts, audit log created, outbox event enqueued."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["admin_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "admin",
|
||||
"is_system_admin": True,
|
||||
"permissions": ["*: *"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
# Create two contacts to merge
|
||||
create_cmd1 = CreateContactCommand(data={
|
||||
"type": "company",
|
||||
"name": "Source Company",
|
||||
"email_1": "source@example.com",
|
||||
})
|
||||
result1 = await create_cmd1.execute(db_session, redis_client, current_user)
|
||||
assert result1.success is True
|
||||
source_id = result1.data["id"]
|
||||
|
||||
create_cmd2 = CreateContactCommand(data={
|
||||
"type": "company",
|
||||
"name": "Target Company",
|
||||
"email_1": "target@example.com",
|
||||
})
|
||||
result2 = await create_cmd2.execute(db_session, redis_client, current_user)
|
||||
assert result2.success is True
|
||||
target_id = result2.data["id"]
|
||||
|
||||
# Merge source → target
|
||||
merge_cmd = MergeContactsCommand(
|
||||
source_contact_id=source_id,
|
||||
target_contact_id=target_id,
|
||||
note="Duplicate detected",
|
||||
)
|
||||
merge_result = await merge_cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert merge_result.success is True
|
||||
assert merge_result.data is not None
|
||||
assert "history" in merge_result.data
|
||||
assert merge_result.data["history"]["source_id"] == source_id
|
||||
assert merge_result.data["history"]["target_id"] == target_id
|
||||
assert len(merge_result.events) >= 1
|
||||
assert merge_result.events[0]["event"] == "contact.merged"
|
||||
|
||||
# Verify audit log
|
||||
await db_session.flush()
|
||||
audit_q = select(AuditLog).where(
|
||||
AuditLog.entity_type == "contact",
|
||||
AuditLog.action == "merge",
|
||||
AuditLog.entity_id == uuid_mod.UUID(target_id),
|
||||
)
|
||||
audit_result = await db_session.execute(audit_q)
|
||||
audit_entries = audit_result.scalars().all()
|
||||
assert len(audit_entries) >= 1
|
||||
|
||||
async def test_merge_same_contact_fails(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Merging a contact with itself fails."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["admin_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "admin",
|
||||
"is_system_admin": True,
|
||||
"permissions": ["*: *"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
# Create a contact
|
||||
create_cmd = CreateContactCommand(data={
|
||||
"type": "company",
|
||||
"name": "Solo Company",
|
||||
})
|
||||
create_result = await create_cmd.execute(db_session, redis_client, current_user)
|
||||
assert create_result.success is True
|
||||
contact_id = create_result.data["id"]
|
||||
|
||||
# Try to merge with itself
|
||||
merge_cmd = MergeContactsCommand(
|
||||
source_contact_id=contact_id,
|
||||
target_contact_id=contact_id,
|
||||
)
|
||||
result = await merge_cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert result.success is False
|
||||
assert "must be different" in result.error
|
||||
|
||||
async def test_merge_contacts_viewer_denied(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Viewer cannot merge contacts."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["viewer_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "viewer",
|
||||
"is_system_admin": False,
|
||||
"permissions": ["contacts:read"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
merge_cmd = MergeContactsCommand(
|
||||
source_contact_id=str(seed["company_a"].id),
|
||||
target_contact_id=str(seed["company_a"].id),
|
||||
)
|
||||
result = await merge_cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert result.success is False
|
||||
assert "Permission denied" in result.error
|
||||
|
||||
async def test_merge_contact_not_found(
|
||||
self, db_session: AsyncSession, redis_client
|
||||
):
|
||||
"""Merging with a non-existent contact fails."""
|
||||
seed = await seed_tenant_and_users(db_session)
|
||||
|
||||
current_user = {
|
||||
"user_id": str(seed["admin_a"].id),
|
||||
"tenant_id": str(seed["tenant_a"].id),
|
||||
"role": "admin",
|
||||
"is_system_admin": True,
|
||||
"permissions": ["*: *"],
|
||||
"denied_permissions": [],
|
||||
"field_permissions": {},
|
||||
}
|
||||
|
||||
random_id = str(uuid_mod.uuid4())
|
||||
merge_cmd = MergeContactsCommand(
|
||||
source_contact_id=random_id,
|
||||
target_contact_id=str(seed["company_a"].id),
|
||||
)
|
||||
result = await merge_cmd.execute(db_session, redis_client, current_user)
|
||||
|
||||
assert result.success is False
|
||||
assert "not found" in result.error.lower()
|
||||
Reference in New Issue
Block a user