From 9cfde7b250dd6f95130276197f93c043d7e617a8 Mon Sep 17 00:00:00 2001 From: Zdenek Styblik Date: Thu, 14 Mar 2024 14:12:37 +0100 Subject: [PATCH] Initial commit --- .github/workflows/ii_wrapper.yml | 31 +++ .gitignore | 2 + ci/run-black.sh | 23 ++ ci/run-flake8.sh | 13 + ci/run-reorder-python-imports.sh | 5 + iibot-ng | 154 +++++++++++ iicmd.py | 212 +++++++++++++++ requirements-ci.txt | 10 + requirements.txt | 1 + tests/conftest.py | 13 + tests/files/fake_fortune.sh | 2 + tests/files/fake_fortune_error.sh | 4 + tests/test_iicmd.py | 433 ++++++++++++++++++++++++++++++ 13 files changed, 903 insertions(+) create mode 100644 .github/workflows/ii_wrapper.yml create mode 100644 .gitignore create mode 100755 ci/run-black.sh create mode 100755 ci/run-flake8.sh create mode 100755 ci/run-reorder-python-imports.sh create mode 100755 iibot-ng create mode 100755 iicmd.py create mode 100644 requirements-ci.txt create mode 100644 requirements.txt create mode 100644 tests/conftest.py create mode 100755 tests/files/fake_fortune.sh create mode 100755 tests/files/fake_fortune_error.sh create mode 100644 tests/test_iicmd.py diff --git a/.github/workflows/ii_wrapper.yml b/.github/workflows/ii_wrapper.yml new file mode 100644 index 0000000..efd6dfc --- /dev/null +++ b/.github/workflows/ii_wrapper.yml @@ -0,0 +1,31 @@ +name: ii-wrapper workflow + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.11 + uses: actions/setup-python@v1 + with: + python-version: 3.11 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-ci.txt + - name: Reorder Python imports + run: | + ./ci/run-reorder-python-imports.sh + - name: Lint with flake8 + run: | + ./ci/run-flake8.sh + - name: Lint with black + run: | + ./ci/run-black.sh check || ( ./ci/run-black.sh diff; exit 1 ) + - name: Test with pytest + run: | + python -m pytest . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a295864 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +__pycache__ diff --git a/ci/run-black.sh b/ci/run-black.sh new file mode 100755 index 0000000..7a54871 --- /dev/null +++ b/ci/run-black.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -e +set -u + + +MODE=${1:?Mode must be given.} + +if [ "${MODE}" == "check" ]; then + black_arg=" --check" +elif [ "${MODE}" == "diff" ]; then + black_arg=" --diff" +elif [ "${MODE}" == "format" ]; then + black_arg="" +else + printf "Mode '%s' is not supported.\n" "${MODE}" 1>&2 + exit 1 +fi + +python3 \ + -m black \ + ${black_arg} \ + -l 80 \ + `find . ! -path '*/\.*' -name '*.py'` diff --git a/ci/run-flake8.sh b/ci/run-flake8.sh new file mode 100755 index 0000000..708c225 --- /dev/null +++ b/ci/run-flake8.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e +set -u + +python3 -m flake8 \ + . \ + --ignore=W503 \ + --application-import-names="app,settings" \ + --import-order-style=pycharm \ + --max-line-length=80 \ + --show-source \ + --count \ + --statistics diff --git a/ci/run-reorder-python-imports.sh b/ci/run-reorder-python-imports.sh new file mode 100755 index 0000000..4fad265 --- /dev/null +++ b/ci/run-reorder-python-imports.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -e +set -u + +reorder-python-imports `find . ! -path '*/\.*' -name '*.py'` diff --git a/iibot-ng b/iibot-ng new file mode 100755 index 0000000..7ad448b --- /dev/null +++ b/iibot-ng @@ -0,0 +1,154 @@ +#!/bin/sh +# Desc: Wrapper for iibot based on script by c00kiemon5ter +# +# Example of .cfg file: +# ~~~ +# net:irc.ssh.cz:#chan1 #chan2 +# nickname:testme +# ircdir:$HOME/ii/ +# ~~~ +# +# 2017/Apr/08 @ Zdenek Styblik +set -e +set -u + +monitor() +{ + local iipid="${1}" + tail -f -n1 --pid=${iipid} "${ircdir}/${network}/${channel}/out" | \ + while read -r nixtime nick msg; do + # if msg is by the system ignore it + if [ "$nick" = '-!-' ]; then + continue + fi + # strip < and >. if msg is by ourself ignore it + nick=$(printf -- "${nick}" | sed -e 's@^<@@' | sed -e 's@>$@@') + if [ "${nick}" = "${nickname}" ]; then + continue + fi + + # if msg contains a url, transform to url command + if printf -- "%s" "${msg}" | grep -q -E -e 'https?://' ; then + exec "${ircdir}/iicmd.py" \ + --nick "${nick}" \ + --message "url ${msg#* }" \ + --ircd "${ircdir}" \ + --network "${network}" \ + --channel "${channel}" \ + --self "${nickname}" | fold -w 255 & + continue + fi + # if msg is a command, invoke iicmd + if printf -- "%s" "${msg}" | grep -q -E -e '^!' ; then + exec "${ircdir}/iicmd.py" \ + --nick "${nick}" \ + --message "${msg#\!}" \ + --ircd "${ircdir}" \ + --network "${network}" \ + --channel "${channel}" \ + --self "${nickname}" | fold -w 255 & + continue + fi + done > "${ircdir}/${network}/${channel}/in" +} + +monitor_link() +{ + local iipid=${1} + IFS=' +' + tail -f -n1 --pid=${iipid} "${ircdir}/${network}/out" | \ + while read -r response; do + if printf -- "%s" "${response}" | grep -q -i -e 'Closing Link' -E -e "${nickname}.*ping timeout"; then + printf "Killing bot.\n" 1>&2 + kill "${iipid}" + break + fi + done +} + +remove_lock() +{ + if [ -n "${pids}" ]; then + printf -- "${pids}" | xargs kill || true + fi + rmdir "${LOCK_DIR}" +} + +IRC_CONFIG=${1:?Supply name of IRC config to use.} +IRC_CONFIG_NAME=$(basename -- "${IRC_CONFIG}") +LOCK_DIR="/tmp/${IRC_CONFIG_NAME}.lock" + +if [ ! -r "${IRC_CONFIG}" ]; then + printf "Config file '%s' doesn't exist or is not readable.\n" \ + ${IRC_CONFIG} 1>&2 + exit 2 +fi + + +ircdir=$(grep -e '^ircdir:' "${IRC_CONFIG}" | cut -d ':' -f 2- | head -n 1) +ircdir=${ircdir:-${HOME}/tmp/ii/ii/} +nickname=$(grep -e '^nickname:' "${IRC_CONFIG}" | awk -F':' '{ print $2 }' |\ + head -n 1) +nickname=${nickname:-"testme"} +net_conf=$(grep -e '^net:' "${IRC_CONFIG}" | sort | uniq | head -n1) +network=$(printf -- "%s" "${net_conf}" | awk -F: '{ print $2 }') +if [ -z "${net_conf}" ] || [ -z "${network}" ]; then + printf "No network configuration has been found in '%s'.\n" \ + "${IRC_CONFIG}" 1>&2 + exit 1 +fi + +mkdir -p "${LOCK_DIR}" || \ + ( printf "Failed to create lock for '%s'.\n" "${IRC_CONFIG}" 1>&2; exit 1; ) +trap remove_lock INT QUIT TERM EXIT + +# some privacy please, thanks +chmod 700 "${ircdir}" +chmod 600 "${ircdir}"/*/ident 1> /dev/null 2>&1 || true + +pids="" +# cleanup +rm -f "${ircdir}/${network}/in" +# connect to network - password is set through the env var IIPASS +ii -i "${ircdir}" -n "${nickname}" -s "${network}" \ + -f "${nickname}" >> "${ircdir}/${network}.log" 2>&1 & +pid="$!" + +# wait for the connection +time_slept=0 +while ! test -p "${ircdir}/${network}/in"; do + sleep 1 + time_slept=$((time_slept + 1)) + if [ ${time_slept} -ge 15 ]; then + break + fi +done + +monitor_link "$pid" & +pids=$(printf -- "%s %s" "${pids}" $!) + +# auth to services +if [ -e "${ircdir}/${network}/ident" ]; then + printf -- "/j nickserv identify %s\n" \ + "$(<"${ircdir}/${network}/ident")" > "${ircdir}/${network}/in" +fi +# clean that up - ident passwd is in there +rm -f "${ircdir}/${network}/nickserv/out" + +sleep 3 +# join channels +for channel in $(printf -- "%s" "${net_conf}" | awk -F':' '{ print $3 }'); do + printf -- "/j %s\n" "${channel}" > "${ircdir}/${network}/in" + if [ ! -e "${ircdir}/${network}/${channel}/out" ]; then + touch "${ircdir}/${network}/${channel}/out" + fi + monitor "${pid}" & + pids=$(printf -- "%s %s" "${pids}" $!) +done + +# if connection is lost, die +wait "${pid}" +remove_lock +trap - INT QUIT TERM EXIT +# EOF diff --git a/iicmd.py b/iicmd.py new file mode 100755 index 0000000..fc7c29b --- /dev/null +++ b/iicmd.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Python implementation of commands for iibot. + +2024/Mar/14 @ Zdenek Styblik +""" +import argparse +import json +import logging +import os +import re +import shutil +import subprocess +import sys +import traceback + +import requests + +# List of supported commands and whether command requires user input or not. +COMMANDS = { + "calc": True, + "echo": True, + "fortune": False, + "list": False, + "ping": False, + "slap": False, + "url": True, + "whereami": False, +} + +HTTP_MAX_REDIRECTS = 2 +HTTP_TIMEOUT = 30 # seconds + + +def cmd_fortune(): + """Try to get a fortune cookie.""" + fortune_fpath = shutil.which("fortune", mode=os.F_OK | os.X_OK) + if not fortune_fpath: + print("Damn, I'm out of fortune cookies! :(") + return + + with subprocess.Popen( + [fortune_fpath, "-osea"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) as fortune_proc: + fortune_out, fortune_err = fortune_proc.communicate() + + if fortune_proc.returncode != 0: + logging.error("fortune RC: %s", fortune_proc.returncode) + logging.error("fortune STDOUT: '%s'", fortune_out) + logging.error("fortune STDERR: '%s'", fortune_err) + print("Oh no, I've dropped my fortune cookie! :(") + return + + print("{:s}".format(fortune_out.decode("utf-8").rstrip("\n"))) + + +def get_url_short(url, bitly_gid, bitly_token): + """Convert URL to a shorter one through bit.ly. + + See https://dev.bitly.com/ for API documentation. + """ + short_url = url + try: + headers = { + "Authorization": "Bearer {:s}".format(bitly_token), + "Content-Type": "application/json", + } + data = { + "long_url": url, + "domain": "bit.ly", + "group_guid": bitly_gid, + } + + rsp_short = requests.post( + "https://api-ssl.bitly.com/v4/shorten", + headers=headers, + data=json.dumps(data), + timeout=HTTP_TIMEOUT, + ) + rsp_short.raise_for_status() + if "link" not in rsp_short.json(): + raise KeyError("Expected key 'link' not found in rsp from bit.ly") + + short_url = rsp_short.json()["link"] + except Exception: + # NOTE: this isn't exactly great, but it simplifies the code. + logging.error( + "Failed to get short URL of '%s' due to: %s", + url, + traceback.format_exc(), + ) + + return short_url + + +def get_url_title(url): + """Try to get and return title of given URL.""" + url_title = "No title" + try: + session = requests.Session() + session.max_redirects = HTTP_MAX_REDIRECTS + rsp_title = session.get(url, timeout=HTTP_TIMEOUT) + rsp_title.raise_for_status() + + match = re.search(r"(?P<title>[^<]*)<\/title>", rsp_title.text) + if match: + url_title = match.group("title") + else: + logging.debug("No title for '{:s}'".format(url)) + except Exception: + # NOTE: this isn't exactly great, but it simplifies the code. + # No title then. + logging.error( + "HTTP req to '%s' has failed: %s", url, traceback.format_exc() + ) + + return url_title + + +def cmd_url(extra): + """Process URL and print-out the result.""" + match = re.search(r".*(?P<url>http[^ ]*).*", extra) + if not match: + logging.debug("No URL detected in '%s'", extra) + return + + url = match.group("url") + # Convert YouTube URLs + url = re.sub( + r".*youtube\..*v=([^&]+).*", r"http://youtube.com/embed/\1", url + ) + url = re.sub(r".*youtu\.be/(.+)", r"http://youtube.com/embed/\1", url) + # Try to get URL's title + url_title = get_url_title(url) + bitly_gid = os.getenv("IICMD_BITLY_GID", None) + bitly_token = os.getenv("IICMD_BITLY_TOKEN", None) + if len(url) > 80 and bitly_gid and bitly_token: + url = get_url_short(url, bitly_gid, bitly_token) + + print("Title for {:s} - {:s}".format(url, url_title)) + + +def main(): + """Run iibot command.""" + logging.basicConfig(stream=sys.stderr, encoding="utf-8") + args = parse_args() + + cmd = args.message.split(" ")[0] + extra = " ".join(args.message.split(" ")[1:]) + # Strip leading/trailing whitespace and check, if we have any "extra" left + # TODO: what about newlines? + extra = extra.strip(" ") + if not extra and cmd in COMMANDS and COMMANDS[cmd] is True: + cmd = "invalid" + + if cmd == "list": + print( + "{:s}: supported commands are - {:s}".format( + args.nick, ", ".join(sorted(list(COMMANDS.keys()))) + ) + ) + elif cmd == "calc": + # TODO: this will be big pain and huge amount of LOC to implement + # See https://stackoverflow.com/a/11952343 + print("{:s}: my ALU is b0rked - does not compute.".format(args.nick)) + elif cmd == "echo": + print("{:s}".format(extra.lstrip("/"))) + elif cmd == "fortune": + cmd_fortune() + elif cmd == "ping": + print("{:s}: pong! Ping-pong, get it?".format(args.nick)) + elif cmd == "slap": + print("{:s}: I'll slap your butt!".format(args.nick)) + elif cmd == "url": + cmd_url(extra) + elif cmd == "whereami": + print("{:s}: this! is!! {:s}!!!".format(args.nick, args.channel)) + else: + print( + "{:s}: what are you on about? Me not understanding.".format( + args.nick + ) + ) + + +def parse_args(): + """Return parsed CLI args.""" + parser = argparse.ArgumentParser() + parser.add_argument("--nick", type=str, required=True) + parser.add_argument("--message", type=str, required=True) + parser.add_argument("--ircd", type=str, required=True) + parser.add_argument("--network", type=str, required=True) + parser.add_argument("--channel", type=str, required=True) + parser.add_argument("--self", type=str, required=True) + args = parser.parse_args() + + if not args.nick: + args.nick = "unknown.stranger" + + if not args.ircd: + raise argparse.ArgumentError("Argument 'ircd' must not be empty") + + if not args.network: + raise argparse.ArgumentError("Argument 'network' must not be empty") + + if not args.channel: + raise argparse.ArgumentError("Argument 'channel' must not be empty") + + return args + + +if __name__ == "__main__": + main() diff --git a/requirements-ci.txt b/requirements-ci.txt new file mode 100644 index 0000000..0738650 --- /dev/null +++ b/requirements-ci.txt @@ -0,0 +1,10 @@ +-r requirements.txt +# lint +black==23.12.1 +flake8 +flake8-docstrings +flake8-import-order +reorder-python-imports +# tests +pytest +requests-mock==1.11.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2c24336 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests==2.31.0 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4bd0770 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +"""Conftest for pytest tests.""" +import pytest +import requests_mock + + +@pytest.fixture +def fixture_mock_requests(): + """Return started up requests_mock and cleanup on teardown.""" + mock_requests = requests_mock.Mocker(real_http=True) + mock_requests.start() + yield mock_requests + + mock_requests.stop() diff --git a/tests/files/fake_fortune.sh b/tests/files/fake_fortune.sh new file mode 100755 index 0000000..189604f --- /dev/null +++ b/tests/files/fake_fortune.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +printf "This is a fake fortune cookie\n" diff --git a/tests/files/fake_fortune_error.sh b/tests/files/fake_fortune_error.sh new file mode 100755 index 0000000..8a08325 --- /dev/null +++ b/tests/files/fake_fortune_error.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +printf "Unfortunate fate.\n" +printf "Something went wrong.\n" 1>&2 +exit 1 diff --git a/tests/test_iicmd.py b/tests/test_iicmd.py new file mode 100644 index 0000000..4a458f0 --- /dev/null +++ b/tests/test_iicmd.py @@ -0,0 +1,433 @@ +"""Unit tests for iicmd.py.""" +import os +import sys +from unittest.mock import patch + +import pytest + +import iicmd # noqa:I202 + +SCRIPT_PATH = os.path.dirname(os.path.realpath(__file__)) + + +@pytest.mark.parametrize( + "msg,expected", + [ + # Expected invocation + ( + "calc 1+1", + "irc_user: my ALU is b0rked - does not compute.\n", + ), + # Missing formula which is required arg. + ( + "calc", + "irc_user: what are you on about? Me not understanding.\n", + ), + # Expected invocation + ( + "echo hello there", + "hello there\n", + ), + # Leading "/" should be stripped. + ( + "echo /hello there", + "hello there\n", + ), + ( + "echo //hello there", + "hello there\n", + ), + # Expected invocation + ( + "list", + ( + "irc_user: supported commands are - calc, echo, fortune, " + "list, ping, slap, url, whereami\n" + ), + ), + # Extra args should be ignored. + ( + "list abc efg", + ( + "irc_user: supported commands are - calc, echo, fortune, " + "list, ping, slap, url, whereami\n" + ), + ), + # Expected invocation + ( + "ping", + "irc_user: pong! Ping-pong, get it?\n", + ), + # Extra args should be ignored. + ( + "ping abc123", + "irc_user: pong! Ping-pong, get it?\n", + ), + # Expected invocation + ( + "slap", + "irc_user: I'll slap your butt!\n", + ), + # Extra args should be ignored. + ( + "slap foo", + "irc_user: I'll slap your butt!\n", + ), + # Expected invocation + ( + "whereami", + "irc_user: this! is!! irc_channel!!!\n", + ), + # Extra args should be ignored. + ( + "whereami WHERE?!", + "irc_user: this! is!! irc_channel!!!\n", + ), + ( + "invalid_command", + "irc_user: what are you on about? Me not understanding.\n", + ), + ( + "invalid_command and some message on the top", + "irc_user: what are you on about? Me not understanding.\n", + ), + ], +) +def test_commands(msg, expected, capsys): + """Test commands which don't require anything special and work OOTB.""" + args = [ + "./iicmd.py", + "--nick=irc_user", + "--message={:s}".format(msg), + "--ircd=irc_ircd", + "--network=irc_network", + "--channel=irc_channel", + "--self=irc_botuser", + ] + with patch.object(sys, "argv", args): + iicmd.main() + + captured = capsys.readouterr() + assert captured.out == expected + assert captured.err == "" + + +@pytest.mark.parametrize( + "msg", + [ + "fortune", + # Extra args should be ignored. + "fortune abc123", + ], +) +@patch("shutil.which") +def test_cmd_fortune(mock_shutil_which, msg, capsys): + """Test cmd_fortune() under ideal conditions.""" + expected = "This is a fake fortune cookie\n" + + fake_fortune = os.path.join(SCRIPT_PATH, "files", "fake_fortune.sh") + mock_shutil_which.return_value = fake_fortune + + args = [ + "./iicmd.py", + "--nick=irc_user", + "--message={:s}".format(msg), + "--ircd=irc_ircd", + "--network=irc_network", + "--channel=irc_channel", + "--self=irc_botuser", + ] + with patch.object(sys, "argv", args): + iicmd.main() + + captured = capsys.readouterr() + assert captured.out == expected + assert captured.err == "" + + +@patch("shutil.which") +def test_cmd_fortune_not_present(mock_shutil_which, capsys): + """Test case when fortune is not present on the system.""" + expected = "Damn, I'm out of fortune cookies! :(\n" + mock_shutil_which.return_value = None + + args = [ + "./iicmd.py", + "--nick=irc_user", + "--message=fortune", + "--ircd=irc_ircd", + "--network=irc_network", + "--channel=irc_channel", + "--self=irc_botuser", + ] + with patch.object(sys, "argv", args): + iicmd.main() + + captured = capsys.readouterr() + assert captured.out == expected + assert captured.err == "" + + +@patch("shutil.which") +def test_cmd_fortune_retcode(mock_shutil_which, capsys, caplog): + """Test case when fortune RC != 0.""" + expected = "Oh no, I've dropped my fortune cookie! :(\n" + # NOTE: I don't want to import logging. + expected_log_tuples = [ + ("root", 40, "fortune RC: 1"), + ("root", 40, "fortune STDOUT: 'b'Unfortunate fate.\\n''"), + ("root", 40, "fortune STDERR: 'b'Something went wrong.\\n''"), + ] + + fake_fortune = os.path.join(SCRIPT_PATH, "files", "fake_fortune_error.sh") + mock_shutil_which.return_value = fake_fortune + + args = [ + "./iicmd.py", + "--nick=irc_user", + "--message=fortune", + "--ircd=irc_ircd", + "--network=irc_network", + "--channel=irc_channel", + "--self=irc_botuser", + ] + with patch.object(sys, "argv", args): + iicmd.main() + + captured = capsys.readouterr() + assert captured.out == expected + assert captured.err == "" + assert caplog.record_tuples == expected_log_tuples + + +def test_cmd_url_no_url_in_message(capsys): + """Test case when there is no HTTP(S) to be matched in the message.""" + args = [ + "./iicmd.py", + "--nick=irc_user", + "--message=url foo bar lar mar test message", + "--ircd=irc_ircd", + "--network=irc_network", + "--channel=irc_channel", + "--self=irc_botuser", + ] + with patch.object(sys, "argv", args): + iicmd.main() + + captured = capsys.readouterr() + assert captured.out == "" + assert captured.err == "" + + +@pytest.mark.parametrize( + "url,expected_url", + [ + ( + "https://www.youtube.com/watch?v=9G-fg6G738c", + "http://youtube.com/embed/9G-fg6G738c", + ), + ( + "https://youtu.be/9G-fg6G738c?si=KD5OpJ_F2yTPIK4E", + "http://youtube.com/embed/9G-fg6G738c?si=KD5OpJ_F2yTPIK4E", + ), + ], +) +def test_cmd_url_youtube_link_conversion( + url, expected_url, capsys, fixture_mock_requests +): + """Test conversion of YouTube links in cmd_url().""" + expected_msg = "Title for {:s} - No title\n".format(expected_url) + + rsp_text = "No title here, just little <title>" + mock_http_url = fixture_mock_requests.get(expected_url, text=rsp_text) + + args = [ + "./iicmd.py", + "--nick=irc_user", + "--message=url foo bar {:s} message".format(url), + "--ircd=irc_ircd", + "--network=irc_network", + "--channel=irc_channel", + "--self=irc_botuser", + ] + with patch.object(sys, "argv", args): + iicmd.main() + + captured = capsys.readouterr() + assert captured.out == expected_msg + assert captured.err == "" + + assert mock_http_url.called is True + + +@pytest.mark.parametrize( + "rsp_text,expected_title", + [ + # No title + ( + "No title here, just little <title>", + "No title", + ), + # Test title + ( + ( + "<html><head><title>Little title" + "" + ), + "Little title", + ), + ], +) +def test_cmd_url_title(rsp_text, expected_title, capsys, fixture_mock_requests): + """Test fetching of title in cmd_url().""" + url = "https://www.example.org" + expected_msg = "Title for {:s} - {:s}\n".format(url, expected_title) + + mock_http_url = fixture_mock_requests.get(url, text=rsp_text) + + args = [ + "./iicmd.py", + "--nick=irc_user", + "--message=url foo bar {:s} message".format(url), + "--ircd=irc_ircd", + "--network=irc_network", + "--channel=irc_channel", + "--self=irc_botuser", + ] + with patch.object(sys, "argv", args): + iicmd.main() + + captured = capsys.readouterr() + assert captured.out == expected_msg + assert captured.err == "" + + assert mock_http_url.called is True + + +@pytest.mark.parametrize( + "url,bitly_token,bitly_gid,bitly_rsp,bitly_called", + [ + # URL is too short -> bit.ly shouldn't be called + ( + "http://www.example.org", + "token", + "gid", + "", + False, + ), + # bit.ly should be called, but GID is not set + ( + ( + "http://www.example.org/uGaiXaGh9aiz2kaejeiW0quahtheiwaiveenge" + "Ghook2aeW6suk4phooc8PaiwooCeT1aep3ahzaiBae" + ), + "token", + "", + "", + False, + ), + # bit.ly should be called, but TOKEN is not set + ( + ( + "http://www.example.org/uGaiXaGh9aiz2kaejeiW0quahtheiwaiveenge" + "Ghook2aeW6suk4phooc8PaiwooCeT1aep3ahzaiBae" + ), + "", + "gid", + "", + False, + ), + # call bit.ly and try to get short URL + ( + ( + "http://www.example.org/uGaiXaGh9aiz2kaejeiW0quahtheiwaiveenge" + "Ghook2aeW6suk4phooc8PaiwooCeT1aep3ahzaiBae" + ), + "token", + "gid", + '{"message":"it is broken"}', + True, + ), + ], +) +def test_cmd_url_no_bitly( + url, + bitly_token, + bitly_gid, + bitly_rsp, + bitly_called, + fixture_mock_requests, + capsys, + monkeypatch, +): + """Test cmd_url() with bit.ly broken one way or another.""" + expected_msg = "Title for {:s} - No title\n".format(url) + + rsp_url_text = "No title here, just a little " + mock_http_url = fixture_mock_requests.get(url, text=rsp_url_text) + + bitly_api_url = "https://api-ssl.bitly.com/v4/shorten" + mock_http_bitly = fixture_mock_requests.post(bitly_api_url, text=bitly_rsp) + + monkeypatch.setenv("IICMD_BITLY_GID", bitly_gid) + monkeypatch.setenv("IICMD_BITLY_TOKEN", bitly_token) + + args = [ + "./iicmd.py", + "--nick=irc_user", + "--message=url foo bar {:s} message".format(url), + "--ircd=irc_ircd", + "--network=irc_network", + "--channel=irc_channel", + "--self=irc_botuser", + ] + with patch.object(sys, "argv", args): + iicmd.main() + + captured = capsys.readouterr() + assert captured.out == expected_msg + assert captured.err == "" + + assert mock_http_bitly.called is bitly_called + assert mock_http_url.called is True + + +def test_cmd_url_with_bitly(fixture_mock_requests, capsys, monkeypatch): + """Test cmd_url() with call to bit.ly.""" + url = ( + "http://www.example.org/uGaiXaGh9aiz2kaejeiW0quahtheiwaiveenge" + "Ghook2aeW6suk4phooc8PaiwooCeT1aep3ahzaiBae" + ) + short_url = "https://short.example.org/abc123" + expected_msg = "Title for {:s} - No title\n".format(short_url) + + bitly_api_url = "https://api-ssl.bitly.com/v4/shorten" + bitly_token = "token" + bitly_gid = "gid" + bitly_rsp = '{{"link":"{:s}"}}'.format(short_url) + + rsp_url_text = "No title here, just a little <title>" + mock_http_url = fixture_mock_requests.get(url, text=rsp_url_text) + + monkeypatch.setenv("IICMD_BITLY_GID", bitly_gid) + monkeypatch.setenv("IICMD_BITLY_TOKEN", bitly_token) + mock_http_bitly = fixture_mock_requests.post(bitly_api_url, text=bitly_rsp) + + args = [ + "./iicmd.py", + "--nick=irc_user", + "--message=url foo bar {:s} message".format(url), + "--ircd=irc_ircd", + "--network=irc_network", + "--channel=irc_channel", + "--self=irc_botuser", + ] + with patch.object(sys, "argv", args): + iicmd.main() + + captured = capsys.readouterr() + assert captured.out == expected_msg + assert captured.err == "" + + assert mock_http_url.called is True + assert mock_http_bitly.called is True