Skip to content

Commit b21a77b

Browse files
gcharestCopilot
andauthored
Feat/enhance sns payload processing (#952)
* feat: isolate AWS SNS payload processing * refactor: update AWS SNS payload tests to use aws_sns module * fix: rename test modules to match target modules * fix: correct import path for AWS SNS payload processing * feat: isolate each SNS notification func into pattern handlers * test: enhance validation tests for AwsNotificationPattern * feat: add validation for Slack block structures and enhance error handling in AWS SNS notification processing * feat: add Slack Block Kit utilities and validation * fix: remove init file from tests directory * fix: reorder import statements in test_main.py * Update app/tests/modules/webhooks/patterns/aws_sns_notifications/test_budget_notification.py Co-authored-by: Copilot <[email protected]> * Update app/tests/modules/webhooks/patterns/aws_sns_notifications/test_abuse_notification.py Co-authored-by: Copilot <[email protected]> * fix: enhance validation for section and header blocks * fix: add logging for skipped revoke_api_key in non-production environments --------- Co-authored-by: Copilot <[email protected]>
1 parent 4ec0753 commit b21a77b

29 files changed

+2804
-1037
lines changed

app/integrations/notify/client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ def post_event(url, payload):
9898
def revoke_api_key(api_key, api_type, github_repo, source):
9999
"""Function to revoke an api key by calling Notify's revoke api endpoint"""
100100
# get the url and jwt_token
101+
102+
if not settings.is_production:
103+
logger.info("revoke_api_key_skipped", api_key=api_key)
104+
return False
101105
url = NOTIFY_API_URL
102106

103107
if url is None:

app/integrations/slack/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
- channels: Module containing the channel related functionality for the Slack integration.
66
- users: Module containing the user related functionality for the Slack integration.
77
- commands: Module containing the command related functionality for the Slack integration.
8+
- blocks: Module containing Slack Block Kit utilities and validation.
89
"""

app/integrations/slack/blocks.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
"""Slack Block Kit utilities and validation.
2+
3+
This module provides utilities for working with Slack Block Kit elements,
4+
including validation and construction helpers.
5+
"""
6+
7+
from typing import Dict, List
8+
9+
10+
def validate_blocks(blocks: List[Dict]) -> bool:
11+
"""
12+
Validate that the provided blocks are valid Slack Block Kit structures.
13+
14+
This performs basic structural validation to catch common errors before
15+
sending blocks to the Slack API. It's not exhaustive but covers the most
16+
common block types and their required fields.
17+
18+
Args:
19+
blocks: List of Slack block dictionaries to validate
20+
21+
Returns:
22+
bool: True if blocks are structurally valid, False otherwise
23+
24+
Examples:
25+
>>> blocks = [
26+
... {"type": "section", "text": {"type": "mrkdwn", "text": "Hello"}},
27+
... {"type": "divider"}
28+
... ]
29+
>>> validate_blocks(blocks)
30+
True
31+
32+
>>> invalid_blocks = [{"text": "missing type"}]
33+
>>> validate_blocks(invalid_blocks)
34+
False
35+
"""
36+
if not isinstance(blocks, list):
37+
return False
38+
39+
for block in blocks:
40+
if not isinstance(block, dict):
41+
return False
42+
43+
if "type" not in block:
44+
return False
45+
46+
# Basic validation for common block types
47+
block_type = block.get("type")
48+
49+
# Section blocks require text OR fields
50+
if block_type == "section" and "text" not in block and "fields" not in block:
51+
return False
52+
53+
# Header blocks require text
54+
if block_type == "header" and "text" not in block:
55+
return False
56+
57+
# Divider blocks should be minimal
58+
if block_type == "divider" and len(block) > 1:
59+
return False
60+
61+
# Actions blocks require elements
62+
if block_type == "actions" and "elements" not in block:
63+
return False
64+
65+
# Context blocks require elements
66+
if block_type == "context" and "elements" not in block:
67+
return False
68+
69+
return True
70+
71+
72+
def create_section_block(text: str, text_type: str = "mrkdwn") -> Dict:
73+
"""
74+
Create a section block with the given text.
75+
76+
Args:
77+
text: The text content for the section
78+
text_type: The text type, either 'mrkdwn' or 'plain_text'
79+
80+
Returns:
81+
Dict: A valid Slack section block
82+
"""
83+
return {"type": "section", "text": {"type": text_type, "text": text}}
84+
85+
86+
def create_header_block(text: str) -> Dict:
87+
"""
88+
Create a header block with the given text.
89+
90+
Args:
91+
text: The text content for the header
92+
93+
Returns:
94+
Dict: A valid Slack header block
95+
"""
96+
return {"type": "header", "text": {"type": "plain_text", "text": text}}
97+
98+
99+
def create_divider_block() -> Dict:
100+
"""
101+
Create a divider block.
102+
103+
Returns:
104+
Dict: A valid Slack divider block
105+
"""
106+
return {"type": "divider"}
107+
108+
109+
def create_context_block(elements: List[Dict]) -> Dict:
110+
"""
111+
Create a context block with the given elements.
112+
113+
Args:
114+
elements: List of text or image elements for the context
115+
116+
Returns:
117+
Dict: A valid Slack context block
118+
"""
119+
return {"type": "context", "elements": elements}

0 commit comments

Comments
 (0)