Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions plan/BUILD_LEARNINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,14 @@ update **both** `pyproject.toml` markers AND any `.github/workflows/` YAML files
reference the mark by name. A renamed mark in test files without a corresponding
workflow update causes `pytest` to select 0 tests and exit with code 5 (no tests
collected), failing CI even though the rename itself is correct.

### 2026-06-12 USE-CASE-SPLIT-881 — flat-to-subpackage test migration pattern

When migrating flat test files into per-submodule subdirectories, the existing
test classes map cleanly onto the semantic clusters already established by the
source split. Removing the old flat files and creating new per-submodule files
(rather than leaving both) avoids duplicate test collection and keeps the
layout strictly mirrored. The parent `conftest.py` fixtures are automatically
inherited via pytest's upward conftest search, so only the vocabulary
registration side-effect import needs copying into each new subdirectory
conftest.
28 changes: 28 additions & 0 deletions plan/history/2606/implementation/ISSUE-881.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
source: ISSUE-881
timestamp: '2026-06-12T20:04:33.391050+00:00'
title: Split received/case.py and received/actor.py into subpackages
type: implementation
---

## Issue #881 — Split large received use-case modules

Converted two 800+ line use-case modules into focused subpackages.

**`received/case/`** submodules: `_helpers.py`, `create.py`, `update.py`,
`engage_defer.py`, `lifecycle.py`, `validate.py`, `__init__.py`. All submodules
under 500 lines (max: 273).

**`received/actor/`** submodules: `suggest.py`, `case_manager_role.py`,
`ownership.py`, `invite.py`, `announce.py`, `__init__.py`. All submodules
under 500 lines (max: 270).

Test layout migrated to mirror source split: flat `test_case.py`,
`test_actor.py`, `test_case_bootstrap_trust.py`, `test_actor_announce_case.py`
replaced by per-submodule files in `test/core/use_cases/received/case/` and
`test/core/use_cases/received/actor/`.

All existing import paths preserved via re-exporting `__init__.py` (AC-3).
3205 unit tests pass, all four linters clean.

