Skip to content

Commit ccc9500

Browse files
authored
fix(content-health-monitor): emit friendly error if URL does not contain a GUID (#268)
* Handle trailing slashes in URLs * Improve error handling of the GUID input
1 parent 0f6e82e commit ccc9500

File tree

4 files changed

+125
-9
lines changed

4 files changed

+125
-9
lines changed

extensions/content-health-monitor/content-health-monitor.qmd

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ monitored_content_guid = get_env_var("MONITORED_CONTENT_GUID", state)
3434
3535
# Extract GUID if it's a string or URL containing a GUID
3636
if monitored_content_guid:
37-
monitored_content_guid = extract_guid(monitored_content_guid)
37+
monitored_content_guid, guid_error_message = extract_guid(monitored_content_guid)
38+
# Handle URL with no GUID error
39+
if guid_error_message:
40+
state.show_instructions = True
41+
state.instructions.append(guid_error_message)
3842
3943
# Only instantiate the client if we have the required environment variables
4044
client = None

extensions/content-health-monitor/content_health_utils.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,36 @@ def extract_guid(input_string):
131131
input_string: String that may contain a GUID
132132
133133
Returns:
134-
str: Extracted GUID or original string if no GUID found
134+
tuple: (extracted_guid, error_message)
135+
- extracted_guid: The extracted GUID or original string if no GUID found
136+
- error_message: Error message if the input doesn't contain a valid GUID, None otherwise
135137
"""
136138
# Match UUIDs in various formats that might appear in URLs
137139
guid_pattern = re.compile(r'[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}')
138140

139141
match = guid_pattern.search(input_string)
140142
if match:
141-
return match.group(0)
143+
return match.group(0), None
142144

143-
# Return original string if no GUID found
144-
return input_string
145+
# Check if the input looks like a URL but doesn't contain a GUID
146+
url_pattern = re.compile(r'^https?://', re.IGNORECASE)
147+
if url_pattern.match(input_string):
148+
error_message = (
149+
f"The URL provided in <code>MONITORED_CONTENT_GUID</code> does not contain a valid GUID. "
150+
f"The URL should contain a GUID like: <code>1d97c1ff-e56c-4074-906f-cb3557685b75</code><br><br>"
151+
f"The URL provided is: <a href=\"{input_string}\" target=\"_blank\" rel=\"noopener noreferrer\">{input_string}</a><br><br>"
152+
f"Please update your environment variable with a valid GUID or a URL containing a GUID."
153+
)
154+
return input_string, error_message
155+
156+
# Handle non-URL strings that don't match GUID format
157+
error_message = (
158+
f"The value provided in <code>MONITORED_CONTENT_GUID</code> is not a valid GUID. "
159+
f"A valid GUID looks like: <code>1d97c1ff-e56c-4074-906f-cb3557685b75</code><br><br>"
160+
f"The provided value was: <code>{input_string}</code><br><br>"
161+
f"Please update your environment variable with a valid GUID or a URL containing a GUID."
162+
)
163+
return input_string, error_message
145164

146165
# Function to get content details from Connect API
147166
def get_content(client, guid):
@@ -259,7 +278,8 @@ def validate(client, guid, connect_server, api_key):
259278
try:
260279
# Use the content_url if available
261280
if not content_url:
262-
content_url = f"{connect_server}/content/{guid}"
281+
base_url = connect_server.rstrip('/')
282+
content_url = f"{base_url}/content/{guid}"
263283

264284
content_response = requests.get(
265285
content_url,

extensions/content-health-monitor/manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"version": "3.11.7",
3333
"package_manager": {
3434
"name": "pip",
35-
"version": "24.2",
35+
"version": "25.1.1",
3636
"package_file": "requirements.txt"
3737
}
3838
},
@@ -41,10 +41,10 @@
4141
"checksum": "5f89d52674b219c0b0ed85f1a5785641"
4242
},
4343
"content-health-monitor.qmd": {
44-
"checksum": "b9d7b54bd001f48f8af928b26be91a8f"
44+
"checksum": "db9599dc24337de8c0becbcefb203371"
4545
},
4646
"content_health_utils.py": {
47-
"checksum": "3d599ebe8d29aa12ae630acc847e290a"
47+
"checksum": "3fc5386653742d8bf625686e486466ea"
4848
},
4949
"images/refresh-report.png": {
5050
"checksum": "e5680e6188eb8d659e4313cb89d0be3b"

extensions/content-health-monitor/test_content_health_utils.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# Functions from content_health_utils
2222
check_server_reachable = content_health_utils.check_server_reachable
2323
extract_error_details = content_health_utils.extract_error_details
24+
extract_guid = content_health_utils.extract_guid
2425
format_error_message = content_health_utils.format_error_message
2526
get_content = content_health_utils.get_content
2627
get_env_var = content_health_utils.get_env_var
@@ -250,6 +251,67 @@ def test_format_error_message_client_error_invalid_json(self):
250251
assert result == error_text
251252

252253

254+
# Tests for extract_guid function
255+
class TestExtractGuid:
256+
257+
def test_extract_guid_with_valid_guid(self):
258+
"""Test extract_guid with a valid GUID string"""
259+
# Setup - Valid GUID string
260+
input_string = "1d97c1ff-e56c-4074-906f-cb3557685b75"
261+
262+
# Execute
263+
result, error_message = extract_guid(input_string)
264+
265+
# Assert
266+
assert result == input_string
267+
assert error_message is None
268+
269+
def test_extract_guid_with_valid_guid_in_url(self):
270+
"""Test extract_guid with a URL containing a valid GUID"""
271+
# Setup - URL with GUID
272+
guid = "1d97c1ff-e56c-4074-906f-cb3557685b75"
273+
input_string = f"https://connect.example.com/content/{guid}/"
274+
275+
# Execute
276+
result, error_message = extract_guid(input_string)
277+
278+
# Assert
279+
assert result == guid
280+
assert error_message is None
281+
282+
def test_extract_guid_with_url_no_guid(self):
283+
"""Test extract_guid with a URL that does not contain a GUID"""
284+
# Setup - URL without GUID
285+
input_string = "https://connect.example.com/content/dashboard"
286+
287+
# Execute
288+
result, error_message = extract_guid(input_string)
289+
290+
# Assert
291+
assert result == input_string
292+
assert error_message is not None
293+
assert "The URL provided in <code>MONITORED_CONTENT_GUID</code> does not contain a valid GUID" in error_message
294+
assert "The URL should contain a GUID like: <code>1d97c1ff-e56c-4074-906f-cb3557685b75</code>" in error_message
295+
assert f"<a href=\"{input_string}\" target=\"_blank\" rel=\"noopener noreferrer\">" in error_message
296+
assert "Please update your environment variable with a valid GUID or a URL containing a GUID" in error_message
297+
298+
def test_extract_guid_with_plain_text(self):
299+
"""Test extract_guid with plain text that is not a URL and does not contain a GUID"""
300+
# Setup - Plain text input
301+
input_string = "some-content-name"
302+
303+
# Execute
304+
result, error_message = extract_guid(input_string)
305+
306+
# Assert
307+
assert result == input_string
308+
assert error_message is not None
309+
assert "The value provided in <code>MONITORED_CONTENT_GUID</code> is not a valid GUID" in error_message
310+
assert "A valid GUID looks like: <code>1d97c1ff-e56c-4074-906f-cb3557685b75</code>" in error_message
311+
assert f"The provided value was: <code>{input_string}</code>" in error_message
312+
assert "Please update your environment variable with a valid GUID or a URL containing a GUID" in error_message
313+
314+
253315
# Tests for get_content and get_user functions
254316
class TestContentAndUserFunctions:
255317

@@ -488,6 +550,36 @@ def test_validate_missing_content_url(self, mock_client, valid_content_response,
488550
mock_get.assert_called_once()
489551
args, kwargs = mock_get.call_args
490552
assert args[0] == f"{connect_test_server}/content/{guid}"
553+
554+
def test_validate_missing_content_url_with_trailing_slash(self, mock_client, valid_content_response, valid_user_response,
555+
mock_response, api_test_key):
556+
"""Test validate when content_url is missing and connect_server has a trailing slash"""
557+
# Setup - content details without content_url
558+
guid = valid_content_response["guid"]
559+
content_without_url = valid_content_response.copy()
560+
del content_without_url["content_url"]
561+
mock_client.content.get.return_value = content_without_url
562+
563+
# Setup - owner details
564+
mock_client.users.get.return_value = valid_user_response
565+
566+
# Setup - connect_server with trailing slash
567+
connect_server_with_trailing_slash = "https://connect.example.com/"
568+
569+
# Setup - HTTP request
570+
with patch('requests.get', return_value=mock_response) as mock_get:
571+
# Execute
572+
result = validate(mock_client, guid, connect_server_with_trailing_slash, api_test_key)
573+
574+
# Assert
575+
assert result["guid"] == guid
576+
assert result["status"] == STATUS_PASS
577+
578+
# Verify HTTP request was made with correctly constructed URL - no double slash
579+
mock_get.assert_called_once()
580+
args, kwargs = mock_get.call_args
581+
assert args[0] == "https://connect.example.com/content/{0}".format(guid)
582+
assert "//" not in args[0].replace("https://", "")
491583

492584
def test_validate_no_title_fallback_to_name(self, mock_client, valid_user_response, mock_response,
493585
connect_test_server, api_test_key):

0 commit comments

Comments
 (0)