Add alertmanager as supported sender and update deps

This commit is contained in:
Abel Luck 2023-11-07 15:14:56 +01:00
parent 05ffc640ed
commit 973e1fd789
18 changed files with 1682 additions and 1155 deletions

4
.gitignore vendored
View file

@ -5,4 +5,6 @@ __pycache__
devstate devstate
.env* .env*
config.json config.json
dev.data dev.data
data
.direnv

View file

@ -11,15 +11,14 @@ variables:
REF_IMAGE: registry.gitlab.com/$CI_PROJECT_NAMESPACE/${CI_PROJECT_NAME}:$CI_COMMIT_REF_NAME REF_IMAGE: registry.gitlab.com/$CI_PROJECT_NAMESPACE/${CI_PROJECT_NAME}:$CI_COMMIT_REF_NAME
check: check:
image: python:3.10-bullseye image: python:3.11-bullseye
stage: check stage: check
script: script:
- apt-get update - apt-get update
- apt-get install -y make libolm-dev - apt-get install -y make libolm-dev python3-poetry
- pip install poetry
- poetry config virtualenvs.create false - poetry config virtualenvs.create false
- poetry install - poetry install
- make ci - make check
build-test: build-test:
image: docker:git image: docker:git

View file

@ -1 +0,0 @@
3.10.5

View file

@ -1,6 +1,6 @@
ARG PYTHON_VERSION=3.10 ARG PYTHON_VERSION=3.11
FROM docker.io/python:${PYTHON_VERSION}-alpine as builder FROM docker.io/python:${PYTHON_VERSION}-alpine as builder
ARG LIBOLM_VERSION=3.2.10 ARG LIBOLM_VERSION=3.2.15
RUN apk add --no-cache \ RUN apk add --no-cache \
make \ make \
cmake \ cmake \

View file

@ -1,9 +1,21 @@
POETRY ?= poetry run POETRY ?= poetry run
SRC := ops_bot SRC := ops_bot
TESTS := tests TESTS := tests
SHELL := $(shell which bash)
APP_VERSION := $(shell git rev-parse --short HEAD)
DOCKER ?= docker
docker-build:
DOCKER_BUILDKIT=1 $(DOCKER) build -f docker/Dockerfile \
--build-arg=$(APP_VERSION) \
-t matrix-ops-bot:latest \
.
fmt: fmt:
$(POETRY) black $(SRC) $(POETRY) black $(SRC)
$(POETRY) black $(TESTS)
$(POETRY) isort --profile black $(SRC) $(POETRY) isort --profile black $(SRC)
$(POETRY) isort --profile black $(TESTS)
lint: lint:
$(POETRY) flake8 $(SRC) $(POETRY) flake8 $(SRC)
$(POETRY) bandit --silent --recursive $(SRC) $(POETRY) bandit --silent --recursive $(SRC)
@ -13,9 +25,12 @@ types:
test: test:
$(POETRY) pytest $(TESTS) $(POETRY) pytest $(TESTS)
ci: lint types test
freeze: freeze:
poetry export --without dev --format=requirements.txt --output requirements.frozen.txt poetry export --without dev --format=requirements.txt --output requirements.frozen.txt
check:
$(MAKE) fmt
$(MAKE) lint
$(MAKE) types
$(MAKE) bandit
$(MAKE) test

View file

@ -9,6 +9,7 @@ Current supported webhooks:
* PagerDuty * PagerDuty
* AWS SNS * AWS SNS
* Gitlab * Gitlab
* Prometheus Alertmanager
## Usage ## Usage

57
ops_bot/alertmanager.py Normal file
View file

