a46aa157f3
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled
Admin can no longer be picked as a red_tech/blue_tech assignee — it administers the site, it doesn't operate tests. Applied to the eligible-assignee list on both the picker UI and the assign endpoint's validation, and dropped from the GET /users/operators pool. push_assignee_update() now falls back to a fresh Jira account-id lookup when the assignee hasn't logged in yet (so jira_account_id is still empty), instead of silently no-op'ing — assignment now syncs to Jira immediately regardless of whether the assignee has ever logged into Aegis before.
538 lines
20 KiB
Python
538 lines
20 KiB
Python
"""Jira service unit tests (FASE-1.2)."""
|
|
|
|
from datetime import datetime
|
|
from unittest.mock import MagicMock, patch
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from app.domain.exceptions import InvalidOperationError
|
|
from app.models.jira_link import JiraLink, JiraLinkEntityType
|
|
from app.services import jira_service
|
|
|
|
|
|
def test_get_jira_client_raises_when_disabled(monkeypatch):
|
|
monkeypatch.setattr("app.services.jira_service.settings.JIRA_ENABLED", False)
|
|
jira_service._jira_client = None
|
|
with pytest.raises(InvalidOperationError, match="not enabled"):
|
|
jira_service.get_jira_client()
|
|
|
|
|
|
@patch("app.services.jira_service.get_jira_client")
|
|
def test_sync_aegis_to_jira_adds_comment(mock_get_client, db):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
|
|
link = JiraLink(
|
|
entity_type=JiraLinkEntityType.test,
|
|
entity_id=uuid4(),
|
|
jira_issue_key="SEC-99",
|
|
)
|
|
jira_service.sync_aegis_to_jira(db, link, {"state": "validated", "result": "pass"})
|
|
|
|
mock_jira.issue_add_comment.assert_called_once()
|
|
args = mock_jira.issue_add_comment.call_args[0]
|
|
assert args[0] == "SEC-99"
|
|
assert "Aegis Sync Update" in args[1]
|
|
assert link.last_synced_at is not None
|
|
|
|
|
|
def _make_link(entity_id, issue_key="SEC-100"):
|
|
return JiraLink(
|
|
entity_type=JiraLinkEntityType.test,
|
|
entity_id=entity_id,
|
|
jira_issue_key=issue_key,
|
|
)
|
|
|
|
|
|
def _make_test(**overrides):
|
|
from app.models.test import Test
|
|
t = MagicMock(spec=Test)
|
|
t.id = uuid4()
|
|
t.name = "Test"
|
|
t.attack_success = None
|
|
t.detection_result = None
|
|
t.containment_result = None
|
|
t.execution_end_time = None
|
|
t.detection_time = None
|
|
t.containment_time = None
|
|
# _build_state_comment() appends these raw (not via an f-string) when
|
|
# truthy, so an un-nulled MagicMock attribute breaks "\n".join(lines).
|
|
t.red_summary = None
|
|
t.blue_summary = None
|
|
t.remediation_steps = None
|
|
t.red_validation_status = None
|
|
t.blue_validation_status = None
|
|
t.red_validation_notes = None
|
|
t.blue_validation_notes = None
|
|
for k, v in overrides.items():
|
|
setattr(t, k, v)
|
|
return t
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_pause_event_sets_on_hold(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test()
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
|
|
jira_service.push_pause_event(db, test, MagicMock(username="alice"), resuming=False)
|
|
|
|
mock_jira.set_issue_status.assert_called_once_with(link.jira_issue_key, "On Hold")
|
|
mock_jira.issue_add_comment.assert_called_once()
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_pause_event_resume_sets_in_progress(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test()
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
|
|
jira_service.push_pause_event(db, test, MagicMock(username="alice"), resuming=True)
|
|
|
|
mock_jira.set_issue_status.assert_called_once_with(link.jira_issue_key, "In Progress")
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_hold_event_sets_blocked(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test()
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
|
|
jira_service.push_hold_event(db, test, MagicMock(username="alice"), resuming=False, reason="waiting on approval")
|
|
|
|
mock_jira.set_issue_status.assert_called_once_with(link.jira_issue_key, "Blocked")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"new_state,expected_status",
|
|
[
|
|
("draft", "To-Do"),
|
|
("red_executing", "In Progress"),
|
|
("red_review", "RT Test Review"),
|
|
("blue_evaluating", "Queued Blue Team"),
|
|
("blue_review", "Blue Team Test Review"),
|
|
("in_review", "Validation"),
|
|
("validated", "Done"),
|
|
("rejected", "Rejected"),
|
|
("disputed", "Dispute"),
|
|
],
|
|
)
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_test_event_maps_every_state_to_jira_status(
|
|
mock_get_client, mock_configured, db, new_state, expected_status
|
|
):
|
|
"""Every TestState the Purple Team Jira workflow needs to reflect must
|
|
have a corresponding status transition — not just red_executing."""
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test()
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
|
|
jira_service.push_test_event(db, test, MagicMock(username="alice", jira_account_id=None), new_state)
|
|
|
|
mock_jira.set_issue_status.assert_called_once_with(link.jira_issue_key, expected_status)
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_bt_work_started_sets_in_progress(mock_get_client, mock_configured, db):
|
|
"""blue_evaluating covers both 'queued, unclaimed' and 'actively being
|
|
worked' — the state alone can't distinguish them, so start_blue_work
|
|
must explicitly push In Progress rather than relying on the generic
|
|
state->status dispatch (which would incorrectly re-push Queued Blue Team)."""
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test()
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
|
|
jira_service.push_bt_work_started(db, test, MagicMock(username="bob"))
|
|
|
|
mock_jira.set_issue_status.assert_called_once_with(link.jira_issue_key, "In Progress")
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_rt_started_sets_date_field(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test()
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
|
|
jira_service.push_rt_started(db, test)
|
|
|
|
mock_jira.update_issue_field.assert_called_once()
|
|
args, kwargs = mock_jira.update_issue_field.call_args
|
|
assert args[0] == link.jira_issue_key
|
|
assert jira_service.JIRA_FIELD_RT_START_DATE in kwargs["fields"]
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_rt_submitted_includes_attack_success(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test(attack_success=MagicMock(value="successful"))
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
|
|
jira_service.push_rt_submitted(db, test)
|
|
|
|
fields = mock_jira.update_issue_field.call_args[1]["fields"]
|
|
assert fields[jira_service.JIRA_FIELD_RT_END_DATE]
|
|
assert fields[jira_service.JIRA_FIELD_ATTACK_SUCCESS] == {"value": "Yes"}
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_bt_submitted_includes_detection_and_hours(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
exec_end = datetime(2026, 1, 1, 10, 0, 0)
|
|
detect = datetime(2026, 1, 1, 12, 0, 0)
|
|
contain = datetime(2026, 1, 1, 13, 30, 0)
|
|
test = _make_test(
|
|
detection_result=MagicMock(value="detected"),
|
|
execution_end_time=exec_end,
|
|
detection_time=detect,
|
|
containment_time=contain,
|
|
)
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
|
|
jira_service.push_bt_submitted(db, test)
|
|
|
|
fields = mock_jira.update_issue_field.call_args[1]["fields"]
|
|
assert fields[jira_service.JIRA_FIELD_ATTACK_DETECTED] == {"value": "Yes"}
|
|
assert fields[jira_service.JIRA_FIELD_TIME_TO_DETECT] == 2.0
|
|
assert fields[jira_service.JIRA_FIELD_TIME_TO_CONTAIN] == 1.5
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_bt_submitted_includes_attack_contained(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test(containment_result=MagicMock(value="partially_contained"))
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
|
|
jira_service.push_bt_submitted(db, test)
|
|
|
|
fields = mock_jira.update_issue_field.call_args[1]["fields"]
|
|
assert fields[jira_service.JIRA_FIELD_ATTACK_CONTAINED] == {"value": "Partial"}
|
|
|
|
|
|
def test_update_test_fields_noop_when_not_configured(db):
|
|
test = _make_test()
|
|
# has_admin_jira_configured defaults to False without mocking — should
|
|
# silently no-op rather than raise.
|
|
jira_service._update_test_fields(db, test, {"customfield_1": "x"})
|
|
|
|
|
|
@patch("app.services.jira_service.get_jira_client")
|
|
def test_search_jira_issues_maps_fields(mock_get_client):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
mock_jira.jql.return_value = {
|
|
"issues": [
|
|
{
|
|
"key": "TST-1",
|
|
"fields": {
|
|
"summary": "Test issue",
|
|
"status": {"name": "In Progress"},
|
|
"assignee": {"displayName": "Alice"},
|
|
"priority": {"name": "High"},
|
|
},
|
|
}
|
|
]
|
|
}
|
|
|
|
results = jira_service.search_jira_issues("project = TEST", max_results=5)
|
|
|
|
assert len(results) == 1
|
|
assert results[0]["issue_key"] == "TST-1"
|
|
assert results[0]["summary"] == "Test issue"
|
|
assert results[0]["status"] == "In Progress"
|
|
mock_jira.jql.assert_called_once()
|
|
|
|
|
|
def _make_technique(**overrides):
|
|
from app.models.technique import Technique
|
|
tech = MagicMock(spec=Technique)
|
|
tech.mitre_id = "T1003.006"
|
|
tech.name = "DCSync"
|
|
tech.tactic = "credential-access"
|
|
tech.severity = "high"
|
|
for k, v in overrides.items():
|
|
setattr(tech, k, v)
|
|
return tech
|
|
|
|
|
|
def _make_test_for_issue(**overrides):
|
|
from app.models.test import Test
|
|
t = MagicMock(spec=Test)
|
|
t.id = uuid4()
|
|
t.name = "Run DSInternals Get-ADReplAccount"
|
|
t.technique_id = uuid4()
|
|
t.procedure_text = "Invoke-DSInternals"
|
|
t.description = "desc"
|
|
t.platform = "Windows"
|
|
t.tool_used = "DSInternals"
|
|
t.data_classification = "pii"
|
|
for k, v in overrides.items():
|
|
setattr(t, k, v)
|
|
return t
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_jira_project_key", return_value="SEC")
|
|
@patch("app.services.jira_service.get_jira_parent_ticket_standalone", return_value=None)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_auto_create_test_issue_retries_without_unscreened_data_sensitivity_field(
|
|
mock_get_client, mock_parent, mock_project_key, mock_configured, db
|
|
):
|
|
# Regression: Jira rejects the WHOLE issue when the data sensitivity
|
|
# field isn't on the project's create screen, which would silently
|
|
# block every test's ticket creation. Must retry once without that field
|
|
# instead of losing the ticket.
|
|
mock_jira = MagicMock()
|
|
mock_jira.issue_create.side_effect = [
|
|
Exception(
|
|
f"Field '{jira_service.JIRA_FIELD_DATA_SENSITIVITY}' cannot be set. "
|
|
"It is not on the appropriate screen, or unknown."
|
|
),
|
|
{"key": "SEC-42", "id": "10042"},
|
|
]
|
|
mock_get_client.return_value = mock_jira
|
|
from app.models.user import User
|
|
test = _make_test_for_issue()
|
|
technique = _make_technique()
|
|
actor = User(username="gerardo-ruiz", email="gerardo.ruiz@kaseya.com", hashed_password="x")
|
|
db.add(actor)
|
|
db.commit()
|
|
|
|
issue_key = jira_service.auto_create_test_issue(db, test, actor, technique=technique)
|
|
|
|
assert issue_key == "SEC-42"
|
|
assert mock_jira.issue_create.call_count == 2
|
|
# Both calls share the same `fields` dict object (mutated in place), so
|
|
# only the final state is inspectable here — confirming the retry
|
|
# dropped the field is what matters.
|
|
final_call_fields = mock_jira.issue_create.call_args_list[1].kwargs["fields"]
|
|
assert jira_service.JIRA_FIELD_DATA_SENSITIVITY not in final_call_fields
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"data_classification,expected_jira_value",
|
|
[
|
|
("public", "Public"),
|
|
("general_use", "General Use"),
|
|
("internal_use_only", "Internal Use Only"),
|
|
("trusted_people", "Trusted People"),
|
|
("customer_content", "Customer Content"),
|
|
("pii", "PII"),
|
|
],
|
|
)
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_jira_project_key", return_value="SEC")
|
|
@patch("app.services.jira_service.get_jira_parent_ticket_standalone", return_value=None)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_auto_create_test_issue_maps_data_classification_to_real_jira_field(
|
|
mock_get_client, mock_parent, mock_project_key, mock_configured, db,
|
|
data_classification, expected_jira_value,
|
|
):
|
|
# Regression: the field ID was wrong (customfield_11233, which doesn't
|
|
# exist in the project) and the classification scheme didn't match
|
|
# Jira's actual 6-option "Data Sensitivity" field at all.
|
|
mock_jira = MagicMock()
|
|
mock_jira.issue_create.return_value = {"key": "SEC-1", "id": "1"}
|
|
mock_get_client.return_value = mock_jira
|
|
from app.models.user import User
|
|
test = _make_test_for_issue(data_classification=data_classification)
|
|
technique = _make_technique()
|
|
actor = User(username="gerardo-ruiz", email="gerardo.ruiz@kaseya.com", hashed_password="x")
|
|
db.add(actor)
|
|
db.commit()
|
|
|
|
jira_service.auto_create_test_issue(db, test, actor, technique=technique)
|
|
|
|
fields = mock_jira.issue_create.call_args.kwargs["fields"]
|
|
assert fields["customfield_11814"] == {"value": expected_jira_value}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"platform,expected_label",
|
|
[
|
|
("Windows", "windows"),
|
|
("linux", "linux"),
|
|
("macOS", "macos"),
|
|
("Azure AD", "azure-ad"),
|
|
(None, None),
|
|
],
|
|
)
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_jira_project_key", return_value="SEC")
|
|
@patch("app.services.jira_service.get_jira_parent_ticket_standalone", return_value=None)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_auto_create_test_issue_adds_platform_label(
|
|
mock_get_client, mock_parent, mock_project_key, mock_configured, db,
|
|
platform, expected_label,
|
|
):
|
|
mock_jira = MagicMock()
|
|
mock_jira.issue_create.return_value = {"key": "SEC-1", "id": "1"}
|
|
mock_get_client.return_value = mock_jira
|
|
from app.models.user import User
|
|
test = _make_test_for_issue(platform=platform)
|
|
technique = _make_technique()
|
|
actor = User(username="gerardo-ruiz", email="gerardo.ruiz@kaseya.com", hashed_password="x")
|
|
db.add(actor)
|
|
db.commit()
|
|
|
|
jira_service.auto_create_test_issue(db, test, actor, technique=technique)
|
|
|
|
labels = mock_jira.issue_create.call_args.kwargs["fields"]["labels"]
|
|
assert ["aegis", "T1003-006"] == labels[:2]
|
|
if expected_label:
|
|
assert expected_label in labels
|
|
else:
|
|
assert len(labels) == 2
|
|
|
|
|
|
def _make_user(**overrides):
|
|
from app.models.user import User
|
|
u = MagicMock(spec=User)
|
|
u.username = "gerardo-ruiz"
|
|
u.email = "gerardo.ruiz@kaseya.com"
|
|
u.jira_account_id = None
|
|
for k, v in overrides.items():
|
|
setattr(u, k, v)
|
|
return u
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_lookup_user_jira_account_id_calls_with_valid_kwargs(mock_get_client, mock_configured, db):
|
|
# Regression test: atlassian-python-api's user_find_by_user_string() takes
|
|
# `limit`, not `maxResults` — passing maxResults raised a TypeError that
|
|
# was silently swallowed, so the lookup always failed even with a valid
|
|
# email and a working admin Jira connection.
|
|
mock_jira = MagicMock()
|
|
mock_jira.user_find_by_user_string.return_value = [
|
|
{"emailAddress": "gerardo.ruiz@kaseya.com", "accountId": "abc123"}
|
|
]
|
|
mock_get_client.return_value = mock_jira
|
|
user = _make_user()
|
|
|
|
updated = jira_service.lookup_user_jira_account_id(db, user)
|
|
|
|
mock_jira.user_find_by_user_string.assert_called_once_with(
|
|
query="gerardo.ruiz@kaseya.com", limit=10
|
|
)
|
|
assert updated is True
|
|
assert user.jira_account_id == "abc123"
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_lookup_user_jira_account_id_no_match(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_jira.user_find_by_user_string.return_value = []
|
|
mock_get_client.return_value = mock_jira
|
|
user = _make_user()
|
|
|
|
updated = jira_service.lookup_user_jira_account_id(db, user)
|
|
|
|
assert updated is False
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_assignee_update_assigns_jira_issue(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test()
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
assignee = _make_user(jira_account_id="account-42")
|
|
|
|
jira_service.push_assignee_update(db, test, assignee)
|
|
|
|
mock_jira.assign_issue.assert_called_once_with(link.jira_issue_key, account_id="account-42")
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_assignee_update_noop_without_jira_account_id(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_jira.user_find_by_user_string.return_value = [] # fallback lookup finds nobody either
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test()
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
assignee = _make_user(jira_account_id=None)
|
|
|
|
jira_service.push_assignee_update(db, test, assignee)
|
|
|
|
mock_jira.assign_issue.assert_not_called()
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_assignee_update_looks_up_missing_account_id(mock_get_client, mock_configured, db):
|
|
"""Regression: a lead can assign someone who has never logged in, so
|
|
jira_account_id isn't populated yet. Must try a fresh lookup instead of
|
|
silently giving up, so the assignment still syncs immediately."""
|
|
mock_jira = MagicMock()
|
|
mock_jira.user_find_by_user_string.return_value = [
|
|
{"emailAddress": "gerardo.ruiz@kaseya.com", "accountId": "freshly-found-id"}
|
|
]
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test()
|
|
link = _make_link(test.id)
|
|
db.add(link)
|
|
db.commit()
|
|
assignee = _make_user(jira_account_id=None)
|
|
|
|
jira_service.push_assignee_update(db, test, assignee)
|
|
|
|
mock_jira.assign_issue.assert_called_once_with(link.jira_issue_key, account_id="freshly-found-id")
|
|
assert assignee.jira_account_id == "freshly-found-id"
|
|
|
|
|
|
@patch("app.services.jira_service.has_admin_jira_configured", return_value=True)
|
|
@patch("app.services.jira_service.get_admin_jira_client")
|
|
def test_push_assignee_update_noop_without_link(mock_get_client, mock_configured, db):
|
|
mock_jira = MagicMock()
|
|
mock_get_client.return_value = mock_jira
|
|
test = _make_test()
|
|
assignee = _make_user(jira_account_id="account-42")
|
|
|
|
jira_service.push_assignee_update(db, test, assignee)
|
|
|
|
mock_jira.assign_issue.assert_not_called()
|