def test_notify_creates_multiple_new_issues(self, get_config_mock):
        """Test notifier creates multiple GH issues when none exist."""
        config_mock = create_autospec(ComplianceConfig)
        config_mock.get.side_effect = [
            {
                'foo': {
                    'repo': ['foo/bar'], 'status': ['fail', 'pass']
                }
            },
            'https://github.com/foo/bar'
        ]
        get_config_mock.return_value = config_mock

        controls = create_autospec(ControlDescriptor)
        controls.get_accreditations.return_value = ['foo']
        self.search_issues_mock.return_value = []

        notifier = GHIssuesNotifier(self.results, controls)
        notifier.notify()
        self.assertEqual(self.search_issues_mock.call_count, 2)
        self.search_issues_mock.assert_any_call(
            'fail_example type:issue in:title is:open repo:foo/bar'
        )
        self.search_issues_mock.assert_any_call(
            'pass_example type:issue in:title is:open repo:foo/bar'
        )
        self.assertEqual(self.add_issue_mock.call_count, 2)
        self.patch_issue_mock.assert_not_called()
        self.add_issue_comment_mock.assert_not_called()
    def test_add_issue_to_project_found(self, get_config_mock):
        """Test issue is not added to project if issue already assigned."""
        config_mock = create_autospec(ComplianceConfig)
        config_mock.get.side_effect = [
            {
                'accred_foo': {
                    'repo': ['foo/bar'],
                    'project': {
                        'valid-project': 'valid-column'
                    },
                    'summary_issue': {
                        'title': 'foo-title'
                    }
                }
            },
            'https://github.com/foo/bar'
        ]
        get_config_mock.return_value = config_mock

        controls = create_autospec(ControlDescriptor)
        controls.get_accreditations.return_value = ['accred_foo']
        self.search_issues_mock.return_value = [
            {
                'id': 1,
                'number': 123,
                'labels': [
                    {
                        'name': 'accreditation: foo', 'other': 'junk'
                    }, {
                        'name': 'run status: pass', 'other': 'junk'
                    }
                ],
                'title': 'foo-title',
                'url': '123.url'
            }
        ]
        self.get_all_projects_mock.return_value = [
            {
                'id': 1, 'name': 'valid-project'
            }
        ]
        self.get_columns_mock.return_value = [
            {
                'id': 11, 'name': 'valid-column'
            }
        ]
        self.get_all_cards_mock.return_value = {
            11: [{
                'content_url': '123.url'
            }]
        }

        notifier = GHIssuesNotifier(self.results, controls)
        notifier.logger.warning = MagicMock()
        notifier.notify()
        self.get_all_projects_mock.assert_called_once_with('foo/bar')
        self.get_columns_mock.assert_called_once_with(1)
        self.get_all_cards_mock.assert_called_once()
        self.add_card_mock.assert_not_called()
        notifier.logger.warning.assert_not_called()
    def test_add_issue_to_project_no_config(self, get_config_mock):
        """Test issue is not added to project without config."""
        config_mock = create_autospec(ComplianceConfig)
        config_mock.get.side_effect = [
            {
                'accred_foo': {
                    'repo': ['foo/bar'],
                    'summary_issue': {
                        'title': 'foo-title'
                    }
                }
            },
            'https://github.com/foo/bar'
        ]
        get_config_mock.return_value = config_mock

        controls = create_autospec(ControlDescriptor)
        controls.get_accreditations.return_value = ['accred_foo']
        self.search_issues_mock.return_value = []

        notifier = GHIssuesNotifier(self.results, controls)
        notifier.logger.warning = MagicMock()
        notifier.notify()
        self.get_all_projects_mock.assert_not_called()
        self.get_columns_mock.assert_not_called()
        self.get_all_cards_mock.assert_not_called()
        self.add_card_mock.assert_not_called()
        notifier.logger.warning.assert_not_called()
    def test_notify_summary_issue_year_rotation_1(
        self, get_config_mock, datetime_mock
    ):
        """Test new yearly summary issue created and assigned to rota 1."""
        config_mock = create_autospec(ComplianceConfig)
        config_mock.get.side_effect = [
            {
                'accred_foo': {
                    'repo': ['foo/bar'],
                    'summary_issue': {
                        'title': 'foo-title',
                        'labels': ['label:foo', 'foo:label'],
                        'message': ['blah blah', 'foo message'],
                        'frequency': 'year',
                        'rotation': [['the-dude'], ['walter', 'donnie']]
                    }
                }
            },
            'https://github.com/foo/bar'
        ]
        get_config_mock.return_value = config_mock
        datetime_mock.utcnow.return_value = datetime(2019, 7, 31)  # Year 2019

        controls = create_autospec(ControlDescriptor)
        controls.get_accreditations.return_value = ['accred_foo']
        self.search_issues_mock.return_value = []

        notifier = GHIssuesNotifier(self.results, controls)
        notifier.notify()
        self.search_issues_mock.assert_called_once_with(
            '2019 - foo-title type:issue in:title is:open repo:foo/bar'
        )
        self.assertEqual(self.add_issue_mock.call_count, 1)
        args, kwargs = self.add_issue_mock.call_args
        self.assertEqual(len(args), 4)
        self.assertEqual(args[0], 'foo')
        self.assertEqual(args[1], 'bar')
        self.assertEqual(args[2], '2019 - foo-title')
        self.assertTrue(args[3].startswith('blah blah\nfoo message\n'))
        self.assertTrue('### Passed Checks' in args[3])
        self.assertTrue('### Errored Checks' in args[3])
        self.assertTrue('### Failures/Warnings' in args[3])
        self.assertEqual(
            kwargs,
            {
                'assignees': ['walter', 'donnie'],
                'labels': ['label:foo', 'foo:label', 'year', '2019']
            }
        )
        self.patch_issue_mock.assert_not_called()
        self.add_issue_comment_mock.assert_not_called()
    def test_notify_adds_label_and_comment_to_issue(self, get_config_mock):
        """Test notifier updates labels, adds comment on existing GH issue."""
        config_mock = create_autospec(ComplianceConfig)
        config_mock.get.side_effect = [
            {
                'foo': {
                    'repo': ['foo/bar'], 'status': ['pass']
                }
            },
            'https://github.com/foo/bar'
        ]
        get_config_mock.return_value = config_mock

        controls = create_autospec(ControlDescriptor)
        controls.get_accreditations.return_value = ['foo']
        self.search_issues_mock.return_value = [
            {
                'id': 1,
                'number': 123,
                'labels': [
                    {
                        'name': 'accreditation: foo', 'other': 'junk'
                    }, {
                        'name': 'run status: warn', 'other': 'junk'
                    }
                ],
                'title': 'pass_example',
                'url': '123.url'
            }
        ]

        notifier = GHIssuesNotifier(self.results, controls)
        notifier.notify()
        self.search_issues_mock.assert_called_once_with(
            'pass_example type:issue in:title is:open repo:foo/bar'
        )
        self.add_issue_mock.assert_not_called()
        self.patch_issue_mock.assert_called_once_with(
            'foo',
            'bar',
            123,
            labels=['accreditation: foo', 'run status: pass']
        )
        self.assertEqual(self.add_issue_comment_mock.call_count, 1)
        args, kwargs = self.add_issue_comment_mock.call_args
        self.assertEqual(len(args), 4)
        self.assertEqual(args[0], 'foo')
        self.assertEqual(args[1], 'bar')
        self.assertEqual(args[2], 123)
        self.assertTrue(args[3].startswith('## Compliance check alert'))
        self.assertEqual(kwargs, {})
    def test_notify_summary_issue_w_labels(self, get_config_mock):
        """Test notifier creates a new summary issue with labels only."""
        config_mock = create_autospec(ComplianceConfig)
        config_mock.get.side_effect = [
            {
                'accred_foo': {
                    'repo': ['foo/bar'],
                    'summary_issue': {
                        'title': 'foo-title',
                        'labels': ['label:foo', 'foo:label']
                    }
                }
            },
            'https://github.com/foo/bar'
        ]
        get_config_mock.return_value = config_mock

        controls = create_autospec(ControlDescriptor)
        controls.get_accreditations.return_value = ['accred_foo']
        self.search_issues_mock.return_value = []

        notifier = GHIssuesNotifier(self.results, controls)
        notifier.notify()
        self.search_issues_mock.assert_called_once_with(
            'foo-title type:issue in:title is:open repo:foo/bar'
        )
        self.assertEqual(self.add_issue_mock.call_count, 1)
        args, kwargs = self.add_issue_mock.call_args
        self.assertEqual(len(args), 4)
        self.assertEqual(args[0], 'foo')
        self.assertEqual(args[1], 'bar')
        self.assertEqual(args[2], 'foo-title')
        self.assertTrue(args[3].startswith('# CHECK RESULTS:'))
        self.assertTrue('### Passed Checks' in args[3])
        self.assertTrue('### Errored Checks' in args[3])
        self.assertTrue('### Failures/Warnings' in args[3])
        self.assertEqual(
            kwargs, {
                'assignees': [], 'labels': ['label:foo', 'foo:label']
            }
        )
        self.patch_issue_mock.assert_not_called()
        self.add_issue_comment_mock.assert_not_called()
    def test_notify_old_alert_does_nothing(self, get_config_mock):
        """Test notifier does not notify for passed check w/out open issue."""
        config_mock = create_autospec(ComplianceConfig)
        config_mock.get.side_effect = [
            {
                'foo': {
                    'repo': ['foo/bar'], 'status': ['fail']
                }
            },
            'https://github.com/foo/bar'
        ]
        get_config_mock.return_value = config_mock

        controls = create_autospec(ControlDescriptor)
        controls.get_accreditations.return_value = ['foo']
        self.search_issues_mock.return_value = []

        notifier = GHIssuesNotifier(self.results, controls)
        notifier.notify()
        self.assertEqual(self.search_issues_mock.call_count, 2)
        self.search_issues_mock.assert_any_call(
            'fail_example type:issue in:title is:open repo:foo/bar'
        )
        self.search_issues_mock.assert_any_call(
            'pass_example type:issue in:title is:open repo:foo/bar'
        )
        self.assertEqual(self.add_issue_mock.call_count, 1)
        args, kwargs = self.add_issue_mock.call_args
        self.assertEqual(len(args), 4)
        self.assertEqual(args[0], 'foo')
        self.assertEqual(args[1], 'bar')
        self.assertEqual(args[2], 'fail_example')
        self.assertTrue(args[3].startswith('## Compliance check alert'))
        self.assertEqual(
            kwargs,
            {
                'assignees': [],
                'labels': ['accreditation: foo', 'run status: fail']
            }
        )
        self.patch_issue_mock.assert_not_called()
        self.add_issue_comment_mock.assert_not_called()
    def test_notify_creates_new_issue_partial_match(self, get_config_mock):
        """Test notifier creates new issue when partial title match exists."""
        config_mock = create_autospec(ComplianceConfig)
        config_mock.get.side_effect = [
            {
                'foo': {
                    'repo': ['foo/bar'], 'status': ['pass']
                }
            },
            'https://github.com/foo/bar'
        ]
        get_config_mock.return_value = config_mock

        controls = create_autospec(ControlDescriptor)
        controls.get_accreditations.return_value = ['foo']
        self.search_issues_mock.return_value = [{'title': 'x pass_example x'}]

        notifier = GHIssuesNotifier(self.results, controls)
        notifier.notify()
        self.search_issues_mock.assert_called_once_with(
            'pass_example type:issue in:title is:open repo:foo/bar'
        )
        self.assertEqual(self.add_issue_mock.call_count, 1)
        args, kwargs = self.add_issue_mock.call_args
        self.assertEqual(len(args), 4)
        self.assertEqual(args[0], 'foo')
        self.assertEqual(args[1], 'bar')
        self.assertEqual(args[2], 'pass_example')
        self.assertTrue(args[3].startswith('## Compliance check alert'))
        self.assertEqual(
            kwargs,
            {
                'assignees': [],
                'labels': ['accreditation: foo', 'run status: pass']
            }
        )

        self.patch_issue_mock.assert_not_called()
        self.add_issue_comment_mock.assert_not_called()
    def test_notify_alerts_in_multiple_repos(self, get_config_mock):
        """Test notifier notifies in multiple repositories."""
        config_mock = create_autospec(ComplianceConfig)
        config_mock.get.side_effect = [
            {
                'foo': {
                    'repo': ['foo/bar', 'bing/bong'], 'status': ['pass']
                }
            },
            'https://github.com/foo/bar'
        ]
        get_config_mock.return_value = config_mock

        controls = create_autospec(ControlDescriptor)
        controls.get_accreditations.return_value = ['foo']
        self.search_issues_mock.return_value = []

        notifier = GHIssuesNotifier(self.results, controls)
        notifier.notify()
        self.assertEqual(self.search_issues_mock.call_count, 2)
        self.search_issues_mock.assert_any_call(
            'pass_example type:issue in:title is:open repo:foo/bar'
        )
        self.search_issues_mock.assert_any_call(
            'pass_example type:issue in:title is:open repo:bing/bong'
        )
        self.assertEqual(self.add_issue_mock.call_count, 2)
        foo_bar_call_args, _ = self.add_issue_mock.call_args_list[0]
        bing_bong_call_args, _ = self.add_issue_mock.call_args_list[1]
        self.assertEqual(foo_bar_call_args[0], 'foo')
        self.assertEqual(foo_bar_call_args[1], 'bar')
        self.assertEqual(bing_bong_call_args[0], 'bing')
        self.assertEqual(bing_bong_call_args[1], 'bong')

        self.patch_issue_mock.assert_not_called()
        self.add_issue_comment_mock.assert_not_called()