@ -0,0 +1,57 @@
import logging
from typing import Any, Dict, List, Tuple
from fastapi import Request
from ops_bot.common import COLOR_ALARM, COLOR_OK, COLOR_UNKNOWN
from ops_bot.config import RoutingKey
def prometheus_alert_to_markdown(
alert_data: Dict, # type: ignore[type-arg]
) -> List[Tuple[str, str]]:
"""
Converts a prometheus alert json to markdown
"""
messages = []
known_labels = ["alertname", "instance", "job"]
for alert in alert_data["alerts"]:
title = (
alert["annotations"]["description"]
if hasattr(alert["annotations"], "description")
else alert["annotations"]["summary"]
)
severity = alert.get("severity", "critical")
if alert["status"] == "resolved":
color = COLOR_OK
elif severity == "critical":
color = COLOR_ALARM
else:
color = COLOR_UNKNOWN
status = alert["status"]
generatorURL = alert.get("generatorURL")
plain = f"{status}: {title}"
header_str = f"{status.upper()}[{status}]"
formatted = f"<strong><font color={color}>{header_str}</font></strong>: [{title}]({generatorURL})"
for label_name in known_labels:
try:
plain += "\n* **{0}**: {1}".format(
label_name.capitalize(), alert["labels"][label_name]
)
formatted += "\n* **{0}**: {1}".format(
label_name.capitalize(), alert["labels"][label_name]
)
except Exception as e:
logging.error("error parsing labels", exc_info=e)
pass
messages.append((plain, formatted))
return messages
async def parse_alertmanager_event(
route: RoutingKey,
payload: Any,
request: Request,
) -> List[Tuple[str, str]]:
return prometheus_alert_to_markdown(payload)

View file

@ -4,7 +4,7 @@ from typing import Any
import click import click
from ops_bot.config import RoutingKey, load_config, save_config from ops_bot.config import RoutingKey, hook_types, load_config, save_config
@click.group() @click.group()
@ -28,20 +28,23 @@ def cli(ctx: Any, config_file: str) -> None:
) )
@click.option("--room-id", help="The room ID to send the messages to") @click.option("--room-id", help="The room ID to send the messages to")
@click.pass_obj @click.pass_obj
def add_hook(config_file: str, name: str, hook_type: str, room_id: str) -> None: def add_hook(config_file: str, name: str, maybe_hook_type: str, room_id: str) -> None:
settings = load_config(config_file) settings = load_config(config_file)
path_key = secrets.token_urlsafe(30) path_key = secrets.token_urlsafe(30)
secret_token = secrets.token_urlsafe(30) secret_token = secrets.token_urlsafe(30)
if name in set([key.name for key in settings.routing_keys]): if name in set([key.name for key in settings.routing_keys]):
print("Error: A hook with that name already exists") print("Error: A hook with that name already exists")
sys.exit(1) sys.exit(1)
if maybe_hook_type not in hook_types:
print(f"Invalid hook type {maybe_hook_type}")
print("Must be one of ", ", ".join(hook_types))
settings.routing_keys.append( settings.routing_keys.append(
RoutingKey( RoutingKey(
name=name, name=name,
path_key=path_key, path_key=path_key,
secret_token=secret_token, secret_token=secret_token,
room_id=room_id, room_id=room_id,
hook_type=hook_type, hook_type=maybe_hook_type, # type: ignore[arg-type]
) )
) )
save_config(settings) save_config(settings)

View file

