From 312d872adcb565223514846bc8b859212ab06c79 Mon Sep 17 00:00:00 2001 From: sonny Date: Sun, 27 Dec 2020 20:55:19 +0100 Subject: [PATCH] Initial test --- ip_listener/cli.py | 17 ++++++---- ip_listener/main.py | 13 +++++--- ip_listener/tests.py | 76 +++++++++++++++++++++++++++++++++++++++++++ poetry.lock | 77 +++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 5 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 ip_listener/tests.py diff --git a/ip_listener/cli.py b/ip_listener/cli.py index 65060be..d6f669a 100644 --- a/ip_listener/cli.py +++ b/ip_listener/cli.py @@ -2,17 +2,20 @@ import click from ip_listener.main import detect -DEFAULT_RESOLVERS = ["@resolver1.opendns.com", "@resolver2.opendns.com"] +DEFAULT_DNS = "myip.opendns.com" +DEFAULT_DNS_NAME = "@resolver1.opendns.com" DEFAULT_API_URL = "https://api.transip.nl/v6" @click.command() -@click.argument("domains", envvar="DOMAIN", nargs=-1) +@click.argument("domains", envvar="DOMAINS", nargs=-1) @click.argument("token", envvar="TOKEN") -@click.option( - "--resolvers", envvar="RESOLVERS", default=DEFAULT_RESOLVERS, multiple=True -) +@click.option("--dns", envvar="DNS", default=DEFAULT_DNS) +@click.option("--dns-name", envvar="DNS_NAME", default=DEFAULT_DNS_NAME) @click.option("--api-url", envvar="API_URL", default=DEFAULT_API_URL) @click.option("--read-only/--write", envvar="READ_ONLY", default=False) -def run(*args): - detect(*args) +def run(domains, token, dns, dns_name, api_url, read_only): + if not domains: + raise ValueError("No domain(s) specified") + + detect(domains, (dns, dns_name), api_url, token, read_only) diff --git a/ip_listener/main.py b/ip_listener/main.py index 2db1030..e15eab1 100644 --- a/ip_listener/main.py +++ b/ip_listener/main.py @@ -1,3 +1,4 @@ +import json import logging import subprocess from concurrent.futures import ThreadPoolExecutor, as_completed @@ -16,6 +17,8 @@ def _get_ip(resolvers): except subprocess.CalledProcessError as e: raise OSError("Unable to retrieve current IP") from e + return output.decode("utf-8").strip() + def _get_domain(domain, token, api_url): headers = {"Authorization": f"Bearer {token}"} @@ -43,7 +46,7 @@ def _get_domain_data(domains, token, api_url): yield {"domain": domain, **response.json()} -def _update_domain(domain, payload, token, api_url): +def _update_domain(domain, payload, api_url, token): headers = {"Authorization": f"Bearer {token}"} return requests.put( @@ -51,13 +54,13 @@ def _update_domain(domain, payload, token, api_url): ) -def _update_domains(updated_domains, token, read_only): +def _update_domains(updated_domains, api_url, token, read_only): if read_only: return with ThreadPoolExecutor(max_workers=10) as executor: futures = { - executor.submit(_update_domain, domain, entries, token): domain + executor.submit(_update_domain, domain, entries, api_url, token): domain for domain, entries in updated_domains.items() } @@ -74,7 +77,7 @@ def _update_domains(updated_domains, token, read_only): logger.info(f"Updated domain {domain}") -def detect(domains, resolvers, token, api_url, read_only): +def detect(domains, resolvers, api_url, token, read_only): ip = _get_ip(resolvers) domain_data = _get_domain_data(domains, token, api_url) updated_domains = {} @@ -97,4 +100,4 @@ def detect(domains, resolvers, token, api_url, read_only): updated_domains[domain] = {"dnsEntries": updated_entries} - _update_domains(updated_domains, token, api_url, read_only) + _update_domains(updated_domains, api_url, token, read_only) diff --git a/ip_listener/tests.py b/ip_listener/tests.py new file mode 100644 index 0000000..35f6f41 --- /dev/null +++ b/ip_listener/tests.py @@ -0,0 +1,76 @@ +import json +from unittest import TestCase +from unittest.mock import patch + +from click.testing import CliRunner + +from ip_listener.cli import DEFAULT_API_URL, run + + +class RunTestCase(TestCase): + def setUp(self): + patcher = patch("ip_listener.main.subprocess.check_output") + self.mocked_dns = patcher.start() + + patcher = patch("ip_listener.main.requests.get") + self.mocked_get = patcher.start() + + patcher = patch("ip_listener.main.requests.put") + self.mocked_put = patcher.start() + + def test_simple(self): + self.mocked_dns.return_value = b"111.420\n" + self.mocked_get.return_value.json.return_value = { + "dnsEntries": [ + { + "name": "@", + "expire": 60, + "type": "A", + "content": "111.421", + } + ], + "_links": [ + { + "rel": "self", + "link": "https://api.transip.nl/v6/domains/foobar.com/dns", + }, + { + "rel": "domain", + "link": "https://api.transip.nl/v6/domains/foobar.com", + }, + ], + } + + with self.assertLogs("ip_listener.main", level="INFO") as logger: + runner = CliRunner() + result = runner.invoke(run, ["foobar.com", "TOKEN"]) + + self.assertEqual( + logger.output, ["INFO:ip_listener.main:Updated domain foobar.com"] + ) + + self.assertEqual(result.exit_code, 0) + + self.mocked_get.assert_called_with( + f"{DEFAULT_API_URL}/domains/foobar.com/dns", + headers={"Authorization": "Bearer TOKEN"}, + ) + + expected_json = json.dumps( + { + "dnsEntries": [ + { + "name": "@", + "expire": 60, + "type": "A", + "content": "111.420", + } + ] + } + ) + + self.mocked_put.assert_called_with( + f"{DEFAULT_API_URL}/domains/foobar.com/dns", + data=expected_json, + headers={"Authorization": "Bearer TOKEN"}, + ) diff --git a/poetry.lock b/poetry.lock index 2b43d23..6bffee5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -39,6 +39,22 @@ toml = ">=0.10.1" typed-ast = ">=1.4.0" typing-extensions = ">=3.7.4" +[[package]] +name = "certifi" +version = "2020.12.5" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "click" version = "7.1.2" @@ -47,6 +63,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "isort" version = "5.6.4" @@ -103,6 +127,24 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + [[package]] name = "toml" version = "0.10.2" @@ -127,10 +169,23 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "urllib3" +version = "1.26.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + [metadata] lock-version = "1.0" python-versions = "^3.7" -content-hash = "94aba70614582f8f382aa70da21bde6f0b851c650646027f17cb04012b2082bd" +content-hash = "c5bce676d27fa0d95f442535e7493e2f9ceba0b7af3cbde1603b41044fc9723a" [metadata.files] appdirs = [ @@ -143,10 +198,22 @@ autoflake = [ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] +certifi = [ + {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, + {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] click = [ {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] isort = [ {file = "isort-5.6.4-py3-none-any.whl", hash = "sha256:dcab1d98b469a12a1a624ead220584391648790275560e1a43e54c5dceae65e7"}, {file = "isort-5.6.4.tar.gz", hash = "sha256:dcaeec1b5f0eca77faea2a35ab790b4f3680ff75590bfcb7145986905aab2f58"}, @@ -210,6 +277,10 @@ regex = [ {file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"}, {file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"}, ] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -242,3 +313,7 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] +urllib3 = [ + {file = "urllib3-1.26.2-py2.py3-none-any.whl", hash = "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"}, + {file = "urllib3-1.26.2.tar.gz", hash = "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08"}, +] diff --git a/pyproject.toml b/pyproject.toml index 83e1e14..9d03171 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ license = "GPL-3.0" python = "^3.7" click = "^7.1.2" python-dotenv = "^0.15.0" +requests = "^2.25.1" [tool.poetry.dev-dependencies] black = "^20.8b1"