2023-07-28 17:11:38 +02:00
|
|
|
import datetime
|
2022-06-03 08:09:47 -06:00
|
|
|
import json
|
2022-11-29 09:41:56 +01:00
|
|
|
import os
|
2022-06-03 08:09:47 -06:00
|
|
|
import sys
|
2022-11-21 16:26:00 +01:00
|
|
|
import typing
|
2022-06-03 08:09:47 -06:00
|
|
|
import uuid
|
|
|
|
|
from importlib import import_module, reload
|
|
|
|
|
|
|
|
|
|
import pytest
|
2023-05-24 14:27:48 +08:00
|
|
|
from celery import Task
|
2022-06-03 08:09:47 -06:00
|
|
|
from django.db.models.signals import post_save
|
|
|
|
|
from django.urls import clear_url_caches
|
2023-07-28 17:11:38 +02:00
|
|
|
from django.utils import timezone
|
2022-06-03 08:09:47 -06:00
|
|
|
from pytest_factoryboy import register
|
|
|
|
|
from rest_framework.test import APIClient
|
|
|
|
|
from telegram import Bot
|
|
|
|
|
|
|
|
|
|
from apps.alerts.models import (
|
|
|
|
|
Alert,
|
|
|
|
|
AlertGroupLogRecord,
|
|
|
|
|
AlertReceiveChannel,
|
|
|
|
|
MaintainableObject,
|
|
|
|
|
ResolutionNote,
|
|
|
|
|
listen_for_alertgrouplogrecord,
|
|
|
|
|
listen_for_alertreceivechannel_model_save,
|
|
|
|
|
)
|
|
|
|
|
from apps.alerts.signals import user_notification_action_triggered_signal
|
|
|
|
|
from apps.alerts.tests.factories import (
|
|
|
|
|
AlertFactory,
|
|
|
|
|
AlertGroupFactory,
|
|
|
|
|
AlertGroupLogRecordFactory,
|
|
|
|
|
AlertReceiveChannelFactory,
|
|
|
|
|
ChannelFilterFactory,
|
|
|
|
|
CustomActionFactory,
|
|
|
|
|
EscalationChainFactory,
|
|
|
|
|
EscalationPolicyFactory,
|
|
|
|
|
InvitationFactory,
|
|
|
|
|
ResolutionNoteFactory,
|
|
|
|
|
ResolutionNoteSlackMessageFactory,
|
|
|
|
|
)
|
2022-11-29 09:41:56 +01:00
|
|
|
from apps.api.permissions import (
|
|
|
|
|
ACTION_PREFIX,
|
|
|
|
|
GrafanaAPIPermission,
|
|
|
|
|
LegacyAccessControlCompatiblePermission,
|
|
|
|
|
LegacyAccessControlRole,
|
|
|
|
|
RBACPermission,
|
|
|
|
|
)
|
2023-07-18 10:31:11 -03:00
|
|
|
from apps.auth_token.models import ApiAuthToken, PluginAuthToken, SlackAuthToken
|
2022-06-03 08:09:47 -06:00
|
|
|
from apps.base.models.user_notification_policy_log_record import (
|
|
|
|
|
UserNotificationPolicyLogRecord,
|
|
|
|
|
listen_for_usernotificationpolicylogrecord_model_save,
|
|
|
|
|
)
|
|
|
|
|
from apps.base.tests.factories import (
|
|
|
|
|
LiveSettingFactory,
|
|
|
|
|
UserNotificationPolicyFactory,
|
|
|
|
|
UserNotificationPolicyLogRecordFactory,
|
|
|
|
|
)
|
2022-10-19 12:32:56 +01:00
|
|
|
from apps.email.tests.factories import EmailMessageFactory
|
2022-06-03 08:09:47 -06:00
|
|
|
from apps.heartbeat.tests.factories import IntegrationHeartBeatFactory
|
2023-11-06 10:31:12 +00:00
|
|
|
from apps.labels.tests.factories import (
|
|
|
|
|
AlertGroupAssociatedLabelFactory,
|
|
|
|
|
AlertReceiveChannelAssociatedLabelFactory,
|
|
|
|
|
LabelKeyFactory,
|
|
|
|
|
LabelValueFactory,
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
WebhookAssociatedLabelFactory,
|
2023-11-06 10:31:12 +00:00
|
|
|
)
|
Mobile app settings backend (#1571)
# What this PR does
Adds mobile app settings support to OnCall backend.
- Adds a new Django model `MobileAppUserSettings` to store push
notification settings
- Adds a new endpoint `/mobile_app/v1/user_settings` to fetch/update
settings from the mobile app
Some additional info on implementation: at first I wanted to extend the
messaging backend system to allow storing / retrieving per-user data and
implement mobile app settings based on those changes. After some thought
I decided not to extend the messaging backend system and have this as
functionality specific to the `mobile_app` Django app. Currently the
messaging backend system is used by the backend and plugin UI, but
mobile app settings are specific only to the mobile app and not
configurable in the plugin UI.
**tldr: wanted to extend messaging backend system, but decided not to do
that**
# Usage
## Get settings via API
`GET /mobile_app/v1/user_settings`
Example response:
```json
{
"default_notification_sound_name": "default_sound", # sound name without file extension
"default_notification_volume_type": "constant",
"default_notification_volume": 0.8,
"default_notification_volume_override": false,
"important_notification_sound_name": "default_sound_important", # sound name without file extension
"important_notification_volume_type": "constant",
"important_notification_volume": 0.8,
"important_notification_override_dnd": true
}
```
## Update settings via API
`PUT /mobile_app/v1/user_settings` - see example response above for
payload shape.
Note that sound names must be passed without file extension. When
sending push notifications, the backend will add `.mp3` to sound names
and pass it to push notification data for Android. For iOS, sound names
will be suffixed with `.aiff` to be used by APNS.
## Get settings from notification data for Android
All the settings from example response will be available in push
notification data (along with `orgId`, `alertGroupId`, `title`, etc.).
Fields `default_notification_volume`,
`default_notification_volume_override` and
`important_notification_volume` , `important_notification_override_dnd`
will be converted to strings due to FCM limitations.
Fields `default_notification_sound_name` and
`important_notification_sound_name` will be suffixed with `.mp3` in push
notification data.
## iOS limitations
While Android push notifications are handled purely on the mobile app
side, iOS notifications are sent via APNS which imposes some
limitations.
- Notification volume cannot be overridden for non-critical
notifications (so fields `default_notification_volume_override` and
`default_notification_volume` will be disregarded for iOS notifications)
- It's not possible to control volume type (i.e. "constant" vs
"intensifying") via APNS. A possible workaround is to have different
sound files for "constant" and "intensifying" and pass that as
`default_notification_sound_name` / `important_notification_sound_name`.
# Which issue(s) this PR fixes
Related to https://github.com/grafana/oncall-private/issues/1602
# Checklist
- [x] Tests updated
- [x] No changelog updates since the changes are not user-facing
2023-03-22 14:47:18 +00:00
|
|
|
from apps.mobile_app.models import MobileAppAuthToken, MobileAppVerificationToken
|
2023-05-24 14:27:48 +08:00
|
|
|
from apps.phone_notifications.phone_backend import PhoneBackend
|
|
|
|
|
from apps.phone_notifications.tests.factories import PhoneCallRecordFactory, SMSRecordFactory
|
|
|
|
|
from apps.phone_notifications.tests.mock_phone_provider import MockPhoneProvider
|
2023-11-06 13:52:50 -03:00
|
|
|
from apps.schedules.ical_utils import memoized_users_in_ical
|
2023-07-28 17:11:38 +02:00
|
|
|
from apps.schedules.models import OnCallScheduleWeb
|
2022-06-03 08:09:47 -06:00
|
|
|
from apps.schedules.tests.factories import (
|
|
|
|
|
CustomOnCallShiftFactory,
|
|
|
|
|
OnCallScheduleCalendarFactory,
|
|
|
|
|
OnCallScheduleFactory,
|
|
|
|
|
OnCallScheduleICalFactory,
|
2023-07-21 21:35:19 +02:00
|
|
|
ShiftSwapRequestFactory,
|
2022-06-03 08:09:47 -06:00
|
|
|
)
|
2023-09-12 10:49:16 +01:00
|
|
|
from apps.slack.client import SlackClient
|
2022-06-03 08:09:47 -06:00
|
|
|
from apps.slack.tests.factories import (
|
|
|
|
|
SlackChannelFactory,
|
|
|
|
|
SlackMessageFactory,
|
|
|
|
|
SlackTeamIdentityFactory,
|
|
|
|
|
SlackUserGroupFactory,
|
|
|
|
|
SlackUserIdentityFactory,
|
|
|
|
|
)
|
|
|
|
|
from apps.telegram.tests.factories import (
|
|
|
|
|
TelegramChannelFactory,
|
|
|
|
|
TelegramChannelVerificationCodeFactory,
|
|
|
|
|
TelegramMessageFactory,
|
|
|
|
|
TelegramToUserConnectorFactory,
|
|
|
|
|
TelegramVerificationCodeFactory,
|
|
|
|
|
)
|
2022-08-26 13:46:50 +05:00
|
|
|
from apps.user_management.models.user import User, listen_for_user_model_save
|
2022-10-27 15:40:46 -06:00
|
|
|
from apps.user_management.tests.factories import OrganizationFactory, RegionFactory, TeamFactory, UserFactory
|
2023-09-27 07:22:52 -06:00
|
|
|
from apps.webhooks.presets.preset_options import WebhookPresetOptions
|
2023-03-21 10:43:37 -03:00
|
|
|
from apps.webhooks.tests.factories import CustomWebhookFactory, WebhookResponseFactory
|
2023-09-27 07:22:52 -06:00
|
|
|
from apps.webhooks.tests.test_webhook_presets import TEST_WEBHOOK_PRESET_ID, TestWebhookPreset
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
register(OrganizationFactory)
|
|
|
|
|
register(UserFactory)
|
|
|
|
|
register(TeamFactory)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
register(AlertReceiveChannelFactory)
|
|
|
|
|
register(ChannelFilterFactory)
|
|
|
|
|
register(EscalationPolicyFactory)
|
|
|
|
|
register(OnCallScheduleICalFactory)
|
|
|
|
|
register(OnCallScheduleCalendarFactory)
|
|
|
|
|
register(CustomOnCallShiftFactory)
|
2023-07-21 21:35:19 +02:00
|
|
|
register(ShiftSwapRequestFactory)
|
2022-06-03 08:09:47 -06:00
|
|
|
register(AlertFactory)
|
|
|
|
|
register(AlertGroupFactory)
|
|
|
|
|
register(AlertGroupLogRecordFactory)
|
|
|
|
|
register(InvitationFactory)
|
|
|
|
|
register(CustomActionFactory)
|
|
|
|
|
register(SlackUserGroupFactory)
|
|
|
|
|
|
|
|
|
|
register(SlackUserIdentityFactory)
|
|
|
|
|
register(SlackTeamIdentityFactory)
|
|
|
|
|
register(SlackMessageFactory)
|
|
|
|
|
|
|
|
|
|
register(TelegramToUserConnectorFactory)
|
|
|
|
|
register(TelegramChannelFactory)
|
|
|
|
|
register(TelegramVerificationCodeFactory)
|
|
|
|
|
register(TelegramChannelVerificationCodeFactory)
|
|
|
|
|
register(TelegramMessageFactory)
|
|
|
|
|
|
|
|
|
|
register(ResolutionNoteSlackMessageFactory)
|
|
|
|
|
|
2023-05-24 14:27:48 +08:00
|
|
|
register(PhoneCallRecordFactory)
|
|
|
|
|
register(SMSRecordFactory)
|
2022-10-19 12:32:56 +01:00
|
|
|
register(EmailMessageFactory)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
register(IntegrationHeartBeatFactory)
|
|
|
|
|
register(LiveSettingFactory)
|
|
|
|
|
|
2023-10-20 09:30:11 +02:00
|
|
|
register(LabelKeyFactory)
|
|
|
|
|
register(LabelValueFactory)
|
|
|
|
|
register(AlertReceiveChannelAssociatedLabelFactory)
|
|
|
|
|
|
2022-11-29 09:41:56 +01:00
|
|
|
IS_RBAC_ENABLED = os.getenv("ONCALL_TESTING_RBAC_ENABLED", "True") == "True"
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def mock_slack_api_call(monkeypatch):
|
|
|
|
|
def mock_api_call(*args, **kwargs):
|
|
|
|
|
return {
|
|
|
|
|
"status": 200,
|
|
|
|
|
"usergroups": [],
|
|
|
|
|
"channel": {"id": "TEST_CHANNEL_ID"},
|
|
|
|
|
"user": {
|
|
|
|
|
"name": "TEST_SLACK_LOGIN",
|
|
|
|
|
"real_name": "TEST_SLACK_NAME",
|
|
|
|
|
"profile": {"image_512": "TEST_SLACK_IMAGE"},
|
|
|
|
|
},
|
|
|
|
|
"team": {"name": "TEST_TEAM"},
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-12 10:49:16 +01:00
|
|
|
monkeypatch.setattr(SlackClient, "api_call", mock_api_call)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def mock_telegram_bot_username(monkeypatch):
|
|
|
|
|
def mock_username(*args, **kwargs):
|
2023-08-01 20:22:42 +02:00
|
|
|
return "oncall_bot"
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
monkeypatch.setattr(Bot, "username", mock_username)
|
|
|
|
|
|
|
|
|
|
|
2023-05-24 14:27:48 +08:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def mock_phone_provider(monkeypatch):
|
|
|
|
|
def mock_get_provider(*args, **kwargs):
|
|
|
|
|
return MockPhoneProvider()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(PhoneBackend, "_get_phone_provider", mock_get_provider)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def mock_apply_async(monkeypatch):
|
|
|
|
|
def mock_apply_async(*args, **kwargs):
|
|
|
|
|
return uuid.uuid4()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(Task, "apply_async", mock_apply_async)
|
|
|
|
|
|
|
|
|
|
|
2023-10-20 09:30:11 +02:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def mock_is_labels_feature_enabled(settings):
|
2023-11-02 10:52:32 +01:00
|
|
|
setattr(settings, "FEATURE_LABELS_ENABLED_FOR_ALL", True)
|
|
|
|
|
|
|
|
|
|
|
2023-11-06 13:52:50 -03:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
|
def clear_ical_users_cache():
|
|
|
|
|
# clear users pks <-> organization cache (persisting between tests)
|
|
|
|
|
memoized_users_in_ical.cache_clear()
|
|
|
|
|
|
|
|
|
|
|
2023-11-02 10:52:32 +01:00
|
|
|
@pytest.fixture
|
|
|
|
|
def mock_is_labels_feature_enabled_for_org(settings):
|
|
|
|
|
def _mock_is_labels_feature_enabled_for_org(org_id):
|
|
|
|
|
setattr(settings, "FEATURE_LABELS_ENABLED_FOR_ALL", False)
|
|
|
|
|
setattr(settings, "FEATURE_LABELS_ENABLED_FOR_GRAFANA_ORGS", [org_id])
|
|
|
|
|
|
|
|
|
|
return _mock_is_labels_feature_enabled_for_org
|
2023-10-20 09:30:11 +02:00
|
|
|
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_organization():
|
|
|
|
|
def _make_organization(**kwargs):
|
2022-11-29 09:41:56 +01:00
|
|
|
return OrganizationFactory(**kwargs, is_rbac_permissions_enabled=IS_RBAC_ENABLED)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
return _make_organization
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2022-11-29 09:41:56 +01:00
|
|
|
def make_user_for_organization(make_user):
|
|
|
|
|
def _make_user_for_organization(organization, role: typing.Optional[LegacyAccessControlRole] = None, **kwargs):
|
2022-08-26 13:46:50 +05:00
|
|
|
post_save.disconnect(listen_for_user_model_save, sender=User)
|
2022-11-29 09:41:56 +01:00
|
|
|
user = make_user(organization=organization, role=role, **kwargs)
|
2022-08-26 13:46:50 +05:00
|
|
|
post_save.disconnect(listen_for_user_model_save, sender=User)
|
2022-06-03 08:09:47 -06:00
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
return _make_user_for_organization
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_token_for_organization():
|
|
|
|
|
def _make_token_for_organization(organization):
|
|
|
|
|
return PluginAuthToken.create_auth_token(organization)
|
|
|
|
|
|
|
|
|
|
return _make_token_for_organization
|
|
|
|
|
|
|
|
|
|
|
add unique idx on user column in mobileapp authtoken table (#1482)
# Which issue(s) this PR fixes
Solves the (rare) issue where a user could potentially have > 1
mobileapp auth token, leading to 500 errors when trying to interact w/
the authtoken (ex. disconnect a mobile app from a user's profile):
```shell
2023-03-07 10:12:13 source=engine:app google_trace_id=e14bf933d634068a48caf093ce43c7f5/5550677047491218352 logger=django.request Internal Server Error: /api/internal/v1/users/U6WJ3BRLM1TR7/unlink_backend
Traceback (most recent call last):
File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/rest_framework/viewsets.py", line 125, in view
return self.dispatch(request, *args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "/etc/app/apps/api/views/user.py", line 453, in unlink_backend
backend.unlink_user(user)
File "/etc/app/apps/mobile_app/backend.py", line 34, in unlink_user
token = MobileAppAuthToken.objects.get(user=user)
File "/usr/local/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/django/db/models/query.py", line 439, in get
raise self.model.MultipleObjectsReturned(
apps.mobile_app.models.MobileAppAuthToken.MultipleObjectsReturned: get() returned more than one MobileAppAuthToken -- it returned 2!
```
## Checklist
- [x] Tests updated
- [ ] Documentation added (N/A)
- [x] `CHANGELOG.md` updated
2023-03-08 13:50:57 +01:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_mobile_app_verification_token_for_user():
|
|
|
|
|
def _make_mobile_app_verification_token_for_user(user, organization):
|
|
|
|
|
return MobileAppVerificationToken.create_auth_token(user, organization)
|
|
|
|
|
|
|
|
|
|
return _make_mobile_app_verification_token_for_user
|
|
|
|
|
|
|
|
|
|
|
Mobile app settings backend (#1571)
# What this PR does
Adds mobile app settings support to OnCall backend.
- Adds a new Django model `MobileAppUserSettings` to store push
notification settings
- Adds a new endpoint `/mobile_app/v1/user_settings` to fetch/update
settings from the mobile app
Some additional info on implementation: at first I wanted to extend the
messaging backend system to allow storing / retrieving per-user data and
implement mobile app settings based on those changes. After some thought
I decided not to extend the messaging backend system and have this as
functionality specific to the `mobile_app` Django app. Currently the
messaging backend system is used by the backend and plugin UI, but
mobile app settings are specific only to the mobile app and not
configurable in the plugin UI.
**tldr: wanted to extend messaging backend system, but decided not to do
that**
# Usage
## Get settings via API
`GET /mobile_app/v1/user_settings`
Example response:
```json
{
"default_notification_sound_name": "default_sound", # sound name without file extension
"default_notification_volume_type": "constant",
"default_notification_volume": 0.8,
"default_notification_volume_override": false,
"important_notification_sound_name": "default_sound_important", # sound name without file extension
"important_notification_volume_type": "constant",
"important_notification_volume": 0.8,
"important_notification_override_dnd": true
}
```
## Update settings via API
`PUT /mobile_app/v1/user_settings` - see example response above for
payload shape.
Note that sound names must be passed without file extension. When
sending push notifications, the backend will add `.mp3` to sound names
and pass it to push notification data for Android. For iOS, sound names
will be suffixed with `.aiff` to be used by APNS.
## Get settings from notification data for Android
All the settings from example response will be available in push
notification data (along with `orgId`, `alertGroupId`, `title`, etc.).
Fields `default_notification_volume`,
`default_notification_volume_override` and
`important_notification_volume` , `important_notification_override_dnd`
will be converted to strings due to FCM limitations.
Fields `default_notification_sound_name` and
`important_notification_sound_name` will be suffixed with `.mp3` in push
notification data.
## iOS limitations
While Android push notifications are handled purely on the mobile app
side, iOS notifications are sent via APNS which imposes some
limitations.
- Notification volume cannot be overridden for non-critical
notifications (so fields `default_notification_volume_override` and
`default_notification_volume` will be disregarded for iOS notifications)
- It's not possible to control volume type (i.e. "constant" vs
"intensifying") via APNS. A possible workaround is to have different
sound files for "constant" and "intensifying" and pass that as
`default_notification_sound_name` / `important_notification_sound_name`.
# Which issue(s) this PR fixes
Related to https://github.com/grafana/oncall-private/issues/1602
# Checklist
- [x] Tests updated
- [x] No changelog updates since the changes are not user-facing
2023-03-22 14:47:18 +00:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_mobile_app_auth_token_for_user():
|
|
|
|
|
def _make_mobile_app_auth_token_for_user(user, organization):
|
|
|
|
|
return MobileAppAuthToken.create_auth_token(user, organization)
|
|
|
|
|
|
|
|
|
|
return _make_mobile_app_auth_token_for_user
|
|
|
|
|
|
|
|
|
|
|
2023-07-18 10:31:11 -03:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_slack_token_for_user():
|
|
|
|
|
def _make_slack_token_for_user(user):
|
|
|
|
|
return SlackAuthToken.create_auth_token(organization=user.organization, user=user)
|
|
|
|
|
|
|
|
|
|
return _make_slack_token_for_user
|
|
|
|
|
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_public_api_token():
|
|
|
|
|
def _make_public_api_token(user, organization, name="test_api_token"):
|
|
|
|
|
return ApiAuthToken.create_auth_token(user, organization, name)
|
|
|
|
|
|
|
|
|
|
return _make_public_api_token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_user_auth_headers():
|
2022-11-21 16:26:00 +01:00
|
|
|
def _make_user_auth_headers(
|
|
|
|
|
user,
|
|
|
|
|
token,
|
|
|
|
|
grafana_token: typing.Optional[str] = None,
|
|
|
|
|
grafana_context_data: typing.Optional[typing.Dict] = None,
|
|
|
|
|
):
|
|
|
|
|
instance_context_headers = {"stack_id": user.organization.stack_id, "org_id": user.organization.org_id}
|
|
|
|
|
grafana_context_headers = {"UserId": user.user_id}
|
|
|
|
|
if grafana_token is not None:
|
|
|
|
|
instance_context_headers["grafana_token"] = grafana_token
|
|
|
|
|
if grafana_context_data is not None:
|
|
|
|
|
grafana_context_headers.update(grafana_context_data)
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
return {
|
2022-11-21 16:26:00 +01:00
|
|
|
"HTTP_X-Instance-Context": json.dumps(instance_context_headers),
|
|
|
|
|
"HTTP_X-Grafana-Context": json.dumps(grafana_context_headers),
|
2022-06-03 08:09:47 -06:00
|
|
|
"HTTP_AUTHORIZATION": f"{token}",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _make_user_auth_headers
|
|
|
|
|
|
|
|
|
|
|
2022-11-29 09:41:56 +01:00
|
|
|
RoleMapping = typing.Dict[LegacyAccessControlRole, typing.List[LegacyAccessControlCompatiblePermission]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_user_permission_role_mapping_from_frontend_plugin_json() -> RoleMapping:
|
|
|
|
|
"""
|
|
|
|
|
This is used to take the RBAC permission -> basic role grants on the frontend
|
|
|
|
|
and test that the RBAC grants work the same way against the backend in terms of authorization
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
class PluginJSONRoleDefinition(typing.TypedDict):
|
|
|
|
|
permissions: typing.List[GrafanaAPIPermission]
|
|
|
|
|
|
|
|
|
|
class PluginJSONRole(typing.TypedDict):
|
|
|
|
|
role: PluginJSONRoleDefinition
|
|
|
|
|
grants: typing.List[str]
|
|
|
|
|
|
|
|
|
|
class PluginJSON(typing.TypedDict):
|
|
|
|
|
roles: typing.List[PluginJSONRole]
|
|
|
|
|
|
|
|
|
|
with open("../grafana-plugin/src/plugin.json") as fp:
|
|
|
|
|
plugin_json: PluginJSON = json.load(fp)
|
|
|
|
|
|
|
|
|
|
role_mapping: RoleMapping = {
|
2023-10-19 14:39:08 -03:00
|
|
|
LegacyAccessControlRole.NONE: [],
|
2022-11-29 09:41:56 +01:00
|
|
|
LegacyAccessControlRole.VIEWER: [],
|
|
|
|
|
LegacyAccessControlRole.EDITOR: [],
|
|
|
|
|
LegacyAccessControlRole.ADMIN: [],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
all_permission_classes: typing.Dict[str, LegacyAccessControlCompatiblePermission] = {
|
|
|
|
|
getattr(RBACPermission.Permissions, attr).value: getattr(RBACPermission.Permissions, attr)
|
|
|
|
|
for attr in dir(RBACPermission.Permissions)
|
|
|
|
|
if not attr.startswith("_")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# we just care about getting the basic role grants, everything else can be ignored
|
|
|
|
|
for role in plugin_json["roles"]:
|
|
|
|
|
if grants := role["grants"]:
|
|
|
|
|
for permission in role["role"]["permissions"]:
|
|
|
|
|
# only concerned with grafana-oncall-app specific grants
|
|
|
|
|
# ignore things like plugins.app:access actions
|
|
|
|
|
action = permission["action"]
|
|
|
|
|
permission_class = None
|
|
|
|
|
|
|
|
|
|
if action.startswith(ACTION_PREFIX):
|
|
|
|
|
permission_class = all_permission_classes[action]
|
|
|
|
|
|
|
|
|
|
if permission_class:
|
|
|
|
|
for grant in grants:
|
|
|
|
|
try:
|
|
|
|
|
role = LegacyAccessControlRole[grant.upper()]
|
|
|
|
|
if role not in role_mapping[role]:
|
|
|
|
|
role_mapping[role].append(permission_class)
|
|
|
|
|
except KeyError:
|
|
|
|
|
# may come across grants like "Grafana Admin"
|
|
|
|
|
# which we can ignore
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
return role_mapping
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ROLE_PERMISSION_MAPPING = get_user_permission_role_mapping_from_frontend_plugin_json()
|
|
|
|
|
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_user():
|
2022-11-29 09:41:56 +01:00
|
|
|
def _make_user(role: typing.Optional[LegacyAccessControlRole] = None, **kwargs):
|
|
|
|
|
role = LegacyAccessControlRole.ADMIN if role is None else role
|
2023-05-02 08:19:34 -04:00
|
|
|
permissions = kwargs.pop("permissions", None)
|
|
|
|
|
if permissions is None:
|
|
|
|
|
permissions_to_grant = ROLE_PERMISSION_MAPPING[role] if IS_RBAC_ENABLED else []
|
|
|
|
|
permissions = [GrafanaAPIPermission(action=perm.value) for perm in permissions_to_grant]
|
|
|
|
|
return UserFactory(role=role, permissions=permissions, **kwargs)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
return _make_user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_organization_and_user(make_organization, make_user_for_organization):
|
2022-11-29 09:41:56 +01:00
|
|
|
def _make_organization_and_user(role: typing.Optional[LegacyAccessControlRole] = None):
|
2022-06-03 08:09:47 -06:00
|
|
|
organization = make_organization()
|
|
|
|
|
user = make_user_for_organization(organization=organization, role=role)
|
|
|
|
|
return organization, user
|
|
|
|
|
|
|
|
|
|
return _make_organization_and_user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_organization_and_user_with_slack_identities(
|
|
|
|
|
make_organization_with_slack_team_identity, make_user_with_slack_user_identity
|
|
|
|
|
):
|
2022-11-29 09:41:56 +01:00
|
|
|
def _make_organization_and_user_with_slack_identities(role: typing.Optional[LegacyAccessControlRole] = None):
|
2022-06-03 08:09:47 -06:00
|
|
|
organization, slack_team_identity = make_organization_with_slack_team_identity()
|
|
|
|
|
user, slack_user_identity = make_user_with_slack_user_identity(slack_team_identity, organization, role=role)
|
|
|
|
|
return organization, user, slack_team_identity, slack_user_identity
|
|
|
|
|
|
|
|
|
|
return _make_organization_and_user_with_slack_identities
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2022-11-29 09:41:56 +01:00
|
|
|
def make_user_with_slack_user_identity(make_user):
|
|
|
|
|
def _make_slack_user_identity_with_user(
|
|
|
|
|
slack_team_identity, organization, role: typing.Optional[LegacyAccessControlRole] = None, **kwargs
|
|
|
|
|
):
|
|
|
|
|
slack_user_identity = SlackUserIdentityFactory(slack_team_identity=slack_team_identity, **kwargs)
|
|
|
|
|
user = make_user(slack_user_identity=slack_user_identity, organization=organization, role=role)
|
2022-06-03 08:09:47 -06:00
|
|
|
return user, slack_user_identity
|
|
|
|
|
|
|
|
|
|
return _make_slack_user_identity_with_user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
2022-11-29 09:41:56 +01:00
|
|
|
def make_organization_with_slack_team_identity(make_slack_team_identity, make_organization):
|
2022-06-03 08:09:47 -06:00
|
|
|
def _make_slack_team_identity_with_organization(**kwargs):
|
|
|
|
|
slack_team_identity = make_slack_team_identity(**kwargs)
|
2022-11-29 09:41:56 +01:00
|
|
|
organization = make_organization(slack_team_identity=slack_team_identity)
|
2022-06-03 08:09:47 -06:00
|
|
|
return organization, slack_team_identity
|
|
|
|
|
|
|
|
|
|
return _make_slack_team_identity_with_organization
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_slack_team_identity():
|
|
|
|
|
def _make_slack_team_identity(**kwargs):
|
|
|
|
|
slack_team_identity = SlackTeamIdentityFactory(**kwargs)
|
|
|
|
|
return slack_team_identity
|
|
|
|
|
|
|
|
|
|
return _make_slack_team_identity
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_slack_user_identity():
|
|
|
|
|
def _make_slack_user_identity(**kwargs):
|
|
|
|
|
slack_user_identity = SlackUserIdentityFactory(**kwargs)
|
|
|
|
|
return slack_user_identity
|
|
|
|
|
|
|
|
|
|
return _make_slack_user_identity
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_slack_message():
|
2023-07-28 17:11:38 +02:00
|
|
|
def _make_slack_message(alert_group=None, organization=None, **kwargs):
|
|
|
|
|
organization = organization or alert_group.channel.organization
|
2022-06-03 08:09:47 -06:00
|
|
|
slack_message = SlackMessageFactory(
|
|
|
|
|
alert_group=alert_group,
|
|
|
|
|
organization=organization,
|
|
|
|
|
_slack_team_identity=organization.slack_team_identity,
|
|
|
|
|
**kwargs,
|
|
|
|
|
)
|
|
|
|
|
return slack_message
|
|
|
|
|
|
|
|
|
|
return _make_slack_message
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def client_with_user():
|
|
|
|
|
def _client_with_user(user):
|
|
|
|
|
"""The client with logged in user"""
|
|
|
|
|
|
|
|
|
|
client = APIClient()
|
|
|
|
|
client.force_login(user)
|
|
|
|
|
|
|
|
|
|
return client
|
|
|
|
|
|
|
|
|
|
return _client_with_user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_team():
|
|
|
|
|
def _make_team(organization, **kwargs):
|
|
|
|
|
team = TeamFactory(organization=organization, **kwargs)
|
|
|
|
|
return team
|
|
|
|
|
|
|
|
|
|
return _make_team
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_alert_receive_channel():
|
|
|
|
|
def _make_alert_receive_channel(organization, **kwargs):
|
|
|
|
|
if "integration" not in kwargs:
|
|
|
|
|
kwargs["integration"] = AlertReceiveChannel.INTEGRATION_GRAFANA
|
|
|
|
|
post_save.disconnect(listen_for_alertreceivechannel_model_save, sender=AlertReceiveChannel)
|
|
|
|
|
alert_receive_channel = AlertReceiveChannelFactory(organization=organization, **kwargs)
|
|
|
|
|
post_save.connect(listen_for_alertreceivechannel_model_save, sender=AlertReceiveChannel)
|
|
|
|
|
return alert_receive_channel
|
|
|
|
|
|
|
|
|
|
return _make_alert_receive_channel
|
|
|
|
|
|
|
|
|
|
|
2023-05-25 20:26:13 +02:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_alert_receive_channel_with_post_save_signal():
|
|
|
|
|
def _make_alert_receive_channel(organization, **kwargs):
|
|
|
|
|
if "integration" not in kwargs:
|
|
|
|
|
kwargs["integration"] = AlertReceiveChannel.INTEGRATION_GRAFANA
|
|
|
|
|
alert_receive_channel = AlertReceiveChannelFactory(organization=organization, **kwargs)
|
|
|
|
|
return alert_receive_channel
|
|
|
|
|
|
|
|
|
|
return _make_alert_receive_channel
|
|
|
|
|
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_channel_filter():
|
|
|
|
|
def _make_channel_filter(alert_receive_channel, filtering_term=None, **kwargs):
|
|
|
|
|
channel_filter = ChannelFilterFactory(
|
|
|
|
|
filtering_term=filtering_term,
|
|
|
|
|
alert_receive_channel=alert_receive_channel,
|
|
|
|
|
**kwargs,
|
|
|
|
|
)
|
|
|
|
|
return channel_filter
|
|
|
|
|
|
|
|
|
|
return _make_channel_filter
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_channel_filter_with_post_save():
|
|
|
|
|
def _make_channel_filter(alert_receive_channel, filtering_term=None, **kwargs):
|
|
|
|
|
channel_filter = ChannelFilterFactory(
|
|
|
|
|
filtering_term=filtering_term,
|
|
|
|
|
alert_receive_channel=alert_receive_channel,
|
|
|
|
|
**kwargs,
|
|
|
|
|
)
|
|
|
|
|
return channel_filter
|
|
|
|
|
|
|
|
|
|
return _make_channel_filter
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_escalation_chain():
|
|
|
|
|
def _make_escalation_chain(organization, **kwargs):
|
|
|
|
|
escalation_chain = EscalationChainFactory(organization=organization, **kwargs)
|
|
|
|
|
return escalation_chain
|
|
|
|
|
|
|
|
|
|
return _make_escalation_chain
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_escalation_policy():
|
|
|
|
|
def _make_escalation_policy(escalation_chain, escalation_policy_step, **kwargs):
|
|
|
|
|
escalation_policy = EscalationPolicyFactory(
|
|
|
|
|
escalation_chain=escalation_chain, step=escalation_policy_step, **kwargs
|
|
|
|
|
)
|
|
|
|
|
return escalation_policy
|
|
|
|
|
|
|
|
|
|
return _make_escalation_policy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_user_notification_policy():
|
|
|
|
|
def _make_user_notification_policy(user, step, **kwargs):
|
|
|
|
|
user_notification_policy = UserNotificationPolicyFactory(user=user, step=step, **kwargs)
|
|
|
|
|
return user_notification_policy
|
|
|
|
|
|
|
|
|
|
return _make_user_notification_policy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_user_notification_policy_log_record():
|
|
|
|
|
def _make_user_notification_policy_log_record(**kwargs):
|
|
|
|
|
post_save.disconnect(
|
|
|
|
|
listen_for_usernotificationpolicylogrecord_model_save, sender=UserNotificationPolicyLogRecord
|
|
|
|
|
)
|
|
|
|
|
user_notification_policy_log_record = UserNotificationPolicyLogRecordFactory(**kwargs)
|
|
|
|
|
post_save.connect(listen_for_usernotificationpolicylogrecord_model_save, sender=UserNotificationPolicyLogRecord)
|
|
|
|
|
|
|
|
|
|
return user_notification_policy_log_record
|
|
|
|
|
|
|
|
|
|
return _make_user_notification_policy_log_record
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_integration_escalation_chain_route_escalation_policy(
|
|
|
|
|
make_alert_receive_channel,
|
|
|
|
|
make_escalation_chain,
|
|
|
|
|
make_channel_filter,
|
|
|
|
|
make_escalation_policy,
|
|
|
|
|
):
|
|
|
|
|
def _make_integration_escalation_chain_route_escalation_policy(organization, escalation_policy_step):
|
|
|
|
|
alert_receive_channel = make_alert_receive_channel(organization)
|
|
|
|
|
escalation_chain = make_escalation_chain(organization)
|
|
|
|
|
default_channel_filter = make_channel_filter(
|
|
|
|
|
alert_receive_channel, escalation_chain=escalation_chain, is_default=True
|
|
|
|
|
)
|
|
|
|
|
escalation_policy = make_escalation_policy(escalation_chain, escalation_policy_step)
|
|
|
|
|
|
|
|
|
|
return alert_receive_channel, escalation_chain, default_channel_filter, escalation_policy
|
|
|
|
|
|
|
|
|
|
return _make_integration_escalation_chain_route_escalation_policy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_invitation():
|
|
|
|
|
def _make_invitation(alert_group, author, invitee, **kwargs):
|
|
|
|
|
invitation = InvitationFactory(alert_group=alert_group, author=author, invitee=invitee, **kwargs)
|
|
|
|
|
return invitation
|
|
|
|
|
|
|
|
|
|
return _make_invitation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_schedule():
|
|
|
|
|
def _make_schedule(organization, schedule_class, **kwargs):
|
|
|
|
|
factory = OnCallScheduleFactory.get_factory_for_class(schedule_class)
|
|
|
|
|
schedule = factory(organization=organization, **kwargs)
|
|
|
|
|
return schedule
|
|
|
|
|
|
|
|
|
|
return _make_schedule
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_on_call_shift():
|
|
|
|
|
def _make_on_call_shift(organization, shift_type, **kwargs):
|
|
|
|
|
on_call_shift = CustomOnCallShiftFactory(organization=organization, type=shift_type, **kwargs)
|
|
|
|
|
return on_call_shift
|
|
|
|
|
|
|
|
|
|
return _make_on_call_shift
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_alert_group():
|
|
|
|
|
def _make_alert_group(alert_receive_channel, **kwargs):
|
|
|
|
|
alert_group = AlertGroupFactory(channel=alert_receive_channel, **kwargs)
|
|
|
|
|
return alert_group
|
|
|
|
|
|
|
|
|
|
return _make_alert_group
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_alert_group_log_record():
|
|
|
|
|
def _make_alert_group_log_record(alert_group, type, author, **kwargs):
|
|
|
|
|
post_save.disconnect(listen_for_alertgrouplogrecord, sender=AlertGroupLogRecord)
|
|
|
|
|
log_record = AlertGroupLogRecordFactory(alert_group=alert_group, type=type, author=author, **kwargs)
|
|
|
|
|
post_save.connect(listen_for_alertgrouplogrecord, sender=AlertGroupLogRecord)
|
|
|
|
|
return log_record
|
|
|
|
|
|
|
|
|
|
return _make_alert_group_log_record
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_resolution_note():
|
|
|
|
|
def _make_resolution_note(alert_group, source=ResolutionNote.Source.WEB, author=None, **kwargs):
|
|
|
|
|
resolution_note = ResolutionNoteFactory(alert_group=alert_group, source=source, author=author, **kwargs)
|
|
|
|
|
return resolution_note
|
|
|
|
|
|
|
|
|
|
return _make_resolution_note
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_resolution_note_slack_message():
|
|
|
|
|
def _make_resolution_note_slack_message(alert_group, user, added_by_user, **kwargs):
|
|
|
|
|
return ResolutionNoteSlackMessageFactory(
|
|
|
|
|
alert_group=alert_group, user=user, added_by_user=added_by_user, **kwargs
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return _make_resolution_note_slack_message
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_alert():
|
|
|
|
|
def _make_alert(alert_group, raw_request_data, **kwargs):
|
|
|
|
|
alert = AlertFactory(group=alert_group, raw_request_data=raw_request_data, **kwargs)
|
|
|
|
|
return alert
|
|
|
|
|
|
|
|
|
|
return _make_alert
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_alert_with_custom_create_method():
|
|
|
|
|
def _make_alert_with_custom_create_method(
|
|
|
|
|
title,
|
|
|
|
|
message,
|
|
|
|
|
image_url,
|
|
|
|
|
link_to_upstream_details,
|
|
|
|
|
alert_receive_channel,
|
|
|
|
|
integration_unique_data,
|
|
|
|
|
raw_request_data,
|
|
|
|
|
**kwargs,
|
|
|
|
|
):
|
|
|
|
|
alert = Alert.create(
|
|
|
|
|
title,
|
|
|
|
|
message,
|
|
|
|
|
image_url,
|
|
|
|
|
link_to_upstream_details,
|
|
|
|
|
alert_receive_channel,
|
|
|
|
|
integration_unique_data,
|
|
|
|
|
raw_request_data,
|
|
|
|
|
**kwargs,
|
|
|
|
|
)
|
|
|
|
|
return alert
|
|
|
|
|
|
|
|
|
|
return _make_alert_with_custom_create_method
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_custom_action():
|
|
|
|
|
def _make_custom_action(organization, **kwargs):
|
|
|
|
|
custom_action = CustomActionFactory(organization=organization, **kwargs)
|
|
|
|
|
return custom_action
|
|
|
|
|
|
|
|
|
|
return _make_custom_action
|
|
|
|
|
|
|
|
|
|
|
2023-03-09 16:39:25 -03:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_custom_webhook():
|
|
|
|
|
def _make_custom_webhook(organization, **kwargs):
|
|
|
|
|
custom_webhook = CustomWebhookFactory(organization=organization, **kwargs)
|
|
|
|
|
return custom_webhook
|
|
|
|
|
|
|
|
|
|
return _make_custom_webhook
|
|
|
|
|
|
|
|
|
|
|
2023-03-21 10:43:37 -03:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_webhook_response():
|
|
|
|
|
def _make_webhook_response(**kwargs):
|
|
|
|
|
webhook_response = WebhookResponseFactory(**kwargs)
|
|
|
|
|
return webhook_response
|
|
|
|
|
|
|
|
|
|
return _make_webhook_response
|
|
|
|
|
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_slack_user_group():
|
|
|
|
|
def _make_slack_user_group(slack_team_identity, **kwargs):
|
|
|
|
|
slack_user_group = SlackUserGroupFactory(slack_team_identity=slack_team_identity, **kwargs)
|
|
|
|
|
return slack_user_group
|
|
|
|
|
|
|
|
|
|
return _make_slack_user_group
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_slack_channel():
|
|
|
|
|
def _make_slack_channel(slack_team_identity, **kwargs):
|
|
|
|
|
schedule = SlackChannelFactory(slack_team_identity=slack_team_identity, **kwargs)
|
|
|
|
|
return schedule
|
|
|
|
|
|
|
|
|
|
return _make_slack_channel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def mock_start_disable_maintenance_task(monkeypatch):
|
|
|
|
|
def mocked_start_disable_maintenance_task(*args, **kwargs):
|
|
|
|
|
return uuid.uuid4()
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(MaintainableObject, "start_disable_maintenance_task", mocked_start_disable_maintenance_task)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_organization_and_user_with_plugin_token(make_organization_and_user, make_token_for_organization):
|
2022-11-29 09:41:56 +01:00
|
|
|
def _make_organization_and_user_with_plugin_token(role: typing.Optional[LegacyAccessControlRole] = None):
|
|
|
|
|
organization, user = make_organization_and_user(role)
|
2022-06-03 08:09:47 -06:00
|
|
|
_, token = make_token_for_organization(organization)
|
|
|
|
|
return organization, user, token
|
|
|
|
|
|
|
|
|
|
return _make_organization_and_user_with_plugin_token
|
|
|
|
|
|
|
|
|
|
|
add unique idx on user column in mobileapp authtoken table (#1482)
# Which issue(s) this PR fixes
Solves the (rare) issue where a user could potentially have > 1
mobileapp auth token, leading to 500 errors when trying to interact w/
the authtoken (ex. disconnect a mobile app from a user's profile):
```shell
2023-03-07 10:12:13 source=engine:app google_trace_id=e14bf933d634068a48caf093ce43c7f5/5550677047491218352 logger=django.request Internal Server Error: /api/internal/v1/users/U6WJ3BRLM1TR7/unlink_backend
Traceback (most recent call last):
File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
return view_func(*args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/rest_framework/viewsets.py", line 125, in view
return self.dispatch(request, *args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 509, in dispatch
response = self.handle_exception(exc)
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 469, in handle_exception
self.raise_uncaught_exception(exc)
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
raise exc
File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 506, in dispatch
response = handler(request, *args, **kwargs)
File "/etc/app/apps/api/views/user.py", line 453, in unlink_backend
backend.unlink_user(user)
File "/etc/app/apps/mobile_app/backend.py", line 34, in unlink_user
token = MobileAppAuthToken.objects.get(user=user)
File "/usr/local/lib/python3.9/site-packages/django/db/models/manager.py", line 85, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/usr/local/lib/python3.9/site-packages/django/db/models/query.py", line 439, in get
raise self.model.MultipleObjectsReturned(
apps.mobile_app.models.MobileAppAuthToken.MultipleObjectsReturned: get() returned more than one MobileAppAuthToken -- it returned 2!
```
## Checklist
- [x] Tests updated
- [ ] Documentation added (N/A)
- [x] `CHANGELOG.md` updated
2023-03-08 13:50:57 +01:00
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_organization_and_user_with_mobile_app_verification_token(
|
|
|
|
|
make_organization_and_user, make_mobile_app_verification_token_for_user
|
|
|
|
|
):
|
|
|
|
|
def _make_organization_and_user_with_mobile_app_verification_token(
|
|
|
|
|
role: typing.Optional[LegacyAccessControlRole] = None,
|
|
|
|
|
):
|
|
|
|
|
organization, user = make_organization_and_user(role)
|
|
|
|
|
_, token = make_mobile_app_verification_token_for_user(user, organization)
|
|
|
|
|
return organization, user, token
|
|
|
|
|
|
|
|
|
|
return _make_organization_and_user_with_mobile_app_verification_token
|
|
|
|
|
|
|
|
|
|
|
Mobile app settings backend (#1571)
# What this PR does
Adds mobile app settings support to OnCall backend.
- Adds a new Django model `MobileAppUserSettings` to store push
notification settings
- Adds a new endpoint `/mobile_app/v1/user_settings` to fetch/update
settings from the mobile app
Some additional info on implementation: at first I wanted to extend the
messaging backend system to allow storing / retrieving per-user data and
implement mobile app settings based on those changes. After some thought
I decided not to extend the messaging backend system and have this as
functionality specific to the `mobile_app` Django app. Currently the
messaging backend system is used by the backend and plugin UI, but
mobile app settings are specific only to the mobile app and not
configurable in the plugin UI.
**tldr: wanted to extend messaging backend system, but decided not to do
that**
# Usage
## Get settings via API
`GET /mobile_app/v1/user_settings`
Example response:
```json
{
"default_notification_sound_name": "default_sound", # sound name without file extension
"default_notification_volume_type": "constant",
"default_notification_volume": 0.8,
"default_notification_volume_override": false,
"important_notification_sound_name": "default_sound_important", # sound name without file extension
"important_notification_volume_type": "constant",
"important_notification_volume": 0.8,
"important_notification_override_dnd": true
}
```
## Update settings via API
`PUT /mobile_app/v1/user_settings` - see example response above for
payload shape.
Note that sound names must be passed without file extension. When
sending push notifications, the backend will add `.mp3` to sound names
and pass it to push notification data for Android. For iOS, sound names
will be suffixed with `.aiff` to be used by APNS.
## Get settings from notification data for Android
All the settings from example response will be available in push
notification data (along with `orgId`, `alertGroupId`, `title`, etc.).
Fields `default_notification_volume`,
`default_notification_volume_override` and
`important_notification_volume` , `important_notification_override_dnd`
will be converted to strings due to FCM limitations.
Fields `default_notification_sound_name` and
`important_notification_sound_name` will be suffixed with `.mp3` in push
notification data.
## iOS limitations
While Android push notifications are handled purely on the mobile app
side, iOS notifications are sent via APNS which imposes some
limitations.
- Notification volume cannot be overridden for non-critical
notifications (so fields `default_notification_volume_override` and
`default_notification_volume` will be disregarded for iOS notifications)
- It's not possible to control volume type (i.e. "constant" vs
"intensifying") via APNS. A possible workaround is to have different
sound files for "constant" and "intensifying" and pass that as
`default_notification_sound_name` / `important_notification_sound_name`.
# Which issue(s) this PR fixes
Related to https://github.com/grafana/oncall-private/issues/1602
# Checklist
- [x] Tests updated
- [x] No changelog updates since the changes are not user-facing
2023-03-22 14:47:18 +00:00
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_organization_and_user_with_mobile_app_auth_token(
|
|
|
|
|
make_organization_and_user, make_mobile_app_auth_token_for_user
|
|
|
|
|
):
|
|
|
|
|
def _make_organization_and_user_with_mobile_app_auth_token(
|
|
|
|
|
role: typing.Optional[LegacyAccessControlRole] = None,
|
|
|
|
|
):
|
|
|
|
|
organization, user = make_organization_and_user(role)
|
|
|
|
|
_, token = make_mobile_app_auth_token_for_user(user, organization)
|
|
|
|
|
return organization, user, token
|
|
|
|
|
|
|
|
|
|
return _make_organization_and_user_with_mobile_app_auth_token
|
|
|
|
|
|
|
|
|
|
|
2022-06-03 08:09:47 -06:00
|
|
|
@pytest.fixture()
|
|
|
|
|
def mock_send_user_notification_signal(monkeypatch):
|
|
|
|
|
def mocked_send_signal(*args, **kwargs):
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(user_notification_action_triggered_signal, "send", mocked_send_signal)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_telegram_user_connector():
|
|
|
|
|
def _make_telegram_user_connector(user, **kwargs):
|
|
|
|
|
return TelegramToUserConnectorFactory(user=user, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_telegram_user_connector
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_telegram_channel():
|
|
|
|
|
def _make_telegram_channel(organization, is_default_channel=False):
|
|
|
|
|
return TelegramChannelFactory(organization=organization, is_default_channel=is_default_channel)
|
|
|
|
|
|
|
|
|
|
return _make_telegram_channel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_telegram_verification_code():
|
|
|
|
|
def _make_telegram_verification_code(user, **kwargs):
|
|
|
|
|
return TelegramVerificationCodeFactory(user=user, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_telegram_verification_code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_telegram_channel_verification_code():
|
|
|
|
|
def _make_telegram_channel_verification_code(organization, author, **kwargs):
|
|
|
|
|
return TelegramChannelVerificationCodeFactory(organization=organization, author=author, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_telegram_channel_verification_code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_telegram_message():
|
|
|
|
|
def _make_telegram_message(alert_group, message_type, **kwargs):
|
|
|
|
|
return TelegramMessageFactory(alert_group=alert_group, message_type=message_type, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_telegram_message
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2023-05-24 14:27:48 +08:00
|
|
|
def make_phone_call_record():
|
|
|
|
|
def _make_phone_call_record(receiver, **kwargs):
|
|
|
|
|
return PhoneCallRecordFactory(receiver=receiver, **kwargs)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-05-24 14:27:48 +08:00
|
|
|
return _make_phone_call_record
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
2023-05-24 14:27:48 +08:00
|
|
|
def make_sms_record():
|
|
|
|
|
def _make_sms_record(receiver, **kwargs):
|
|
|
|
|
return SMSRecordFactory(receiver=receiver, **kwargs)
|
2022-06-03 08:09:47 -06:00
|
|
|
|
2023-05-24 14:27:48 +08:00
|
|
|
return _make_sms_record
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
2022-10-19 12:32:56 +01:00
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_email_message():
|
|
|
|
|
def _make_email_message(receiver, **kwargs):
|
|
|
|
|
return EmailMessageFactory(receiver=receiver, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_email_message
|
2022-06-03 08:09:47 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_live_setting():
|
|
|
|
|
def _make_live_setting(name, **kwargs):
|
|
|
|
|
return LiveSettingFactory(name=name, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_live_setting
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_integration_heartbeat():
|
|
|
|
|
def _make_integration_heartbeat(alert_receive_channel, timeout_seconds=60, last_heartbeat_time=None, **kwargs):
|
|
|
|
|
return IntegrationHeartBeatFactory(
|
|
|
|
|
alert_receive_channel=alert_receive_channel,
|
|
|
|
|
timeout_seconds=timeout_seconds,
|
|
|
|
|
last_heartbeat_time=last_heartbeat_time,
|
|
|
|
|
**kwargs,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return _make_integration_heartbeat
|
|
|
|
|
|
|
|
|
|
|
2023-05-12 11:44:09 -04:00
|
|
|
@pytest.fixture
|
2023-01-19 11:15:56 +00:00
|
|
|
def reload_urls(settings):
|
|
|
|
|
"""
|
|
|
|
|
Reloads Django URLs, especially useful when testing conditionally registered URLs
|
|
|
|
|
"""
|
|
|
|
|
|
2023-05-12 11:44:09 -04:00
|
|
|
def _reload_urls():
|
|
|
|
|
clear_url_caches()
|
|
|
|
|
urlconf = settings.ROOT_URLCONF
|
|
|
|
|
if urlconf in sys.modules:
|
|
|
|
|
reload(sys.modules[urlconf])
|
|
|
|
|
else:
|
|
|
|
|
import_module(urlconf)
|
|
|
|
|
|
|
|
|
|
return _reload_urls
|
2022-10-27 15:40:46 -06:00
|
|
|
|
|
|
|
|
|
2023-01-19 11:15:56 +00:00
|
|
|
@pytest.fixture()
|
2023-05-12 11:44:09 -04:00
|
|
|
def load_slack_urls(settings, reload_urls):
|
2023-01-19 11:15:56 +00:00
|
|
|
settings.FEATURE_SLACK_INTEGRATION_ENABLED = True
|
2023-05-12 11:44:09 -04:00
|
|
|
reload_urls()
|
2023-01-19 11:15:56 +00:00
|
|
|
|
|
|
|
|
|
2022-10-27 15:40:46 -06:00
|
|
|
@pytest.fixture
|
|
|
|
|
def make_region():
|
|
|
|
|
def _make_region(**kwargs):
|
|
|
|
|
region = RegionFactory(**kwargs)
|
|
|
|
|
return region
|
|
|
|
|
|
|
|
|
|
return _make_region
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_organization_and_region(make_organization, make_region):
|
|
|
|
|
def _make_organization_and_region():
|
|
|
|
|
organization = make_organization()
|
|
|
|
|
region = make_region()
|
|
|
|
|
organization.migration_destination = region
|
|
|
|
|
return organization, region
|
|
|
|
|
|
|
|
|
|
return _make_organization_and_region
|
2023-01-05 12:42:55 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def make_organization_and_user_with_token(make_organization_and_user, make_public_api_token):
|
|
|
|
|
def _make_organization_and_user_with_token():
|
|
|
|
|
organization, user = make_organization_and_user()
|
|
|
|
|
_, token = make_public_api_token(user, organization)
|
|
|
|
|
return organization, user, token
|
|
|
|
|
|
|
|
|
|
return _make_organization_and_user_with_token
|
2023-07-21 21:35:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_shift_swap_request():
|
|
|
|
|
def _make_shift_swap_request(schedule, beneficiary, **kwargs):
|
|
|
|
|
return ShiftSwapRequestFactory(schedule=schedule, beneficiary=beneficiary, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_shift_swap_request
|
2023-07-28 17:11:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def shift_swap_request_setup(
|
|
|
|
|
make_schedule, make_organization_and_user, make_user_for_organization, make_shift_swap_request
|
|
|
|
|
):
|
|
|
|
|
def _shift_swap_request_setup(**kwargs):
|
|
|
|
|
organization, beneficiary = make_organization_and_user()
|
|
|
|
|
benefactor = make_user_for_organization(organization)
|
|
|
|
|
|
|
|
|
|
schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb)
|
|
|
|
|
tomorrow = timezone.now() + datetime.timedelta(days=1)
|
|
|
|
|
two_days_from_now = tomorrow + datetime.timedelta(days=1)
|
|
|
|
|
|
|
|
|
|
ssr = make_shift_swap_request(schedule, beneficiary, swap_start=tomorrow, swap_end=two_days_from_now, **kwargs)
|
|
|
|
|
|
|
|
|
|
return ssr, beneficiary, benefactor
|
|
|
|
|
|
|
|
|
|
return _shift_swap_request_setup
|
2023-09-27 07:22:52 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def webhook_preset_api_setup():
|
|
|
|
|
WebhookPresetOptions.WEBHOOK_PRESETS = {TEST_WEBHOOK_PRESET_ID: TestWebhookPreset()}
|
|
|
|
|
WebhookPresetOptions.WEBHOOK_PRESET_CHOICES = [
|
|
|
|
|
preset.metadata for preset in WebhookPresetOptions.WEBHOOK_PRESETS.values()
|
|
|
|
|
]
|
2023-10-20 09:30:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_label_key():
|
2023-11-27 17:53:54 +00:00
|
|
|
def _make_label_key(organization, key_id=None, key_name=None, **kwargs):
|
|
|
|
|
if key_id is not None:
|
|
|
|
|
kwargs["id"] = key_id
|
|
|
|
|
|
|
|
|
|
if key_name is not None:
|
|
|
|
|
kwargs["name"] = key_name
|
|
|
|
|
|
2023-10-20 09:30:11 +02:00
|
|
|
return LabelKeyFactory(organization=organization, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_label_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_label_value():
|
2023-11-27 17:53:54 +00:00
|
|
|
def _make_label_value(key, value_id=None, value_name=None, **kwargs):
|
|
|
|
|
if value_id is not None:
|
|
|
|
|
kwargs["id"] = value_id
|
|
|
|
|
|
|
|
|
|
if value_name is not None:
|
|
|
|
|
kwargs["name"] = value_name
|
|
|
|
|
|
2023-10-20 09:30:11 +02:00
|
|
|
return LabelValueFactory(key=key, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_label_value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_label_key_and_value(make_label_key, make_label_value):
|
2023-11-27 17:53:54 +00:00
|
|
|
def _make_label_key_and_value(organization, key_id=None, key_name=None, value_id=None, value_name=None):
|
|
|
|
|
key = make_label_key(organization=organization, key_id=key_id, key_name=key_name)
|
|
|
|
|
value = make_label_value(key=key, value_id=value_id, value_name=value_name)
|
2023-10-20 09:30:11 +02:00
|
|
|
return key, value
|
|
|
|
|
|
|
|
|
|
return _make_label_key_and_value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_integration_label_association(make_label_key_and_value):
|
2023-11-27 17:53:54 +00:00
|
|
|
def _make_integration_label_association(
|
|
|
|
|
organization, alert_receive_channel, key_id=None, key_name=None, value_id=None, value_name=None, **kwargs
|
|
|
|
|
):
|
|
|
|
|
key, value = make_label_key_and_value(
|
|
|
|
|
organization, key_id=key_id, key_name=key_name, value_id=value_id, value_name=value_name
|
|
|
|
|
)
|
2023-10-20 09:30:11 +02:00
|
|
|
return AlertReceiveChannelAssociatedLabelFactory(
|
|
|
|
|
alert_receive_channel=alert_receive_channel, organization=organization, key=key, value=value, **kwargs
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return _make_integration_label_association
|
2023-11-06 10:31:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_alert_group_label_association():
|
|
|
|
|
def _make_alert_group_label_association(organization, alert_group, **kwargs):
|
|
|
|
|
return AlertGroupAssociatedLabelFactory(alert_group=alert_group, organization=organization, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_alert_group_label_association
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_webhook_label_association(make_label_key_and_value):
|
|
|
|
|
def _make_integration_label_association(organization, webhook, **kwargs):
|
|
|
|
|
key, value = make_label_key_and_value(organization)
|
|
|
|
|
return WebhookAssociatedLabelFactory(webhook=webhook, organization=organization, key=key, value=value, **kwargs)
|
|
|
|
|
|
|
|
|
|
return _make_integration_label_association
|