@ -1,30 +1,37 @@
import json import json
import logging import logging
import os
import typing
from pathlib import Path from pathlib import Path
from typing import List, Literal from typing import List, Literal
from pydantic import BaseSettings from pydantic_settings import BaseSettings, SettingsConfigDict
from ops_bot.matrix import MatrixClientSettings from ops_bot.matrix import MatrixClientSettings
env_path = os.getenv("BOT_ENV_FILE")
HookType = Literal["gitlab", "pagerduty", "aws-sns", "alertmanager"]
hook_types = typing.get_args(HookType)
class RoutingKey(BaseSettings): class RoutingKey(BaseSettings):
name: str name: str
path_key: str path_key: str
secret_token: str secret_token: str
room_id: str room_id: str
hook_type: Literal["gitlab", "pagerduty", "aws-sns"] hook_type: HookType
class BotSettings(BaseSettings): class BotSettings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="BOT_", env_file=env_path, env_file_encoding="utf-8"
)
routing_keys: List[RoutingKey] routing_keys: List[RoutingKey]
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO" log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
matrix: MatrixClientSettings matrix: MatrixClientSettings
class Config:
env_prefix = "BOT_"
case_sensitive = False
def get_rooms(self) -> List[str]: def get_rooms(self) -> List[str]:
return list(set([key.room_id for key in self.routing_keys])) return list(set([key.room_id for key in self.routing_keys]))
@ -35,9 +42,10 @@ def config_file_exists(filename: str) -> bool:
def load_config(filename: str = "config.json") -> BotSettings: def load_config(filename: str = "config.json") -> BotSettings:
if config_file_exists(filename): if config_file_exists(filename):
bot_settings = BotSettings.parse_file(filename) with open(filename, "r") as f:
bot_settings = BotSettings.model_validate_json(f.read())
else: else:
bot_settings = BotSettings(_env_file=".env", _env_file_encoding="utf-8") bot_settings = BotSettings() # type: ignore[call-arg]
logging.getLogger().setLevel(bot_settings.log_level) logging.getLogger().setLevel(bot_settings.log_level)
return bot_settings return bot_settings

View file

@ -59,9 +59,9 @@ async def handle_event(x_gitlab_event: str, payload: Any) -> List[Tuple[str, str
**attr.asdict(subevt, recurse=False), **attr.asdict(subevt, recurse=False),
**{key: getattr(subevt, key) for key in subevt.event_properties}, **{key: getattr(subevt, key) for key in subevt.event_properties},
"abort": abort, "abort": abort,
**base_args, # type: ignore **base_args,
} }
args["templates"] = templates.proxy(args) # type: ignore args["templates"] = templates.proxy(args)
html = tpl.render(**args) html = tpl.render(**args)
if not html or aborted: if not html or aborted:
@ -87,4 +87,6 @@ async def parse_event(
route: RoutingKey, payload: Any, request: Request route: RoutingKey, payload: Any, request: Request
) -> List[Tuple[str, str]]: ) -> List[Tuple[str, str]]:
x_gitlab_event = request.headers.get("x-gitlab-event") x_gitlab_event = request.headers.get("x-gitlab-event")
return await handle_event(x_gitlab_event, payload) if x_gitlab_event:
return await handle_event(x_gitlab_event, payload)
return []

View file

