fix(campaigns): defer Jira ticket creation to start_date, gate recurring campaigns behind manager approval
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
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
- Approve endpoint now only creates Jira tickets immediately when start_date is now/past; a new periodic job (every 15 min) catches campaigns whose scheduled start_date has since arrived. - Recurring campaign clones now go to pending_approval instead of active, routing through the same manager-approval gate as any other campaign; managers are notified instead of red_tech. - Fix UTC conversion for the campaign approval start_date input and extract shared isoToDatetimeLocal/datetimeLocalToIso helpers.
This commit is contained in:
@@ -21,6 +21,7 @@ from app.services.campaign_service import (
|
||||
from app.services.campaign_scheduler_service import (
|
||||
calculate_next_run,
|
||||
check_and_run_recurring_campaigns,
|
||||
sync_due_campaign_jira_tickets,
|
||||
)
|
||||
from app.services.snapshot_service import (
|
||||
create_snapshot,
|
||||
@@ -199,7 +200,10 @@ class TestCampaigns:
|
||||
)
|
||||
assert child is not None
|
||||
assert "Run" in child.name
|
||||
assert child.status == "active"
|
||||
# Recurrence only automates spawning the run — it still needs a
|
||||
# manager's approval like any other campaign, same as the manual
|
||||
# draft -> submit -> approve path.
|
||||
assert child.status == "pending_approval"
|
||||
|
||||
# Check child tests are fresh copies (new IDs, draft state)
|
||||
child_cts = (
|
||||
@@ -217,12 +221,92 @@ class TestCampaigns:
|
||||
test = db.query(Test).filter(Test.id == ct.test_id).first()
|
||||
assert test.state == TestState.draft
|
||||
|
||||
def test_campaign_cloning_notifies_managers(self, db, campaign_with_tests, manager_user):
|
||||
"""Managers must hear about a spawned recurring run — it's sitting in
|
||||
their approval queue just like a manually-submitted campaign."""
|
||||
from app.models.notification import Notification
|
||||
|
||||
campaign = campaign_with_tests["campaign"]
|
||||
campaign.is_recurring = True
|
||||
campaign.recurrence_pattern = "monthly"
|
||||
campaign.next_run_at = datetime.utcnow() - timedelta(hours=1)
|
||||
db.commit()
|
||||
|
||||
check_and_run_recurring_campaigns(db)
|
||||
|
||||
notif = (
|
||||
db.query(Notification)
|
||||
.filter(Notification.user_id == manager_user.id, Notification.type == "campaign_pending_approval")
|
||||
.first()
|
||||
)
|
||||
assert notif is not None
|
||||
|
||||
# Check parent was updated
|
||||
db.refresh(campaign)
|
||||
assert campaign.last_run_at is not None
|
||||
assert campaign.next_run_at > datetime.utcnow()
|
||||
|
||||
|
||||
class TestDueCampaignJiraSync:
|
||||
"""sync_due_campaign_jira_tickets — the periodic catch-up job for
|
||||
campaigns approved with a future start_date whose Jira tickets were
|
||||
deliberately skipped at approval time."""
|
||||
|
||||
def test_creates_tickets_for_due_campaign_without_jira_link(self, db, campaign_with_tests, admin_user):
|
||||
from unittest.mock import patch
|
||||
|
||||
campaign = campaign_with_tests["campaign"]
|
||||
campaign.status = "active"
|
||||
campaign.approved_by = admin_user.id
|
||||
campaign.start_date = datetime.utcnow() - timedelta(hours=1) # now due
|
||||
db.commit()
|
||||
|
||||
with patch(
|
||||
"app.services.jira_service.get_campaign_jira_key", return_value=None
|
||||
) as mock_get_key, patch(
|
||||
"app.services.jira_service.auto_create_campaign_issue", return_value="PT-200"
|
||||
) as mock_create_campaign, patch(
|
||||
"app.services.jira_service.get_test_jira_key", return_value=None
|
||||
), patch(
|
||||
"app.services.jira_service.auto_create_test_issue"
|
||||
) as mock_create_test:
|
||||
processed = sync_due_campaign_jira_tickets(db)
|
||||
|
||||
assert processed == 1
|
||||
mock_get_key.assert_called_once()
|
||||
mock_create_campaign.assert_called_once()
|
||||
assert mock_create_test.call_count == len(campaign_with_tests["tests"])
|
||||
|
||||
def test_skips_campaign_with_future_start_date(self, db, campaign_with_tests, admin_user):
|
||||
campaign = campaign_with_tests["campaign"]
|
||||
campaign.status = "active"
|
||||
campaign.approved_by = admin_user.id
|
||||
campaign.start_date = datetime.utcnow() + timedelta(days=30)
|
||||
db.commit()
|
||||
|
||||
processed = sync_due_campaign_jira_tickets(db)
|
||||
assert processed == 0
|
||||
|
||||
def test_skips_campaign_already_linked_to_jira(self, db, campaign_with_tests, admin_user):
|
||||
from app.models.jira_link import JiraLink, JiraLinkEntityType
|
||||
|
||||
campaign = campaign_with_tests["campaign"]
|
||||
campaign.status = "active"
|
||||
campaign.approved_by = admin_user.id
|
||||
campaign.start_date = datetime.utcnow() - timedelta(hours=1)
|
||||
db.add(JiraLink(
|
||||
entity_type=JiraLinkEntityType.campaign,
|
||||
entity_id=campaign.id,
|
||||
jira_issue_key="PT-1",
|
||||
jira_project_key="PT",
|
||||
created_by=admin_user.id,
|
||||
))
|
||||
db.commit()
|
||||
|
||||
processed = sync_due_campaign_jira_tickets(db)
|
||||
assert processed == 0
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Snapshot Tests
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user