From 2158f71fd0e71b04998619055fe66af9cb204784 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:10:06 +0000 Subject: [PATCH] feat: normalize URLs without scheme to https:// MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add normalize_url() helper function in commands.py - Automatically prefix URLs without scheme (e.g. github.com → https://github.com) - Applies to both -link flag and auto-detected URLs - Add 5 new tests for URL normalization - Fix existing tests to handle 5-value return from parse_args() Examples: /add Fix bug github.com/user/repo → stored as: https://github.com/user/repo --- apps/telegram-bot/commands.py | 13 ++++++- apps/telegram-bot/tests/test_commands.py | 48 +++++++++++++++++------- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/apps/telegram-bot/commands.py b/apps/telegram-bot/commands.py index 0870d3a..055cfc2 100644 --- a/apps/telegram-bot/commands.py +++ b/apps/telegram-bot/commands.py @@ -81,6 +81,15 @@ def parse_args( return True return False + def normalize_url(url: str) -> str: + """Normalize URL by adding https:// prefix if missing.""" + if not url: + return url + if url.startswith("http://") or url.startswith("https://"): + return url + # Add https:// for URLs without scheme + return f"https://{url}" + def is_time(s: str) -> bool: if not s or ":" not in s: return False @@ -106,7 +115,7 @@ def parse_args( if arg == "-link": if i + 1 < len(args) and not args[i + 1].startswith("-"): - link = args[i + 1] + link = normalize_url(args[i + 1]) i += 2 else: clear_link = True @@ -129,7 +138,7 @@ def parse_args( clear_date = True i += 1 elif not link and is_url(arg): - link = arg + link = normalize_url(arg) i += 1 elif due_date_ts is None: due_date_ts = parse_date_with_tz(arg) diff --git a/apps/telegram-bot/tests/test_commands.py b/apps/telegram-bot/tests/test_commands.py index 2898c31..2b8e469 100644 --- a/apps/telegram-bot/tests/test_commands.py +++ b/apps/telegram-bot/tests/test_commands.py @@ -52,75 +52,97 @@ class TestExtractArgs: class TestParseArgs: def test_text_only(self): - text, link, due = parse_args(["hello", "world"]) + text, link, due, _, _ = parse_args(["hello", "world"]) assert text == "hello world" assert link is None assert due is None def test_link_extracted(self): - text, link, due = parse_args(["hello", "https://example.com"]) + text, link, due, _, _ = parse_args(["hello", "https://example.com"]) # "hello" is non-link non-date → becomes text; only the URL becomes link assert text == "hello" assert link == "https://example.com" assert due is None def test_text_and_link(self): - text, link, due = parse_args(["hello", "world", "https://example.com"]) + text, link, due, _, _ = parse_args(["hello", "world", "https://example.com"]) assert text == "hello world" assert link == "https://example.com" def test_due_date_parsed(self): - text, link, due = parse_args(["hello", "tomorrow"]) + text, link, due, _, _ = parse_args(["hello", "tomorrow"]) assert text == "hello" assert due is not None # Should be some time in the future assert due > int(time.time()) def test_all_three(self): - text, link, due = parse_args(["hello", "https://example.com", "tomorrow"]) + text, link, due, _, _ = parse_args(["hello", "https://example.com", "tomorrow"]) assert text == "hello" assert link == "https://example.com" assert due is not None def test_http_and_https_both_detected(self): - _, link1, _ = parse_args(["http://example.com"]) - _, link2, _ = parse_args(["https://example.com"]) + _, link1, _, _, _ = parse_args(["http://example.com"]) + _, link2, _, _, _ = parse_args(["https://example.com"]) assert link1 == "http://example.com" assert link2 == "https://example.com" def test_non_url_non_date_becomes_text(self): - text, link, due = parse_args(["fix", "the", "bug"]) + text, link, due, _, _ = parse_args(["fix", "the", "bug"]) assert text == "fix the bug" assert link is None assert due is None def test_multiple_links_first_only(self): - _, link, _ = parse_args(["text", "https://first.com", "https://second.com"]) + _, link, _, _, _ = parse_args(["text", "https://first.com", "https://second.com"]) assert link == "https://first.com" def test_due_date_after_link(self): - text, link, due = parse_args(["task", "https://example.com", "in 5 days"]) + text, link, due, _, _ = parse_args(["task", "https://example.com", "in 5 days"]) assert text == "task" assert link == "https://example.com" assert due is not None def test_empty_args(self): - text, link, due = parse_args([]) + text, link, due, _, _ = parse_args([]) assert text is None assert link is None assert due is None def test_date_parser_failure_returns_none(self): # "asdfjkl" is not parseable → goes to text - text, link, due = parse_args(["hello", "asdfjkl"]) + text, link, due, _, _ = parse_args(["hello", "asdfjkl"]) assert text == "hello asdfjkl" assert due is None def test_link_takes_first_match(self): # Even if it's not a valid URL, starts with https:// - _, link, _ = parse_args(["skip", "https://not-real.but-still-a-link"]) + _, link, _, _, _ = parse_args(["skip", "https://not-real.but-still-a-link"]) assert link == "https://not-real.but-still-a-link" + def test_url_without_scheme_normalized_to_https(self): + """URLs without scheme should get https:// prefix.""" + _, link, _, _, _ = parse_args(["github.com/user/repo"]) + assert link == "https://github.com/user/repo" + + def test_url_without_scheme_github_normalized(self): + text, link, _, _, _ = parse_args(["Fix bug", "github.com/owner/repo"]) + assert text == "Fix bug" + assert link == "https://github.com/owner/repo" + + def test_url_with_explicit_https_unchanged(self): + _, link, _, _, _ = parse_args(["task", "https://example.com/page"]) + assert link == "https://example.com/page" + + def test_url_with_http_unchanged(self): + _, link, _, _, _ = parse_args(["task", "http://example.com/page"]) + assert link == "http://example.com/page" + + def test_url_link_flag_without_scheme_normalized(self): + _, link, _, _, _ = parse_args(["-link", "example.com/path"]) + assert link == "https://example.com/path" + class TestFormatBounty: def _row(