@ -1,10 +1,12 @@
import asyncio import asyncio
import logging import logging
import os import os
from typing import Any, Dict, List, Optional, Protocol, Tuple, cast import sys
from contextlib import asynccontextmanager
from typing import Any, AsyncIterator, Dict, List, Optional, Protocol, Tuple, cast
import json_logging
import uvicorn import uvicorn
from dotenv import load_dotenv
from fastapi import Depends, FastAPI, HTTPException, Request, status from fastapi import Depends, FastAPI, HTTPException, Request, status
from fastapi.security import ( from fastapi.security import (
HTTPAuthorizationCredentials, HTTPAuthorizationCredentials,
@ -13,18 +15,11 @@ from fastapi.security import (
HTTPBearer, HTTPBearer,
) )
from ops_bot import aws, pagerduty from ops_bot import alertmanager, aws, pagerduty
from ops_bot.config import BotSettings, RoutingKey, load_config from ops_bot.config import BotSettings, RoutingKey, load_config
from ops_bot.gitlab import hook as gitlab_hook from ops_bot.gitlab import hook as gitlab_hook
from ops_bot.matrix import MatrixClient from ops_bot.matrix import MatrixClient
load_dotenv()
app = FastAPI()
bearer_security = HTTPBearer(auto_error=False)
basic_security = HTTPBasic(auto_error=False)
async def get_matrix_service(request: Request) -> MatrixClient: async def get_matrix_service(request: Request) -> MatrixClient:
"""A helper to fetch the matrix client from the app state""" """A helper to fetch the matrix client from the app state"""
@ -37,21 +32,29 @@ async def matrix_main(matrix_client: MatrixClient) -> None:
await asyncio.gather(*workers) await asyncio.gather(*workers)
@app.on_event("startup") @asynccontextmanager
async def startup_event() -> None: async def lifespan(app: FastAPI) -> AsyncIterator[None]:
config_fname = os.environ.get("BOT_CONFIG_FILE", "config.yaml") config_fname = os.environ.get("BOT_CONFIG_FILE", "config.yaml")
bot_settings = load_config(config_fname) bot_settings = load_config(config_fname)
c = MatrixClient(settings=bot_settings.matrix, join_rooms=bot_settings.get_rooms()) c = MatrixClient(settings=bot_settings.matrix, join_rooms=bot_settings.get_rooms())
app.state.matrix_client = c app.state.matrix_client = c
app.state.bot_settings = bot_settings app.state.bot_settings = bot_settings
asyncio.create_task(matrix_main(c)) asyncio.create_task(matrix_main(c))
yield
@app.on_event("shutdown")
async def shutdown_event() -> None:
await app.state.matrix_client.shutdown() await app.state.matrix_client.shutdown()
app = FastAPI(lifespan=lifespan)
bearer_security = HTTPBearer(auto_error=False)
basic_security = HTTPBasic(auto_error=False)
log = logging.getLogger("ops_bot")
log.addHandler(logging.StreamHandler(sys.stdout))
json_logging.init_fastapi(enable_json=True)
json_logging.init_request_instrument(app)
@app.get("/") @app.get("/")
async def root() -> Dict[str, str]: async def root() -> Dict[str, str]:
return {"message": "Hello World"} return {"message": "Hello World"}
@ -113,6 +116,7 @@ handlers: Dict[str, Tuple[Authorizer, ParseHandler]] = {
"gitlab": (gitlab_hook.authorize, gitlab_hook.parse_event), "gitlab": (gitlab_hook.authorize, gitlab_hook.parse_event),
"pagerduty": (bearer_token_authorizer, pagerduty.parse_pagerduty_event), "pagerduty": (bearer_token_authorizer, pagerduty.parse_pagerduty_event),
"aws-sns": (nop_authorizer, aws.parse_sns_event), "aws-sns": (nop_authorizer, aws.parse_sns_event),
"alertmanager": (nop_authorizer, alertmanager.parse_alertmanager_event),
} }

View file

@ -1,12 +1,14 @@
import json import json
import logging import logging
import os
import pathlib import pathlib
import sys import sys
from typing import List, Optional, Protocol from typing import List, Optional, Protocol
from markdown import markdown from markdown import markdown
from nio import AsyncClient, AsyncClientConfig, LoginResponse from nio import AsyncClient, AsyncClientConfig, LoginResponse
from pydantic import BaseModel, BaseSettings from pydantic import BaseModel
from pydantic_settings import BaseSettings
class ClientCredentials(BaseModel): class ClientCredentials(BaseModel):
@ -72,6 +74,9 @@ class MatrixClient:
self.greeting_sent = False self.greeting_sent = False
if self.store_path and not os.path.isdir(self.store_path):
os.mkdir(self.store_path)
async def start(self) -> None: async def start(self) -> None:
await self.login() await self.login()
@ -95,9 +100,9 @@ class MatrixClient:
async def login_fresh(self) -> None: async def login_fresh(self) -> None:
self.client = AsyncClient( self.client = AsyncClient(
self.settings.homeserver, homeserver=self.settings.homeserver,
self.settings.user_id, user=self.settings.user_id,
self.settings.store_path, store_path=str(self.settings.store_path),
config=self.client_config, config=self.client_config,
ssl=self.settings.verify_ssl, ssl=self.settings.verify_ssl,
) )
@ -117,11 +122,12 @@ class MatrixClient:
async def login_with_credentials(self) -> None: async def login_with_credentials(self) -> None:
credentials = self.credential_store.read() credentials = self.credential_store.read()
self.client = AsyncClient( self.client = AsyncClient(
credentials.homeserver, homeserver=credentials.homeserver,
credentials.user_id, user=credentials.user_id,
device_id=credentials.device_id, device_id=credentials.device_id,
store_path=self.store_path, store_path=str(self.store_path),
config=self.client_config, config=self.client_config,
ssl=True, ssl=True,
) )

