about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--pyproject.toml2
-rw-r--r--src/scadere/check.py10
-rw-r--r--src/scadere/listen.py9
-rw-r--r--tst/test_help.py69
4 files changed, 80 insertions, 10 deletions
diff --git a/pyproject.toml b/pyproject.toml
index 3e2d251..60291a8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = 'flit_core.buildapi'
 name = 'scadere'
 description = 'TLS certificate renewal reminder'
 readme = 'README.md'
-requires-python = '>=3.9'
+requires-python = '>=3.10'
 license = { file = 'COPYING' }
 authors = [ { name = 'Nguyễn Gia Phong', email = 'cnx@loang.net' } ]
 maintainers = [ { name = 'Nguyễn Gia Phong', email = 'chung@loa.loang.net' } ]
diff --git a/src/scadere/check.py b/src/scadere/check.py
index 7ef2d4a..5764382 100644
--- a/src/scadere/check.py
+++ b/src/scadere/check.py
@@ -23,7 +23,7 @@ from email.utils import parsedate_to_datetime as parsedate
 from itertools import chain
 from socket import AF_INET, socket
 from ssl import create_default_context as tls_context
-from sys import stderr, stdout
+from sys import argv, stderr, stdout
 
 from . import __version__, GNUHelpFormatter, NetLoc
 
@@ -66,7 +66,7 @@ def check(netlocs, after, output, fake_ca=None):
                       file=output)
 
 
-def main():
+def main(arguments=argv[1:]):
     """Run TLS checker."""
     description = ('Check TLS certificate expiration of HOST,'
                    ' where PORT defaults to 443.')
@@ -82,11 +82,11 @@ def main():
     parser.add_argument('-o', '--output', metavar='PATH',
                         type=FileType('w'), default=stdout,
                         help='output file (default to stdout)')
-    args = parser.parse_args()
-    with args.output:
+    args = parser.parse_args(arguments)
+    with args.output:  # pragma: no cover
         after = datetime.now(tz=timezone.utc) + timedelta(days=args.days)
         check(args.netloc, after, args.output)
 
 
-if __name__ == '__main__':
+if __name__ == '__main__':  # pragma: no cover
     main()
diff --git a/src/scadere/listen.py b/src/scadere/listen.py
index 4870e2f..80929ea 100644
--- a/src/scadere/listen.py
+++ b/src/scadere/listen.py
@@ -26,6 +26,7 @@ from urllib.parse import parse_qs, urljoin, urlsplit
 from xml.etree.ElementTree import (Element as xml_element,
                                    SubElement as xml_subelement,
                                    indent, tostring as str_from_xml)
+from sys import argv
 
 from . import __version__, GNUHelpFormatter, NetLoc
 
@@ -168,7 +169,7 @@ async def listen(certs, base_url, host, port):  # pragma: no cover
         await server.serve_forever()
 
 
-def main():
+def main(arguments=argv[1:]):
     """Launch server."""
     description = ('Serve the TLS certificate expiration feed'
                    ' from INPUT file for base URL at HOST:PORT,'
@@ -183,9 +184,9 @@ def main():
     parser.add_argument('base_url', metavar='URL')
     parser.add_argument('netloc', metavar='[HOST][:PORT]', nargs='?',
                         type=NetLoc(None), default=('localhost', None))
-    args = parser.parse_args()
-    run(listen(args.certs, args.base_url, *args.netloc))
+    args = parser.parse_args(arguments)
+    run(listen(args.certs, args.base_url, *args.netloc))  # pragma: no cover
 
 
-if __name__ == '__main__':
+if __name__ == '__main__':  # pragma: no cover
     main()
diff --git a/tst/test_help.py b/tst/test_help.py
new file mode 100644
index 0000000..429d7c4
--- /dev/null
+++ b/tst/test_help.py
@@ -0,0 +1,69 @@
+# Tests for CLI help formatting
+# Copyright (C) 2025  Nguyễn Gia Phong
+#
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Scadere is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with scadere.  If not, see <https://www.gnu.org/licenses/>.
+
+from contextlib import redirect_stdout, suppress
+from io import StringIO
+
+from hypothesis import given
+from pytest import fixture, mark, raises
+
+from scadere import NetLoc
+from scadere.check import main as check
+from scadere.listen import main as listen
+
+
+@fixture(scope='session')
+def help_string(request):
+    string = StringIO()
+    with suppress(SystemExit), redirect_stdout(string):
+        request.param(['--help'])
+    return string.getvalue()
+
+
+@mark.parametrize('help_string', [check, listen], indirect=True)
+def test_usage_prefix(help_string):
+    assert help_string.startswith('Usage: ')
+
+
+@mark.parametrize('help_string', [check, listen], indirect=True)
+def test_options_heading(help_string):
+    assert '\n\nOptions:\n' in help_string
+
+
+@mark.parametrize('help_string', [check], indirect=True)
+def test_one_or_more(help_string):
+    assert ' HOST[:PORT]...\n' in help_string
+
+
+@mark.parametrize('help_string', [check], indirect=True)
+@mark.parametrize('short,long,metavar', [('-d', '--days', 'DAYS'),
+                                         ('-o', '--output', 'PATH')])
+def test_long_option(help_string, short, long, metavar):
+    assert f'{short} {metavar}, {long}={metavar}' in help_string
+
+
+@given(...)
+def test_netloc(hostname: str, port: int | None, default_port: int | None):
+    netloc = NetLoc(default_port)
+    if port is not None:
+        assert netloc(f'{hostname}:{port}') == (hostname, port)
+    elif default_port is None:
+        with raises(ValueError):
+            netloc(f'{hostname}:{port}')
+    else:
+        assert netloc(hostname) == (hostname, default_port)