Skip to content

Commit

Permalink
✨ Add ConfigurationStep for Subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
swrichards committed Dec 11, 2024
1 parent d81ca6c commit 34b525c
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 9 deletions.
30 changes: 24 additions & 6 deletions docs/setup_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@ Setup configuration
Loading notification configuration from a YAML file
***************************************************

This library provides a ``ConfigurationStep``
This library provides two ``ConfigurationStep``s
(from the library ``django-setup-configuration``, see the
`documentation <https://github.com/maykinmedia/django-setup-configuration>`_
for more information on how to run ``setup_configuration``)
to configure the notification configuration.
for more information on how to run ``setup_configuration``): one to configure the
service and retry settings, another to configure notification endpoint subscriptions.

To add this step to your configuration steps, add ``django_setup_configuration`` to ``INSTALLED_APPS`` and add the following setting:
To add these steps to your configuration steps, add ``django_setup_configuration``
to ``INSTALLED_APPS`` and add the following settings:

.. code:: python
SETUP_CONFIGURATION_STEPS = [
...
# Note the order: NotificationSubscriptionConfigurationStep expects a notification service
# to have been configured by NotificationConfigurationStep
"notifications_api_common.contrib.setup_configuration.steps.NotificationConfigurationStep"
"notifications_api_common.contrib.setup_configuration.steps.NotificationSubscriptionConfigurationStep"
...
]
The YAML file that is passed to ``setup_configuration`` must set the
``notifications_config_enable`` flag to ``true`` to enable the step. All fields under ``notifications_config`` are optional.
The YAML file that is passed to ``setup_configuration`` must set the appropriate
flag and fields for both steps:

Example file:

Expand All @@ -34,7 +38,21 @@ Example file:
notification_delivery_retry_backoff: 2
notification_delivery_retry_backoff_max: 3
notifications_subscriptions_config_enable: true
notifications_subscriptions_config:
items:
- identifier: my-subscription
callback_url: http://my/callback
client_id: the-client
secret: supersecret
uuid: 0f616bfd-aacc-4d85-a140-2af17a56217b
channels:
- Foo
- Bar
If the ``notifications_api_service_identifier`` is specified, it might also be useful
to use the `ServiceConfigurationStep <https://zgw-consumers.readthedocs.io/en/latest/setup_config.html>`_
from ``zgw-consumers``.

Note that the ``uuid`` field in your subscriptions config must point to an existing
``Abonnement`` in the Open Notificaties service configured through ``notifications_config``.
29 changes: 28 additions & 1 deletion notifications_api_common/contrib/setup_configuration/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django_setup_configuration.models import ConfigurationModel, DjangoModelRef
from pydantic import UUID4, Field

from notifications_api_common.models import NotificationsConfig
from notifications_api_common.models import NotificationsConfig, Subscription


class NotificationConfigurationModel(ConfigurationModel):
Expand All @@ -16,3 +17,29 @@ class Meta:
"notification_delivery_retry_backoff_max",
]
}


class SubscriptionConfigurationItem(ConfigurationModel):
uuid: UUID4 = Field(
description="The UUID for this subscription. Must match the UUID of the corresponding `Abonnement` in Open Notificaties."
)

channels: list[str] = DjangoModelRef(
Subscription,
"channels",
default_factory=list,
)

class Meta:
django_model_refs = {
Subscription: [
"identifier",
"callback_url",
"client_id",
"secret",
]
}


class SubscriptionConfigurationModel(ConfigurationModel):
items: list[SubscriptionConfigurationItem]
56 changes: 54 additions & 2 deletions notifications_api_common/contrib/setup_configuration/steps.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import logging

from django_setup_configuration.configuration import BaseConfigurationStep
from django_setup_configuration.exceptions import ConfigurationRunFailed
from zgw_consumers.models import Service

from notifications_api_common.models import NotificationsConfig
from notifications_api_common.models import NotificationsConfig, Subscription

from .models import NotificationConfigurationModel, SubscriptionConfigurationModel

from .models import NotificationConfigurationModel
logger = logging.getLogger(__name__)


def get_service(slug: str) -> Service:
Expand Down Expand Up @@ -49,3 +54,50 @@ def execute(self, model: NotificationConfigurationModel):
)

config.save()


class NotificationSubscriptionConfigurationStep(
BaseConfigurationStep[SubscriptionConfigurationModel]
):
"""
Configure settings for Notificaties API Subscriptions
"""

verbose_name = "Configuration for Notificaties API Subscriptions"
config_model = SubscriptionConfigurationModel
namespace = "notifications_subscriptions_config"
enable_setting = "notifications_subscriptions_config_enable"

def execute(self, model: SubscriptionConfigurationModel):
config = NotificationsConfig.get_solo()

if not (notifications_api := config.notifications_api_service):
raise ConfigurationRunFailed(
"No Notifications API Service configured. Please ensure you've first "
f"run {NotificationConfigurationStep.__name__}"
)

if len(model.items) == 0:
raise ConfigurationRunFailed("You must configure at least one subscription")

