"""Unit tests for the suspend-ticket query actions on the Lambda handler.""" from typing import Any from pytest import MonkeyPatch from jira_client import app class FakeClient: """Fake JiraClient returning canned raw tickets and capturing the JQL.""" def __init__(self, raw_tickets: list[dict[str, Any]]) -> None: """Store the canned tickets and init the captured-query slot.""" self._raw_tickets = raw_tickets self.last_query: str | None = None def query_jira_tickets(self, query: str) -> list[dict[str, Any]]: """Record the query and return the canned tickets.""" self.last_query = query return self._raw_tickets def _adf_ticket(key: str, text: str) -> dict[str, Any]: """Build a raw Jira ticket whose ADF description is a single paragraph. :param key: The Jira issue key. :param text: The paragraph text content. :return: A raw ticket dict shaped like the Jira search API response. """ return { 'key': key, 'fields': { 'description': { 'content': [ { 'type': 'paragraph', 'content': [{'type': 'text', 'text': text}], } ] } }, } SUSPEND_DESCRIPTION = ( 'A suspend network access request has been submitted for John Doe. ' 'Please ensure all application access is removed. ' 'User ID: JD001 Title: Engineer Email: john.doe@example.com ' 'Department: Engineering Suspension Start Date: 2026-05-01 ' 'Suspension End Date: 2026-11-01' ) OFFBOARDING_DESCRIPTION = ( 'An offboarding request has been submitted for Alice Bob. ' 'Email Address: alice.bob@example.com Last day of Employment: 2026-06-15' ) def _install_fake_client( monkeypatch: MonkeyPatch, raw_tickets: list[dict[str, Any]] ) -> FakeClient: """Patch ``app._get_client`` to return a FakeClient with *raw_tickets*.""" fake = FakeClient(raw_tickets) monkeypatch.setattr(app, '_get_client', lambda: fake) return fake def test_query_suspend_tickets_parses_fields(monkeypatch: MonkeyPatch) -> None: """A suspend ticket is parsed into email, full_name and start date.""" fake = _install_fake_client( monkeypatch, [_adf_ticket('SYS-1', SUSPEND_DESCRIPTION)] ) result = app.handler({'action': 'query-suspend-tickets'}, None) assert result == { 'tickets': [ { 'id': 'SYS-1', 'email': 'john.doe@example.com', 'full_name': 'John Doe', 'last_working_day': '2026-05-01', } ] } assert fake.last_query is not None assert 'Global Technology Suspend Request' in fake.last_query def test_query_suspend_tickets_empty(monkeypatch: MonkeyPatch) -> None: """No matching tickets yields an empty list.""" _install_fake_client(monkeypatch, []) result = app.handler({'action': 'query-suspend-tickets'}, None) assert result == {'tickets': []} def test_query_suspend_tickets_skips_missing_description( monkeypatch: MonkeyPatch, ) -> None: """A ticket without a description is soft-skipped.""" _install_fake_client(monkeypatch, [{'key': 'SYS-2', 'fields': {}}]) result = app.handler({'action': 'query-suspend-tickets'}, None) assert result == {'tickets': []} def test_query_suspend_tickets_rejects_offboarding_description( monkeypatch: MonkeyPatch, ) -> None: """An offboarding description does not satisfy the suspend sentinel. This is the cross-type guard: the suspend action must never pick up an offboarding ticket, even if the underlying JQL ever returned one. """ _install_fake_client(monkeypatch, [_adf_ticket('SYS-3', OFFBOARDING_DESCRIPTION)]) result = app.handler({'action': 'query-suspend-tickets'}, None) assert result == {'tickets': []} def test_query_approved_suspend_tickets_uses_approved_jql( monkeypatch: MonkeyPatch, ) -> None: """The approved-suspend action queries with the approved-for-suspension JQL.""" fake = _install_fake_client( monkeypatch, [_adf_ticket('SYS-4', SUSPEND_DESCRIPTION)] ) result = app.handler({'action': 'query-approved-suspend-tickets'}, None) assert result['tickets'][0]['id'] == 'SYS-4' assert fake.last_query is not None assert 'approved-for-suspension' in fake.last_query assert 'Global Technology Suspend Request' in fake.last_query