diff --git a/connect/customer/content_matcher.py b/connect/customer/content_matcher.py index 416ba2c..0827b49 100644 --- a/connect/customer/content_matcher.py +++ b/connect/customer/content_matcher.py @@ -14,7 +14,7 @@ def _find_best_matching_content( ) -> _ContentBlock | None: parsed = urllib.parse.urlparse(resource_url) host = parsed.netloc - path = parsed.path + path = parsed.path or "/" if not parsed.scheme or not host: debug_log(debug, f"Cannot parse resource URL: {resource_url}") return None @@ -25,23 +25,30 @@ def _find_best_matching_content( best_specificity = -1 for block in content_blocks: - pattern = urllib.parse.urlparse(block.url_pattern) - if not pattern.scheme or not pattern.netloc: - debug_log(debug, f"Skipping block with invalid URL pattern: {block.url_pattern}") - continue - - if pattern.netloc != host: - debug_log( - debug, - f"Skipping block: host mismatch (pattern={pattern.netloc}, resource={host})", - ) - continue - - if pattern.path == path: + is_path_only = block.url_pattern.startswith("/") + + if is_path_only: + pattern_path = block.url_pattern + else: + pattern = urllib.parse.urlparse(block.url_pattern) + if not pattern.scheme or not pattern.netloc: + debug_log(debug, f"Skipping block with invalid URL pattern: {block.url_pattern}") + continue + + if pattern.netloc != host: + debug_log( + debug, + f"Skipping block: host mismatch (pattern={pattern.netloc}, resource={host})", + ) + continue + + pattern_path = pattern.path or "/" + + if pattern_path == path: debug_log(debug, f"Exact match found: {block.url_pattern}") return block - specificity = _score_path_pattern(pattern.path or "/", path or "/") + specificity = _score_path_pattern(pattern_path, path) if specificity > best_specificity: best_specificity = specificity best_match = block diff --git a/tests/customer/test_content_matcher.py b/tests/customer/test_content_matcher.py index a38772a..8a1bf5e 100644 --- a/tests/customer/test_content_matcher.py +++ b/tests/customer/test_content_matcher.py @@ -73,6 +73,68 @@ def test_find_best_matching_content_handles_port_specific_host_matching() -> Non assert _find_best_matching_content(blocks, "http://localhost:3001/page") is None +def test_find_best_matching_content_matches_path_only_pattern() -> None: + """Path-only patterns (starting with /) match regardless of host.""" + blocks = [ + _ContentBlock(url_pattern="/article/*", server="http://127.0.0.1:8787", license_xml=""), + ] + + match = _find_best_matching_content(blocks, "http://127.0.0.1:7676/article/foo") + + assert match is not None + assert match.url_pattern == "/article/*" + + +def test_find_best_matching_content_exact_path_only_wins_over_wildcard() -> None: + """Exact path-only match beats a path-only wildcard.""" + blocks = [ + _ContentBlock(url_pattern="/*", server="http://127.0.0.1:8787", license_xml=""), + _ContentBlock(url_pattern="/article/foo", server="http://127.0.0.1:8787", license_xml=""), + ] + + match = _find_best_matching_content(blocks, "http://127.0.0.1:7676/article/foo") + + assert match is not None + assert match.url_pattern == "/article/foo" + + +def test_find_best_matching_content_path_only_matches_any_host() -> None: + """Path-only patterns match any host.""" + blocks = [ + _ContentBlock(url_pattern="/article/*", server="http://127.0.0.1:8787", license_xml=""), + ] + + match = _find_best_matching_content(blocks, "http://totally-different-host.com/article/foo") + + assert match is not None + assert match.url_pattern == "/article/*" + + +def test_find_best_matching_content_mixes_full_url_and_path_only() -> None: + """Path-only pattern with higher specificity wins over full-URL pattern.""" + blocks = [ + _ContentBlock(url_pattern="http://127.0.0.1:7676/*", server="http://127.0.0.1:8787", license_xml=""), + _ContentBlock(url_pattern="/article/*", server="http://127.0.0.1:8787", license_xml=""), + ] + + match = _find_best_matching_content(blocks, "http://127.0.0.1:7676/article/foo") + + assert match is not None + assert match.url_pattern == "/article/*" + + +def test_find_best_matching_content_matches_root_url_without_trailing_slash() -> None: + """A resource URL with no path (e.g. http://host) matches a / pattern.""" + blocks = [ + _ContentBlock(url_pattern="/", server="http://127.0.0.1:8787", license_xml=""), + ] + + match = _find_best_matching_content(blocks, "http://127.0.0.1:7676") + + assert match is not None + assert match.url_pattern == "/" + + def test_find_best_matching_content_returns_none_for_empty_blocks() -> None: """Returns None when no content blocks are provided.""" assert _find_best_matching_content([], "http://example.com/page") is None