2499
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,23 +5,26 @@ description = ""
authors = ["Abel Luck <abel@guardianproject.info>"] authors = ["Abel Luck <abel@guardianproject.info>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10" python = "^3.11"
matrix-nio = {extras = ["e2e"], version = "^0.19.0"} matrix-nio = {extras = ["e2e"], version = "^0.22.1"}
fastapi = "^0.79.0" fastapi = "^0.104.1"
uvicorn = "^0.18.2" uvicorn = "^0.24.0"
termcolor = "^1.1.0" termcolor = "^1.1.0"
Markdown = "^3.4.1" Markdown = "^3.4.1"
pydantic = {extras = ["dotenv"], version = "^1.9.1"}
commonmark = "^0.9.1" commonmark = "^0.9.1"
Jinja2 = "^3.1.2" Jinja2 = "^3.1.2"
mautrix = "^0.18.8" mautrix = "^0.20.2"
click = "^8.1.3" click = "^8.1.3"
json-logging = "^1.3.0"
pydantic-settings = "^2.0.3"
prometheus-client = "^0.18.0"
prometheus-fastapi-instrumentator = "^6.1.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^7.2.0" pytest = "^7.2.0"
black = "^22.10.0" black = "^22.10.0"
isort = "^5.10.1" isort = "^5.10.1"
mypy = "^0.991" mypy = "^1.2.0"
bandit = "^1.7.4" bandit = "^1.7.4"
flake8 = "^6.0.0" flake8 = "^6.0.0"
flake8-black = "^0.3.5" flake8-black = "^0.3.5"
@ -68,4 +71,4 @@ warn_unreachable = true
show_error_codes = true show_error_codes = true
# Explicit is better than implicit # Explicit is better than implicit
no_implicit_optional = true no_implicit_optional = true

33
shell.nix Normal file
View file

@ -0,0 +1,33 @@
{ system ? "x86_64-linux", pkgs ? import <nixpkgs> { inherit system; } }:
let
packages = [
pkgs.python311
pkgs.poetry
pkgs.zsh
];
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
pkgs.stdenv.cc.cc
# Add any missing library needed
# You can use the nix-index package to locate them, e.g. nix-locate -w --top-level --at-root /lib/libudev.so.1
];
# Put the venv on the repo, so direnv can access it
POETRY_VIRTUALENVS_IN_PROJECT = "true";
POETRY_VIRTUALENVS_PATH = "{project-dir}/.venv";
# Use python from path, so you can use a different version to the one bundled with poetry
POETRY_VIRTUALENVS_PREFER_ACTIVE_PYTHON = "true";
in
pkgs.mkShell {
buildInputs = packages;
shellHook = ''
export SHELL=${pkgs.zsh}
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}"
export POETRY_VIRTUALENVS_IN_PROJECT="${POETRY_VIRTUALENVS_IN_PROJECT}"
export POETRY_VIRTUALENVS_PATH="${POETRY_VIRTUALENVS_PATH}"
export POETRY_VIRTUALENVS_PREFER_ACTIVE_PYTHON="${POETRY_VIRTUALENVS_PREFER_ACTIVE_PYTHON}"
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
'';
}

View file

