oncall-engine/engine/apps/user_management/tests/test_sync.py
Joey Orlando abedea72bf
don't force create default user notification policies (#4608)
# What this PR does

Related to https://github.com/grafana/oncall/issues/4410

The changes in this PR are a prerequisite to
https://github.com/grafana/terraform-provider-grafana/pull/1653. See the
conversation
[here](https://raintank-corp.slack.com/archives/C04JCU51NF8/p1719806995902499?thread_ts=1719520920.744319&cid=C04JCU51NF8)
for more context on why we decided to move away from always creating
default personal notification rules for users.

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.
2024-07-05 15:08:17 -04:00

475 lines
17 KiB
Python

from contextlib import contextmanager
from dataclasses import dataclass
from typing import Optional
from unittest.mock import patch
import pytest
from django.conf import settings
from django.test import override_settings
from apps.alerts.models import AlertReceiveChannel
from apps.api.permissions import LegacyAccessControlRole
from apps.grafana_plugin.helpers.client import GrafanaAPIClient
from apps.user_management.models import Team, User
from apps.user_management.sync import (
_sync_grafana_incident_plugin,
_sync_grafana_labels_plugin,
cleanup_organization,
sync_organization,
)
MOCK_GRAFANA_INCIDENT_BACKEND_URL = "https://grafana-incident.test"
@contextmanager
def patched_grafana_api_client(organization, is_rbac_enabled_for_organization=False):
GRAFANA_INCIDENT_PLUGIN_BACKEND_URL_KEY = "backendUrl"
with patch("apps.user_management.sync.GrafanaAPIClient") as mock_grafana_api_client:
mock_grafana_api_client.GRAFANA_INCIDENT_PLUGIN_BACKEND_URL_KEY = GRAFANA_INCIDENT_PLUGIN_BACKEND_URL_KEY
mock_client_instance = mock_grafana_api_client.return_value
mock_client_instance.get_users.return_value = [
{
"userId": 1,
"email": "test@test.test",
"name": "Test",
"login": "test",
"role": "admin",
"avatarUrl": "test.test/test",
"permissions": [],
},
]
mock_client_instance.get_teams.return_value = (
{
"totalCount": 1,
"teams": (
{
"id": 1,
"name": "Test",
"email": "test@test.test",
"avatarUrl": "test.test/test",
},
),
},
None,
)
mock_client_instance.get_team_members.return_value = (
[
{
"orgId": organization.org_id,
"teamId": 1,
"userId": 1,
},
],
None,
)
mock_client_instance.get_grafana_incident_plugin_settings.return_value = (
{"enabled": True, "jsonData": {GRAFANA_INCIDENT_PLUGIN_BACKEND_URL_KEY: MOCK_GRAFANA_INCIDENT_BACKEND_URL}},
None,
)
mock_client_instance.get_grafana_labels_plugin_settings.return_value = (
{"enabled": True, "jsonData": {}},
None,
)
mock_client_instance.check_token.return_value = (None, {"connected": True})
mock_client_instance.is_rbac_enabled_for_organization.return_value = is_rbac_enabled_for_organization
yield mock_grafana_api_client
@pytest.mark.django_db
def test_sync_users_for_organization(make_organization, make_user_for_organization):
organization = make_organization(grafana_url="https://test.test")
users = tuple(make_user_for_organization(organization, user_id=user_id) for user_id in (1, 2))
api_users = tuple(
{
"userId": user_id,
"email": "test@test.test",
"name": "Test",
"login": "test",
"role": "admin",
"avatarUrl": "/test/1234",
"permissions": [],
}
for user_id in (2, 3)
)
User.objects.sync_for_organization(organization, api_users=api_users)
assert organization.users.count() == 2
# check that excess users are deleted
assert not organization.users.filter(pk=users[0].pk).exists()
# check that existing users are updated
updated_user = organization.users.filter(pk=users[1].pk).first()
assert updated_user is not None
assert updated_user.name == api_users[0]["name"]
assert updated_user.email == api_users[0]["email"]
assert updated_user.avatar_full_url == "https://test.test/test/1234"
# check that missing users are created
created_user = organization.users.filter(user_id=api_users[1]["userId"]).first()
assert created_user is not None
assert created_user.user_id == api_users[1]["userId"]
assert created_user.name == api_users[1]["name"]
assert created_user.avatar_full_url == "https://test.test/test/1234"
@pytest.mark.django_db
def test_sync_users_for_organization_role_none(make_organization, make_user_for_organization):
organization = make_organization(grafana_url="https://test.test")
users = tuple(make_user_for_organization(organization, user_id=user_id) for user_id in (1, 2))
api_users = tuple(
{
"userId": user_id,
"email": "test@test.test",
"name": "Test",
"login": "test",
"role": "None",
"avatarUrl": "/test/1234",
"permissions": [],
}
for user_id in (2, 3)
)
User.objects.sync_for_organization(organization, api_users=api_users)
assert organization.users.count() == 2
# check that excess users are deleted
assert not organization.users.filter(pk=users[0].pk).exists()
# check that existing users are updated
updated_user = organization.users.filter(pk=users[1].pk).first()
assert updated_user is not None
assert updated_user.role == LegacyAccessControlRole.NONE
# check that missing users are created
created_user = organization.users.filter(user_id=api_users[1]["userId"]).first()
assert created_user is not None
assert created_user.user_id == api_users[1]["userId"]
assert created_user.role == LegacyAccessControlRole.NONE
@pytest.mark.django_db
def test_sync_teams_for_organization(make_organization, make_team, make_alert_receive_channel):
organization = make_organization()
teams = tuple(make_team(organization, team_id=team_id) for team_id in (1, 2, 3))
direct_paging_integrations = tuple(
make_alert_receive_channel(organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=team)
for team in teams[:2]
)
api_teams = tuple(
{"id": team_id, "name": "Test", "email": "test@test.test", "avatarUrl": "test.test/test"}
for team_id in (2, 3, 4)
)
Team.objects.sync_for_organization(organization, api_teams=api_teams)
assert organization.teams.count() == 3
# check that excess teams and direct paging integrations are deleted
assert not organization.teams.filter(pk=teams[0].pk).exists()
assert not organization.alert_receive_channels.filter(pk=direct_paging_integrations[0].pk).exists()
# check that existing teams are updated
updated_team = organization.teams.filter(pk=teams[1].pk).first()
assert updated_team is not None
assert updated_team.name == api_teams[0]["name"]
assert updated_team.email == api_teams[0]["email"]
assert organization.alert_receive_channels.filter(pk=direct_paging_integrations[1].pk).exists()
# check that missing teams are created
created_team = organization.teams.filter(team_id=api_teams[2]["id"]).first()
assert created_team is not None
assert created_team.team_id == api_teams[2]["id"]
assert created_team.name == api_teams[2]["name"]
# check that direct paging is created for created team
direct_paging_integration = AlertReceiveChannel.objects.get(
organization=organization,
integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING,
team=created_team,
)
assert direct_paging_integration.channel_filters.count() == 1
assert direct_paging_integration.channel_filters.first().order == 0
assert direct_paging_integration.channel_filters.first().is_default
# check that direct paging is created for existing team
direct_paging_integration = AlertReceiveChannel.objects.get(
organization=organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING, team=teams[2]
)
assert direct_paging_integration.channel_filters.count() == 1
assert direct_paging_integration.channel_filters.first().order == 0
assert direct_paging_integration.channel_filters.first().is_default
@pytest.mark.django_db
def test_sync_users_for_team(make_organization, make_user_for_organization, make_team):
organization = make_organization()
team = make_team(organization)
users = tuple(make_user_for_organization(organization) for _ in range(2))
api_members = (
{
"orgId": organization.org_id,
"teamId": team.team_id,
"userId": users[0].user_id,
},
)
User.objects.sync_for_team(team, api_members=api_members)
assert team.users.count() == 1
assert team.users.get() == users[0]
@pytest.mark.django_db
@patch("apps.user_management.sync.org_sync_signal")
def test_sync_organization(mocked_org_sync_signal, make_organization):
organization = make_organization()
with patched_grafana_api_client(organization):
sync_organization(organization)
# check that users are populated
assert organization.users.count() == 1
user = organization.users.get()
assert user.user_id == 1
# check that teams are populated
assert organization.teams.count() == 1
team = organization.teams.get()
assert team.team_id == 1
# check that team members are populated
assert team.users.count() == 1
assert team.users.get() == user
# check that is_grafana_incident_enabled flag is set
assert organization.is_grafana_incident_enabled is True
assert organization.grafana_incident_backend_url == MOCK_GRAFANA_INCIDENT_BACKEND_URL
# check that is_grafana_labels_enabled flag is set
assert organization.is_grafana_labels_enabled is True
mocked_org_sync_signal.send.assert_called_once_with(sender=None, organization=organization)
@pytest.mark.parametrize("is_rbac_enabled_for_organization", [False, True])
@override_settings(LICENSE=settings.OPEN_SOURCE_LICENSE_NAME)
@pytest.mark.django_db
def test_sync_organization_is_rbac_permissions_enabled_open_source(make_organization, is_rbac_enabled_for_organization):
organization = make_organization()
with patched_grafana_api_client(organization, is_rbac_enabled_for_organization):
sync_organization(organization)
organization.refresh_from_db()
assert organization.is_rbac_permissions_enabled == is_rbac_enabled_for_organization
@pytest.mark.parametrize(
"gcom_api_response,grafana_api_response,org_initial_value,org_is_rbac_permissions_enabled_expected_value",
[
# stack is in an inactive state, rely on org's previous state of is_rbac_permissions_enabled
(False, False, False, False),
(False, False, True, True),
# stack is active, Grafana API tells us RBAC is not enabled
(True, False, True, False),
# stack is active, Grafana API tells us RBAC is enabled
(True, True, False, True),
],
)
@patch("apps.user_management.sync.GcomAPIClient")
@override_settings(LICENSE=settings.CLOUD_LICENSE_NAME)
@pytest.mark.django_db
def test_sync_organization_is_rbac_permissions_enabled_cloud(
mock_gcom_client,
make_organization,
gcom_api_response,
grafana_api_response,
org_initial_value,
org_is_rbac_permissions_enabled_expected_value,
):
stack_id = 5
organization = make_organization(stack_id=stack_id, is_rbac_permissions_enabled=org_initial_value)
mock_gcom_client.return_value.is_stack_active.return_value = gcom_api_response
assert organization.is_rbac_permissions_enabled == org_initial_value
with patched_grafana_api_client(organization, grafana_api_response) as mock_grafana_api_client:
sync_organization(organization)
organization.refresh_from_db()
assert organization.is_rbac_permissions_enabled == org_is_rbac_permissions_enabled_expected_value
mock_gcom_client.return_value.is_stack_active.assert_called_once_with(stack_id)
if gcom_api_response:
mock_grafana_api_client.return_value.is_rbac_enabled_for_organization.assert_called_once_with()
@pytest.mark.django_db
def test_duplicate_user_ids(make_organization, make_user_for_organization):
organization = make_organization()
user = make_user_for_organization(organization, user_id=1)
api_users = []
User.objects.sync_for_organization(organization, api_users=api_users)
user.refresh_from_db()
assert user.is_active is None
assert organization.users.count() == 0
assert User.objects.filter_with_deleted().count() == 1
api_users = [
{
"userId": 1,
"email": "newtest@test.test",
"name": "New Test",
"login": "test",
"role": "admin",
"avatarUrl": "test.test/test",
"permissions": [],
}
]
User.objects.sync_for_organization(organization, api_users=api_users)
assert organization.users.count() == 1
assert organization.users.get().email == "newtest@test.test"
assert User.objects.filter_with_deleted().count() == 2
@pytest.mark.django_db
@pytest.mark.parametrize("is_deleted", [True, False])
def test_cleanup_organization_deleted(make_organization, is_deleted):
organization = make_organization(gcom_token="TEST_GCOM_TOKEN")
with patch("apps.grafana_plugin.helpers.client.GcomAPIClient.is_stack_deleted", return_value=is_deleted):
cleanup_organization(organization.id)
organization.refresh_from_db()
assert (organization.deleted_at is not None) == is_deleted
@pytest.mark.django_db
def test_organization_not_deleted(make_organization):
organization = make_organization(gcom_token="TEST_GCOM_TOKEN")
with patch("apps.grafana_plugin.helpers.client.GcomAPIClient.is_stack_deleted") as mock_method:
exception_message = "Test Exception"
mock_method.side_effect = Exception(exception_message)
with pytest.raises(Exception) as e:
cleanup_organization(organization.id)
assert str(e.value) == exception_message
organization.refresh_from_db()
assert organization.deleted_at is None
@pytest.mark.django_db
@pytest.mark.parametrize("task_lock_acquired", [True, False])
@patch("apps.user_management.sync.task_lock")
@patch("apps.user_management.sync.uuid.uuid4", return_value="random")
def test_sync_organization_lock(
_mock_uuid4,
mock_task_lock,
make_organization,
task_lock_acquired,
):
organization = make_organization()
lock_cache_key = f"sync-organization-lock-{organization.id}"
mock_task_lock.return_value.__enter__.return_value = task_lock_acquired
with patched_grafana_api_client(organization) as mock_grafana_api_client:
sync_organization(organization)
mock_task_lock.assert_called_once_with(lock_cache_key, "random")
if task_lock_acquired:
mock_grafana_api_client.assert_called_once()
else:
# task lock could not be acquired
mock_grafana_api_client.assert_not_called()
@dataclass
class TestSyncGrafanaLabelsPluginParams:
__test__ = False
response: tuple
expected_result: bool
@pytest.mark.django_db
@pytest.mark.parametrize(
"test_params",
[
TestSyncGrafanaLabelsPluginParams(({"enabled": True, "jsonData": {}}, None), True),
TestSyncGrafanaLabelsPluginParams(({"enabled": True}, None), True),
TestSyncGrafanaLabelsPluginParams(({"enabled": False}, None), False),
],
)
@pytest.mark.django_db
def test_sync_grafana_labels_plugin(make_organization, test_params: TestSyncGrafanaLabelsPluginParams):
organization = make_organization()
organization.is_grafana_labels_enabled = False # by default in tests it's true, so setting to false
with patch.object(
GrafanaAPIClient,
"get_grafana_labels_plugin_settings",
return_value=test_params.response,
):
grafana_api_client = GrafanaAPIClient(api_url=organization.grafana_url, api_token=organization.api_token)
_sync_grafana_labels_plugin(organization, grafana_api_client)
assert organization.is_grafana_labels_enabled is test_params.expected_result
@dataclass
class TestSyncGrafanaIncidentParams:
__test__ = False
response: tuple
expected_flag: bool
expected_url: Optional[str]
@pytest.mark.django_db
@pytest.mark.parametrize(
"test_params",
[
TestSyncGrafanaIncidentParams(
({"enabled": True, "jsonData": {"backendUrl": MOCK_GRAFANA_INCIDENT_BACKEND_URL}}, None),
True,
MOCK_GRAFANA_INCIDENT_BACKEND_URL,
),
TestSyncGrafanaIncidentParams(({"enabled": True}, None), True, None),
TestSyncGrafanaIncidentParams(({"enabled": True, "jsonData": None}, None), True, None),
# missing jsonData (sometimes this is what we get back from the Grafana API)
TestSyncGrafanaIncidentParams(({"enabled": False}, None), False, None), # plugin is disabled for some reason
],
)
@pytest.mark.django_db
def test_sync_grafana_incident_plugin(make_organization, test_params: TestSyncGrafanaIncidentParams):
organization = make_organization()
with patch.object(
GrafanaAPIClient,
"get_grafana_incident_plugin_settings",
return_value=test_params.response,
):
grafana_api_client = GrafanaAPIClient(api_url=organization.grafana_url, api_token=organization.api_token)
_sync_grafana_incident_plugin(organization, grafana_api_client)
assert organization.is_grafana_incident_enabled is test_params.expected_flag
assert organization.grafana_incident_backend_url is test_params.expected_url