for item in model.items:
detail_url = (
notifications_api.api_root.rstrip("/") + f"/abonnement/{item.uuid!s}"
)
subscription, created = Subscription.objects.update_or_create(
identifier=item.identifier,
defaults={
"client_id": item.client_id,
"secret": item.secret,
"channels": item.channels,
"callback_url": item.callback_url,
"_subscription": detail_url,
},
)

logger.debug(
"%s subscription with identifier='%s' and pk='%s'",
"Created" if created else "Updated",
subscription.identifier,
subscription.pk,
)
19 changes: 19 additions & 0 deletions tests/files/setup_config_subscriptions_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
notifications_subscriptions_config_enable: true
notifications_subscriptions_config:
items:
- identifier: my-subscription
callback_url: http://my/callback
client_id: the-client
secret: supersecret
uuid: 0f616bfd-aacc-4d85-a140-2af17a56217b
channels:
- Foo
- Bar
- identifier: my-other-subscription
callback_url: http://my/other-callback
client_id: the-client
secret: supersecret
uuid: a33cf110-06b6-454e-b7e9-5139c172ca9a
channels:
- Fuh
- Bahr
143 changes: 143 additions & 0 deletions tests/test_subscription_configuration_steps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import pytest
from django_setup_configuration.exceptions import ConfigurationRunFailed
from django_setup_configuration.test_utils import execute_single_step
from zgw_consumers.test.factories import ServiceFactory

from notifications_api_common.contrib.setup_configuration.steps import (
NotificationSubscriptionConfigurationStep,
)
from notifications_api_common.models import NotificationsConfig, Subscription

CONFIG_FILE_PATH = "tests/files/setup_config_subscriptions_config.yaml"


@pytest.mark.django_db
def test_execute_configuration_step_success():
config = NotificationsConfig.get_solo()
config.notifications_api_service = ServiceFactory.create(
slug="notifs-api", api_root="http://notificaties.local/api/v1/"
)
config.save()

execute_single_step(
NotificationSubscriptionConfigurationStep, yaml_source=CONFIG_FILE_PATH
)

value = list(
Subscription.objects.values_list(
"identifier",
"client_id",
"secret",
"channels",
"callback_url",
"_subscription",
)
)
expected = [
(
"my-subscription",
"the-client",
"supersecret",
["Foo", "Bar"],
"http://my/callback",
"http://notificaties.local/api/v1/abonnement/0f616bfd-aacc-4d85-a140-2af17a56217b",
),
(
"my-other-subscription",
"the-client",
"supersecret",
["Fuh", "Bahr"],
"http://my/other-callback",
"http://notificaties.local/api/v1/abonnement/a33cf110-06b6-454e-b7e9-5139c172ca9a",
),
]

assert value == expected


@pytest.mark.django_db
def test_existing_items_are_updated():
config = NotificationsConfig.get_solo()
config.notifications_api_service = ServiceFactory.create(
slug="notifs-api", api_root="http://notificaties.local/api/v1/"
)
config.save()

Subscription.objects.create(
identifier="my-subscription",
callback_url="http://my/initial-callback",
client_id="the-old-client",
secret="secretsuper",
channels=["Fuzz"],
)

execute_single_step(
NotificationSubscriptionConfigurationStep, yaml_source=CONFIG_FILE_PATH
)

value = list(
Subscription.objects.values_list(
"identifier",
"client_id",
"secret",
"channels",
"callback_url",
"_subscription",
)
)
expected = [
# Updated
(
"my-subscription",
"the-client",
"supersecret",
["Foo", "Bar"],
"http://my/callback",
"http://notificaties.local/api/v1/abonnement/0f616bfd-aacc-4d85-a140-2af17a56217b",
),
# Created
(
"my-other-subscription",
"the-client",
"supersecret",
["Fuh", "Bahr"],
"http://my/other-callback",
"http://notificaties.local/api/v1/abonnement/a33cf110-06b6-454e-b7e9-5139c172ca9a",
),
]

assert value == expected


@pytest.mark.django_db
def test_missing_notifications_service_raises():
config = NotificationsConfig.get_solo()
config.notifications_api_service = None
config.save()

with pytest.raises(ConfigurationRunFailed) as excinfo:
execute_single_step(
NotificationSubscriptionConfigurationStep, yaml_source=CONFIG_FILE_PATH
)

assert (
str(excinfo.value)
== "No Notifications API Service configured. Please ensure you've first run NotificationConfigurationStep"
)


@pytest.mark.django_db
def test_no_items_raises():
config = NotificationsConfig.get_solo()
config.notifications_api_service = ServiceFactory.create(
slug="notifs-api", api_root="http://notificaties.local/api/v1/"
)
config.save()

with pytest.raises(ConfigurationRunFailed) as excinfo:
execute_single_step(
NotificationSubscriptionConfigurationStep,
object_source={"notifications_subscriptions_config": {"items": []}},
)

assert str(excinfo.value) == "You must configure at least one subscription"

0 comments on commit 34b525c

Please sign in to comment.