@ -0,0 +1,57 @@
from ops_bot.alertmanager import prometheus_alert_to_markdown
import json
payload = json.loads(
"""
{
"receiver": "matrix",
"status": "firing",
"alerts": [
{
"status": "firing",
"labels": {
"alertname": "InstanceDown",
"environment": "monitoring.example.com",
"instance": "webserver.example.com",
"job": "node_exporter",
"severity": "critical"
},
"annotations": {
"description": "webserver.example.com of job node_exporter has been down for more than 5 minutes.",
"summary": "THIS IS A TEST Instance webserver.example.com down"
},
"startsAt": "2022-06-23T11:53:14.318Z",
"endsAt": "0001-01-01T00:00:00Z",
"generatorURL": "http://monitoring.example.com:9090",
"fingerprint": "9cd7837114d58797"
}
],
"groupLabels": {
"alertname": "InstanceDown"
},
"commonLabels": {
"alertname": "InstanceDown",
"environment": "monitoring.example.com",
"instance": "webserver.example.com",
"job": "node_exporter",
"severity": "critical"
},
"commonAnnotations": {
"description": "webserver.example.com of job node_exporter has been down for more than 5 minutes.",
"summary": "Instance webserver.example.com down"
},
"externalURL": "https://alert.example",
"version": "4",
"groupKey": "",
"truncatedAlerts": 0
}"""
)
def test_alertmanager():
r = prometheus_alert_to_markdown(payload)
assert len(r) == 1
plain, formatted = r[0]
assert "firing" in plain and "Instance webserver.example.com down" in plain
assert "firing" in formatted and "Instance webserver.example.com down" in formatted

View file