PR: [#941](https://github.com/CERTCC/Vultron/pull/941)
Empty file.
14 changes: 14 additions & 0 deletions test/core/use_cases/received/actor/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""
Fixtures for test/core/use_cases/received tests.

Imports VulnerabilityCase (and related wire-layer types) as a side effect so
that the global vocabulary registry is populated before any test in this
directory runs. Without this import the registry may be empty when tests run
in isolation, causing TinyDB's record_to_object() to fall back to returning a
raw Document instead of a deserialized domain object.
"""

# noqa: F401 — imported for vocabulary registration side-effect
from vultron.wire.as2.vocab.objects.vulnerability_case import ( # noqa: F401
VulnerabilityCase,
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from vultron.adapters.driven.datalayer_sqlite import SqliteDataLayer
from vultron.core.models.report_case_link import VultronReportCaseLink
from vultron.core.use_cases.received.actor import (
from vultron.core.use_cases.received.actor.announce import (
AnnounceVulnerabilityCaseReceivedUseCase,
)
from vultron.wire.as2.factories import announce_vulnerability_case_activity
Expand Down
257 changes: 257 additions & 0 deletions test/core/use_cases/received/actor/test_case_manager_role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
# Copyright (c) 2025-2026 Carnegie Mellon University and Contributors.
# - see Contributors.md for a full list of Contributors
# - see ContributionInstructions.md for information on how you can Contribute to this project
# Vultron Multiparty Coordinated Vulnerability Disclosure Protocol Prototype is
# licensed under a MIT (SEI)-style license, please see LICENSE.md distributed
# with this Software or contact permission@sei.cmu.edu for full terms.
# Created, in part, with funding and support from the United States Government
# (see Acknowledgments file). This program may include and/or can make use of
# certain third party source code, object code, documentation and other files
# ("Third Party Software"). See LICENSE.md for more details.
# Carnegie Mellon®, CERT® and CERT Coordination Center® are registered in the
# U.S. Patent and Trademark Office by Carnegie Mellon University
"""Tests for case manager role delegation received use cases."""

import logging
from unittest.mock import MagicMock

from vultron.core.use_cases.received.actor.case_manager_role import (
AcceptCaseManagerRoleReceivedUseCase,
OfferCaseManagerRoleReceivedUseCase,
RejectCaseManagerRoleReceivedUseCase,
)
from vultron.wire.as2.factories import (
accept_case_manager_role_activity,
offer_case_manager_role_activity,
reject_case_manager_role_activity,
)


class TestCaseManagerRoleDelegationUseCases:
"""Tests for offer/accept/reject CASE_MANAGER role delegation use cases.

DEMOMA-08-002: CASE_MANAGER delegation is distinct from ownership transfer.
"""

_VENDOR_URI = "https://example.org/actors/vendor"
_CASE_ACTOR_URI = "https://example.org/actors/case-actor"
_CASE_URI = "https://example.org/cases/urn:uuid:test-case-mgr"
_PARTICIPANT_URI = (
"https://example.org/participants/urn:uuid:case-actor-participant"
)

def _make_offer(self):
from vultron.wire.as2.vocab.objects.case_participant import (
CaseParticipant,
)
from vultron.wire.as2.vocab.objects.vulnerability_case import (
VulnerabilityCase,
)

case = VulnerabilityCase(id_=self._CASE_URI, name="CASE-MGR-TEST")
participant = CaseParticipant(
id_=self._PARTICIPANT_URI,
attributed_to=self._CASE_ACTOR_URI,
context=self._CASE_URI,
)
return offer_case_manager_role_activity(
case,
target=participant,
actor=self._VENDOR_URI,
)

def test_offer_case_manager_role_persists_offer(self, make_payload):
"""OfferCaseManagerRoleReceivedUseCase persists the offer activity."""
from vultron.adapters.driven.datalayer_sqlite import SqliteDataLayer

dl = SqliteDataLayer("sqlite:///:memory:")
offer = self._make_offer()
event = make_payload(offer)

OfferCaseManagerRoleReceivedUseCase(dl, event).execute()

stored = dl.get(offer.type_.value, offer.id_)
assert stored is not None

def test_offer_case_manager_role_idempotent(self, make_payload):
"""Repeated execution of OfferCaseManagerRoleReceivedUseCase is a no-op."""
from vultron.adapters.driven.datalayer_sqlite import SqliteDataLayer

dl = SqliteDataLayer("sqlite:///:memory:")
offer = self._make_offer()
event = make_payload(offer)

OfferCaseManagerRoleReceivedUseCase(dl, event).execute()
OfferCaseManagerRoleReceivedUseCase(dl, event).execute()

stored = dl.get(offer.type_.value, offer.id_)
assert stored is not None

def test_accept_case_manager_role_persists_acceptance(self, make_payload):
"""AcceptCaseManagerRoleReceivedUseCase persists the acceptance activity."""
from vultron.adapters.driven.datalayer_sqlite import SqliteDataLayer

dl = SqliteDataLayer("sqlite:///:memory:")
offer = self._make_offer()
accept = accept_case_manager_role_activity(
offer, actor=self._CASE_ACTOR_URI
)
event = make_payload(accept)

AcceptCaseManagerRoleReceivedUseCase(dl, event).execute()

stored = dl.get(accept.type_.value, accept.id_)
assert stored is not None

def test_accept_case_manager_role_logs_acceptance(
self, caplog, make_payload
):
"""AcceptCaseManagerRoleReceivedUseCase logs acceptance without raising."""
from vultron.adapters.driven.datalayer_sqlite import SqliteDataLayer

dl = SqliteDataLayer("sqlite:///:memory:")
offer = self._make_offer()
accept = accept_case_manager_role_activity(
offer, actor=self._CASE_ACTOR_URI
)
event = make_payload(accept)

with caplog.at_level(logging.INFO):
AcceptCaseManagerRoleReceivedUseCase(dl, event).execute()

assert any("accepted" in r.message.lower() for r in caplog.records)

def test_reject_case_manager_role_logs_warning(self, caplog, make_payload):
"""RejectCaseManagerRoleReceivedUseCase logs a warning without raising."""
offer = self._make_offer()
reject = reject_case_manager_role_activity(
offer, actor=self._CASE_ACTOR_URI
)
event = make_payload(reject)

with caplog.at_level(logging.WARNING):
RejectCaseManagerRoleReceivedUseCase(MagicMock(), event).execute()

assert any("rejected" in r.message.lower() for r in caplog.records)

def test_offer_case_manager_role_auto_accepts_when_trigger_given(
self, make_payload
):
"""OfferCaseManagerRoleReceivedUseCase auto-accepts when trigger_activity provided."""
from unittest.mock import MagicMock, patch
from vultron.adapters.driven.datalayer_sqlite import SqliteDataLayer
from vultron.wire.as2.vocab.base.objects.actors import as_Organization

dl = SqliteDataLayer("sqlite:///:memory:")
local_actor = as_Organization(id_=self._CASE_ACTOR_URI)
dl.create(local_actor)

offer = self._make_offer()
event = make_payload(offer)

trigger = MagicMock()
trigger.accept_case_manager_role.return_value = (
"https://example.org/activities/accept-1"
)

with patch(
"vultron.core.use_cases.triggers._helpers.add_activity_to_outbox"
):
OfferCaseManagerRoleReceivedUseCase(
dl, event, trigger_activity=trigger
).execute()

trigger.accept_case_manager_role.assert_called_once()
call_kwargs = trigger.accept_case_manager_role.call_args
assert call_kwargs.kwargs["offer_id"] == offer.id_
assert call_kwargs.kwargs["vendor_id"] == self._VENDOR_URI

def test_offer_case_manager_role_no_auto_accept_without_trigger(
self, make_payload
):
"""OfferCaseManagerRoleReceivedUseCase skips auto-accept when trigger_activity is None."""
from vultron.adapters.driven.datalayer_sqlite import SqliteDataLayer

dl = SqliteDataLayer("sqlite:///:memory:")
offer = self._make_offer()
event = make_payload(offer)

# No trigger_activity — should not raise
OfferCaseManagerRoleReceivedUseCase(dl, event).execute()

stored = dl.get(offer.type_.value, offer.id_)
assert stored is not None

def test_accept_case_manager_role_sends_bootstrap_when_reporter_found(
self, make_payload
):
"""AcceptCaseManagerRoleReceivedUseCase sends Create(Case) to Reporter on accept."""
from unittest.mock import MagicMock, patch
from vultron.adapters.driven.datalayer_sqlite import SqliteDataLayer
from vultron.wire.as2.vocab.base.objects.actors import as_Organization
from vultron.core.models.vultron_types import VultronParticipant
from vultron.core.states.roles import CVDRole
from vultron.wire.as2.vocab.objects.vulnerability_case import (
VulnerabilityCase,
)

dl = SqliteDataLayer("sqlite:///:memory:")
vendor = as_Organization(id_=self._VENDOR_URI)
reporter_id = "https://example.org/actors/reporter"
reporter_participant_id = f"{self._CASE_URI}/participants/reporter"
reporter_participant = VultronParticipant(
id_=reporter_participant_id,
attributed_to=reporter_id,
context=self._CASE_URI,
name="Reporter",
case_roles=[CVDRole.FINDER, CVDRole.REPORTER],
)
case = VulnerabilityCase(id_=self._CASE_URI, name="BOOTSTRAP-TEST")
case.actor_participant_index[reporter_id] = reporter_participant_id
dl.create(vendor)
dl.create(reporter_participant)
dl.create(case)

offer = self._make_offer()
accept = accept_case_manager_role_activity(
offer, actor=self._CASE_ACTOR_URI
)
event = make_payload(accept)

trigger = MagicMock()
trigger.create_case.return_value = (
"https://example.org/activities/create-1",
{},
)

with patch(
"vultron.core.use_cases.triggers._helpers.add_activity_to_outbox"
):
AcceptCaseManagerRoleReceivedUseCase(
dl, event, trigger_activity=trigger
).execute()

trigger.create_case.assert_called_once()
call_kwargs = trigger.create_case.call_args
assert call_kwargs.kwargs.get("to") == [reporter_id] or (
len(call_kwargs.args) >= 3 and reporter_id in call_kwargs.args
)

def test_accept_case_manager_role_no_bootstrap_without_trigger(
self, make_payload
):
"""AcceptCaseManagerRoleReceivedUseCase skips bootstrap when trigger_activity is None."""
from vultron.adapters.driven.datalayer_sqlite import SqliteDataLayer

dl = SqliteDataLayer("sqlite:///:memory:")
offer = self._make_offer()
accept = accept_case_manager_role_activity(
offer, actor=self._CASE_ACTOR_URI
)
event = make_payload(accept)

# No trigger_activity — should not raise
AcceptCaseManagerRoleReceivedUseCase(dl, event).execute()

stored = dl.get(accept.type_.value, accept.id_)
assert stored is not None
Loading