mirror of
https://github.com/Kozea/Radicale.git
synced 2025-08-07 18:30:54 +00:00
Merge pull request #1835 from pbiering/test-hooks
Test hooks for rabbitmq+email
This commit is contained in:
commit
d92e06e850
8 changed files with 289 additions and 2 deletions
|
@ -12,6 +12,7 @@
|
|||
* Fix: catch case where getpwuid is not returning a username
|
||||
* Fix: add support for query without comp-type
|
||||
* Fix: expanded event with dates are missing VALUE=DATE
|
||||
* Add: [hook] dryrun: option to disable real hook action for testing, add tests for email+rabbitmq
|
||||
|
||||
## 3.5.4
|
||||
* Improve: item filter enhanced for 3rd level supporting VALARM and honoring TRIGGER (offset or absolute)
|
||||
|
|
|
@ -1529,6 +1529,14 @@ Available types:
|
|||
|
||||
Default: `none`
|
||||
|
||||
##### dryrun
|
||||
|
||||
_(> 3.5.4)_
|
||||
|
||||
Dry-Run (do not really trigger hook action)
|
||||
|
||||
Default: `False`
|
||||
|
||||
##### rabbitmq_endpoint
|
||||
|
||||
_(>= 3.2.0)_
|
||||
|
|
7
config
7
config
|
@ -313,9 +313,16 @@
|
|||
# Hook types
|
||||
# Value: none | rabbitmq | email
|
||||
#type = none
|
||||
|
||||
# dry-run (do not really trigger hook action)
|
||||
#dryrun = False
|
||||
|
||||
# hook: rabbitmq
|
||||
#rabbitmq_endpoint =
|
||||
#rabbitmq_topic =
|
||||
#rabbitmq_queue_type = classic
|
||||
|
||||
# hook: email
|
||||
#smtp_server = localhost
|
||||
#smtp_port = 25
|
||||
#smtp_security = starttls
|
||||
|
|
|
@ -427,6 +427,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
|||
"help": "hook backend",
|
||||
"type": str,
|
||||
"internal": hook.INTERNAL_TYPES}),
|
||||
("dryrun", {
|
||||
"value": "False",
|
||||
"help": "dry-run (do not really trigger hook action)",
|
||||
"type": bool}),
|
||||
("rabbitmq_endpoint", {
|
||||
"value": "",
|
||||
"help": "endpoint where rabbitmq server is running",
|
||||
|
|
|
@ -1,6 +1,20 @@
|
|||
# This file is related to Radicale - CalDAV and CardDAV server
|
||||
# for email notifications
|
||||
# Copyright © 2025-2025 Nate Harris
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import enum
|
||||
import re
|
||||
import smtplib
|
||||
|
@ -596,6 +610,7 @@ class EmailConfig:
|
|||
password: str,
|
||||
from_email: str,
|
||||
send_mass_emails: bool,
|
||||
dryrun: bool,
|
||||
added_template: MessageTemplate,
|
||||
removed_template: MessageTemplate):
|
||||
self.host = host
|
||||
|
@ -606,6 +621,7 @@ class EmailConfig:
|
|||
self.password = password
|
||||
self.from_email = from_email
|
||||
self.send_mass_emails = send_mass_emails
|
||||
self.dryrun = dryrun
|
||||
self.added_template = added_template
|
||||
self.removed_template = removed_template
|
||||
self.updated_template = added_template # Reuse added template for updated events
|
||||
|
@ -616,7 +632,7 @@ class EmailConfig:
|
|||
Return a string representation of the EmailConfig.
|
||||
"""
|
||||
return f"EmailConfig(host={self.host}, port={self.port}, username={self.username}, " \
|
||||
f"from_email={self.from_email}, send_mass_emails={self.send_mass_emails})"
|
||||
f"from_email={self.from_email}, send_mass_emails={self.send_mass_emails}, dryrun={self.dryrun})"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
@ -733,6 +749,10 @@ class EmailConfig:
|
|||
logger.warning("No valid email addresses found in attendees. Cannot send email.")
|
||||
return False
|
||||
|
||||
if self.dryrun is True:
|
||||
logger.warning("Hook 'email': DRY-RUN _send_email / to_addresses=%r", to_addresses)
|
||||
return True
|
||||
|
||||
# Add headers
|
||||
message = MIMEMultipart("mixed")
|
||||
message["From"] = self.from_email
|
||||
|
@ -805,6 +825,7 @@ def _read_event(vobject_data: str) -> EmailEvent:
|
|||
class Hook(BaseHook):
|
||||
def __init__(self, configuration):
|
||||
super().__init__(configuration)
|
||||
self.dryrun = self.configuration.get("hook", "dryrun")
|
||||
self.email_config = EmailConfig(
|
||||
host=self.configuration.get("hook", "smtp_server"),
|
||||
port=self.configuration.get("hook", "smtp_port"),
|
||||
|
@ -814,6 +835,7 @@ class Hook(BaseHook):
|
|||
password=self.configuration.get("hook", "smtp_password"),
|
||||
from_email=self.configuration.get("hook", "from_email"),
|
||||
send_mass_emails=self.configuration.get("hook", "mass_email"),
|
||||
dryrun=self.configuration.get("hook", "dryrun"),
|
||||
added_template=MessageTemplate(
|
||||
subject="You have been added to an event",
|
||||
body=self.configuration.get("hook", "added_template")
|
||||
|
@ -844,7 +866,10 @@ class Hook(BaseHook):
|
|||
:type notification_item: HookNotificationItem
|
||||
:return: None
|
||||
"""
|
||||
logger.debug("Received notification item: %s", notification_item)
|
||||
if self.dryrun:
|
||||
logger.warning("Hook 'email': DRY-RUN received notification_item: %r", vars(notification_item))
|
||||
else:
|
||||
logger.debug("Received notification_item: %r", vars(notification_item))
|
||||
try:
|
||||
notification_type = HookNotificationItemTypes(value=notification_item.type)
|
||||
except ValueError:
|
||||
|
|
|
@ -1,3 +1,20 @@
|
|||
# This file is part of Radicale - CalDAV and CardDAV server
|
||||
# Copyright © 2020-2024 Tuna Celik <tuna@jakpark.com>
|
||||
# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pika
|
||||
from pika.exceptions import ChannelWrongStateError, StreamLostError
|
||||
|
||||
|
@ -14,16 +31,26 @@ class Hook(hook.BaseHook):
|
|||
self._topic = configuration.get("hook", "rabbitmq_topic")
|
||||
self._queue_type = configuration.get("hook", "rabbitmq_queue_type")
|
||||
self._encoding = configuration.get("encoding", "stock")
|
||||
self._dryrun = configuration.get("hook", "dryrun")
|
||||
logger.info("Hook 'rabbitmq': endpoint=%r topic=%r queue_type=%r dryrun=%s", self._endpoint, self._topic, self._queue_type, self._dryrun)
|
||||
|
||||
self._make_connection_synced()
|
||||
self._make_declare_queue_synced()
|
||||
|
||||
def _make_connection_synced(self):
|
||||
parameters = pika.URLParameters(self._endpoint)
|
||||
if self._dryrun is True:
|
||||
logger.warning("Hook 'rabbitmq': DRY-RUN _make_connection_synced / parameters=%r", parameters)
|
||||
return
|
||||
logger.debug("Hook 'rabbitmq': _make_connection_synced / parameters=%r", parameters)
|
||||
connection = pika.BlockingConnection(parameters)
|
||||
self._channel = connection.channel()
|
||||
|
||||
def _make_declare_queue_synced(self):
|
||||
if self._dryrun is True:
|
||||
logger.warning("Hook 'rabbitmq': DRY-RUN _make_declare_queue_synced")
|
||||
return
|
||||
logger.debug("Hook 'rabbitmq': _make_declare_queue_synced")
|
||||
self._channel.queue_declare(queue=self._topic, durable=True, arguments={"x-queue-type": self._queue_type})
|
||||
|
||||
def notify(self, notification_item):
|
||||
|
@ -31,6 +58,9 @@ class Hook(hook.BaseHook):
|
|||
self._notify(notification_item, True)
|
||||
|
||||
def _notify(self, notification_item, recall):
|
||||
if self._dryrun is True:
|
||||
logger.warning("Hook 'rabbitmq': DRY-RUN _notify / notification_item: %r", vars(notification_item))
|
||||
return
|
||||
try:
|
||||
self._channel.basic_publish(
|
||||
exchange='',
|
||||
|
|
110
radicale/tests/test_hook_email.py
Normal file
110
radicale/tests/test_hook_email.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
# This file is part of Radicale - CalDAV and CardDAV server
|
||||
# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Radicale tests related to hook 'email'
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from radicale.tests import BaseTest
|
||||
from radicale.tests.helpers import get_file_content
|
||||
|
||||
|
||||
class TestHooks(BaseTest):
|
||||
"""Tests with hooks."""
|
||||
|
||||
def setup_method(self) -> None:
|
||||
BaseTest.setup_method(self)
|
||||
rights_file_path = os.path.join(self.colpath, "rights")
|
||||
with open(rights_file_path, "w") as f:
|
||||
f.write("""\
|
||||
[permit delete collection]
|
||||
user: .*
|
||||
collection: test-permit-delete
|
||||
permissions: RrWwD
|
||||
|
||||
[forbid delete collection]
|
||||
user: .*
|
||||
collection: test-forbid-delete
|
||||
permissions: RrWwd
|
||||
|
||||
[permit overwrite collection]
|
||||
user: .*
|
||||
collection: test-permit-overwrite
|
||||
permissions: RrWwO
|
||||
|
||||
[forbid overwrite collection]
|
||||
user: .*
|
||||
collection: test-forbid-overwrite
|
||||
permissions: RrWwo
|
||||
|
||||
[allow all]
|
||||
user: .*
|
||||
collection: .*
|
||||
permissions: RrWw""")
|
||||
self.configure({"rights": {"file": rights_file_path,
|
||||
"type": "from_file"}})
|
||||
self.configure({"hook": {"type": "email",
|
||||
"dryrun": "True"}})
|
||||
|
||||
def test_add_event(self, caplog) -> None:
|
||||
caplog.set_level(logging.WARNING)
|
||||
"""Add an event."""
|
||||
self.mkcalendar("/calendar.ics/")
|
||||
event = get_file_content("event1.ics")
|
||||
path = "/calendar.ics/event1.ics"
|
||||
self.put(path, event)
|
||||
_, headers, answer = self.request("GET", path, check=200)
|
||||
assert "ETag" in headers
|
||||
assert headers["Content-Type"] == "text/calendar; charset=utf-8"
|
||||
assert "VEVENT" in answer
|
||||
assert "Event" in answer
|
||||
assert "UID:event" in answer
|
||||
found = 0
|
||||
for line in caplog.messages:
|
||||
if line.find("notification_item: {'type': 'upsert'") != -1:
|
||||
found = found | 1
|
||||
if line.find("to_addresses=['janedoe@example.com']") != -1:
|
||||
found = found | 2
|
||||
if line.find("to_addresses=['johndoe@example.com']") != -1:
|
||||
found = found | 4
|
||||
if (found != 7):
|
||||
raise ValueError("Logging misses expected log lines, found=%d", found)
|
||||
|
||||
def test_delete_event(self, caplog) -> None:
|
||||
caplog.set_level(logging.WARNING)
|
||||
"""Delete an event."""
|
||||
self.mkcalendar("/calendar.ics/")
|
||||
event = get_file_content("event1.ics")
|
||||
path = "/calendar.ics/event1.ics"
|
||||
self.put(path, event)
|
||||
_, responses = self.delete(path)
|
||||
assert responses[path] == 200
|
||||
_, answer = self.get("/calendar.ics/")
|
||||
assert "VEVENT" not in answer
|
||||
found = 0
|
||||
for line in caplog.messages:
|
||||
if line.find("notification_item: {'type': 'delete'") != -1:
|
||||
found = found | 1
|
||||
if line.find("to_addresses=['janedoe@example.com']") != -1:
|
||||
found = found | 2
|
||||
if line.find("to_addresses=['johndoe@example.com']") != -1:
|
||||
found = found | 4
|
||||
if (found != 7):
|
||||
raise ValueError("Logging misses expected log lines, found=%d", found)
|
102
radicale/tests/test_hook_rabbitmq.py
Normal file
102
radicale/tests/test_hook_rabbitmq.py
Normal file
|
@ -0,0 +1,102 @@
|
|||
# This file is part of Radicale - CalDAV and CardDAV server
|
||||
# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
Radicale tests related to hook 'rabbitmq'
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from radicale.tests import BaseTest
|
||||
from radicale.tests.helpers import get_file_content
|
||||
|
||||
|
||||
class TestHooks(BaseTest):
|
||||
"""Tests with hooks."""
|
||||
|
||||
def setup_method(self) -> None:
|
||||
BaseTest.setup_method(self)
|
||||
rights_file_path = os.path.join(self.colpath, "rights")
|
||||
with open(rights_file_path, "w") as f:
|
||||
f.write("""\
|
||||
[permit delete collection]
|
||||
user: .*
|
||||
collection: test-permit-delete
|
||||
permissions: RrWwD
|
||||
|
||||
[forbid delete collection]
|
||||
user: .*
|
||||
collection: test-forbid-delete
|
||||
permissions: RrWwd
|
||||
|
||||
[permit overwrite collection]
|
||||
user: .*
|
||||
collection: test-permit-overwrite
|
||||
permissions: RrWwO
|
||||
|
||||
[forbid overwrite collection]
|
||||
user: .*
|
||||
collection: test-forbid-overwrite
|
||||
permissions: RrWwo
|
||||
|
||||
[allow all]
|
||||
user: .*
|
||||
collection: .*
|
||||
permissions: RrWw""")
|
||||
self.configure({"rights": {"file": rights_file_path,
|
||||
"type": "from_file"}})
|
||||
self.configure({"hook": {"type": "rabbitmq",
|
||||
"dryrun": "True"}})
|
||||
|
||||
def test_add_event(self, caplog) -> None:
|
||||
caplog.set_level(logging.WARNING)
|
||||
"""Add an event."""
|
||||
self.mkcalendar("/calendar.ics/")
|
||||
event = get_file_content("event1.ics")
|
||||
path = "/calendar.ics/event1.ics"
|
||||
self.put(path, event)
|
||||
_, headers, answer = self.request("GET", path, check=200)
|
||||
assert "ETag" in headers
|
||||
assert headers["Content-Type"] == "text/calendar; charset=utf-8"
|
||||
assert "VEVENT" in answer
|
||||
assert "Event" in answer
|
||||
assert "UID:event" in answer
|
||||
found = False
|
||||
for line in caplog.messages:
|
||||
if line.find("notification_item: {'type': 'upsert'") != -1:
|
||||
found = True
|
||||
if (found is False):
|
||||
raise ValueError("Logging misses expected log line")
|
||||
|
||||
def test_delete_event(self, caplog) -> None:
|
||||
caplog.set_level(logging.WARNING)
|
||||
"""Delete an event."""
|
||||
self.mkcalendar("/calendar.ics/")
|
||||
event = get_file_content("event1.ics")
|
||||
path = "/calendar.ics/event1.ics"
|
||||
self.put(path, event)
|
||||
_, responses = self.delete(path)
|
||||
assert responses[path] == 200
|
||||
_, answer = self.get("/calendar.ics/")
|
||||
assert "VEVENT" not in answer
|
||||
found = False
|
||||
for line in caplog.messages:
|
||||
if line.find("notification_item: {'type': 'delete'") != -1:
|
||||
found = True
|
||||
if (found is False):
|
||||
raise ValueError("Logging misses expected log line")
|
Loading…
Add table
Add a link
Reference in a new issue