@ -1,4 +1,5 @@
import json import json
from ops_bot.gitlab import hook from ops_bot.gitlab import hook
from ops_bot.util.template import TemplateUtil from ops_bot.util.template import TemplateUtil
@ -148,16 +149,27 @@ issue_open_payload_raw = """
} }
}""" }"""
issue_open_payload = json.loads(issue_open_payload_raw) issue_open_payload = json.loads(issue_open_payload_raw)
def test_templates(): def test_templates():
# print(gitlab.messages._loader.list_templates()) # print(gitlab.messages._loader.list_templates())
tpl = hook.messages["test"] tpl = hook.messages["test"]
args = issue_open_payload | {"util": TemplateUtil} args = issue_open_payload | {"util": TemplateUtil}
args["templates"] = hook.templates.proxy(args) args["templates"] = hook.templates.proxy(args)
assert tpl.render(**args) == "<strong data-mautrix-exclude-plaintext>[<a data-mautrix-exclude-plaintext href=\"http://example.com/gitlabhq/gitlab-test\">gitlabhq/gitlab-test</a>]</strong> <a data-mautrix-exclude-plaintext href=\"\">root</a>" assert (
tpl.render(**args)
== '<strong data-mautrix-exclude-plaintext>[<a data-mautrix-exclude-plaintext href="http://example.com/gitlabhq/gitlab-test">gitlabhq/gitlab-test</a>]</strong> <a data-mautrix-exclude-plaintext href="">root</a>'
)
async def test_hook(): async def test_hook():
r = await hook.handle_event("Issue Hook", issue_open_payload) r = await hook.handle_event("Issue Hook", issue_open_payload)
assert r[0] assert r[0]
assert r[0][0] == "[gitlabhq/gitlab-test] root opened [issue #23](http://example.com/diaspora/issues/23): New API: create/update/delete file\n \n> Create new API for manipulations with repository\nAPI" assert (
assert r[0][1] == "<strong data-mautrix-exclude-plaintext>[<a data-mautrix-exclude-plaintext href=\"http://example.com/gitlabhq/gitlab-test\">gitlabhq/gitlab-test</a>]</strong> <a data-mautrix-exclude-plaintext href=\"http://example.com/root\">root</a>\n opened <a href=\"http://example.com/diaspora/issues/23\" >issue #23</a>: New API: create/update/delete file<br/>\n <blockquote><p>Create new API for manipulations with repository</p>\n</blockquote>\n <span data-mx-color=\"#000000\"\n data-mx-bg-color=\"#ffffff\"\n title=\"API related issues\"\n >&nbsp;API&nbsp;</span>" r[0][0]
== "[gitlabhq/gitlab-test] root opened [issue #23](http://example.com/diaspora/issues/23): New API: create/update/delete file\n \n> Create new API for manipulations with repository\nAPI"
)
assert (
r[0][1]
== '<strong data-mautrix-exclude-plaintext>[<a data-mautrix-exclude-plaintext href="http://example.com/gitlabhq/gitlab-test">gitlabhq/gitlab-test</a>]</strong> <a data-mautrix-exclude-plaintext href="http://example.com/root">root</a>\n opened <a href="http://example.com/diaspora/issues/23" >issue #23</a>: New API: create/update/delete file<br/>\n <blockquote><p>Create new API for manipulations with repository</p>\n</blockquote>\n <span data-mx-color="#000000"\n data-mx-bg-color="#ffffff"\n title="API related issues"\n >&nbsp;API&nbsp;</span>'
)

View file

@ -1,11 +1,12 @@
import json import json
from ops_bot import __version__
from ops_bot import aws from ops_bot import __version__, aws
def test_version() -> None: def test_version() -> None:
assert __version__ == "0.1.0" assert __version__ == "0.1.0"
sns_subscribtion_unsubscribe = """{ sns_subscribtion_unsubscribe = """{
"Type" : "UnsubscribeConfirmation", "Type" : "UnsubscribeConfirmation",
"MessageId" : "47138184-6831-46b8-8f7c-afc488602d7d", "MessageId" : "47138184-6831-46b8-8f7c-afc488602d7d",
@ -44,19 +45,25 @@ sns_notification = """{
"UnsubscribeURL" : "https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-west-2:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96" "UnsubscribeURL" : "https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-west-2:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96"
}""" }"""
async def test_aws_sns_notification() -> None: async def test_aws_sns_notification() -> None:
r = await aws.parse_sns_event(None, json.loads(sns_notification), None) r = await aws.parse_sns_event(None, json.loads(sns_notification), None)
assert r[0][0] == "My First Message\nHello world!" assert r[0][0] == "My First Message\nHello world!"
assert r[0][1] == "<strong><font color=#dc3545>My First Message</font></strong>\n<p>Hello world!</p>" assert (
r[0][1]
== "<strong><font color=#dc3545>My First Message</font></strong>\n<p>Hello world!</p>"
)
async def test_aws_sns_subscribe() -> None: async def test_aws_sns_subscribe() -> None:
r = await aws.parse_sns_event(None, json.loads(sns_subscribtion_confirm), None) r = await aws.parse_sns_event(None, json.loads(sns_subscribtion_confirm), None)
print(r) print(r)
expected = 'You have chosen to subscribe to the topic arn:aws:sns:us-west-2:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message.\n\nhttps://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37...' expected = "You have chosen to subscribe to the topic arn:aws:sns:us-west-2:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message.\n\nhttps://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37..."
assert r[0] == (expected, expected) assert r[0] == (expected, expected)
async def test_aws_sns_unsubscribe() -> None: async def test_aws_sns_unsubscribe() -> None:
r = await aws.parse_sns_event(None, json.loads(sns_subscribtion_unsubscribe), None) r = await aws.parse_sns_event(None, json.loads(sns_subscribtion_unsubscribe), None)
print(r) print(r)
expected = 'You have chosen to deactivate subscription arn:aws:sns:us-west-2:123456789012:MyTopic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.\n\nhttps://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37fb6...' expected = "You have chosen to deactivate subscription arn:aws:sns:us-west-2:123456789012:MyTopic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.\n\nhttps://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-west-2:123456789012:MyTopic&Token=2336412f37fb6..."
assert r[0] == (expected, expected) assert r[0] == (expected, expected)