afterhours-shift-manager/tests/release_notifier/test_handler.py

68 lines
2.1 KiB
Python
Raw Normal View History

Add changelog-driven releases and App Home tab (#112) * Add changelog-driven releases and App Home tab Version the bot continuously from CHANGELOG.md (the single source of truth for both the version and the staff-readable notes) and surface changes to users in two ways: - A new afterhours-release-notifier Lambda posts a "What's New" message to the shift channel on minor/major releases (patches stay silent). - The bot gains an App Home "About" tab showing what it does, the command list, and the current version's notes. release.yaml runs on Deploy success (not release:published — GITHUB_TOKEN events don't start downstream workflows), checks out the deployed commit, and tags + publishes a GitHub Release + invokes the notifier. It assumes a dedicated, boundary-carrying OIDC role scoped to InvokeFunction on the notifier; the account's cfn role gates role creation on that boundary. The manual Version Bump workflow is retired. A CI guard enforces that a CHANGELOG edit is a clean SemVer bump and that the in-package copy matches. * Harden release workflow and regex against CodeQL findings Address three code-scanning alerts on the PR: - Critical (actions/untrusted-checkout): split release.yaml into a read-only `prepare` job that checks out and runs repo code, and a privileged `publish` job (contents:write + OIDC) that never checks out repo code — it tags, releases, and invokes purely through the GitHub and AWS APIs. Also assert head_branch == main. - High x2 (py/polynomial-redos): rewrite the italic and link regexes in markdown_to_mrkdwn with possessive quantifiers and exclusive character classes so they run in linear time on adversarial input. Adds a regression test. * Move release/announce into Deploy workflow to clear CodeQL The workflow_run-triggered release.yaml kept tripping CodeQL's privileged-context rules (untrusted-checkout, then cache-poisoning) — CodeQL distrusts any workflow_run that checks out a ref, regardless of the main-only guarantee, and there is no autofix. Fold the release job into deploy.yaml gated on `needs: deploy`. A push-to-main run is a trusted context, so checking out and running repo code with write/OIDC is safe there. This still gates on deploy success and serializes via the deploy concurrency group, and removes the separate workflow entirely.
2026-06-11 19:41:31 -04:00
"""Tests for the release-notifier Lambda handler."""
from unittest.mock import MagicMock
import pytest
@pytest.fixture
def slack(notifier_app, monkeypatch):
"""Fake Slack WebClient; chat_postMessage returns a message ts."""
fake = MagicMock(name="slack")
fake.chat_postMessage.return_value = {"ts": "111.222"}
monkeypatch.setattr(notifier_app, "WebClient", MagicMock(return_value=fake))
monkeypatch.setattr(notifier_app, "get_secret", lambda _id: "xoxb-test")
return fake
@pytest.fixture
def env(monkeypatch):
monkeypatch.setenv(
"SLACK_BOT_TOKEN_SECRET", "afterhours-shift-manager/slack-bot-token"
)
monkeypatch.setenv("SHIFT_CHANNEL", "C_RELEASES")
def test_posts_announcement_to_channel(notifier_app, slack, env):
result = notifier_app.handler(
{
"version": "1.10.0",
"notes": "**Release notes** now self-announce.",
"date_label": "June 11, 2026",
},
None,
)
assert result == {"announced": True, "version": "1.10.0", "ts": "111.222"}
slack.chat_postMessage.assert_called_once()
kwargs = slack.chat_postMessage.call_args.kwargs
assert kwargs["channel"] == "C_RELEASES"
assert kwargs["text"] == "What's New — v1.10.0"
# Markdown was converted to Slack mrkdwn in the rendered blocks.
rendered = str(kwargs["blocks"])
assert "*Release notes*" in rendered
def test_uses_the_slack_bot_token_secret(notifier_app, slack, env, monkeypatch):
seen = {}
monkeypatch.setattr(
notifier_app, "get_secret", lambda sid: seen.setdefault("id", sid) or "xoxb"
)
notifier_app.handler({"version": "2.0.0", "notes": "Big."}, None)
assert seen["id"] == "afterhours-shift-manager/slack-bot-token"
@pytest.mark.parametrize(
"event",
[
{},
{"version": "1.10.0"}, # missing notes
{"notes": "x"}, # missing version
{"version": "", "notes": "x"}, # empty version
],
)
def test_rejects_incomplete_event(notifier_app, slack, env, event):
with pytest.raises(ValueError):
notifier_app.handler(event, None)
slack.chat_postMessage.assert_not_called()