diff --git a/cli/webapp.py b/cli/webapp.py index 6f9d34d..e651370 100644 --- a/cli/webapp.py +++ b/cli/webapp.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import getpass from enum import Enum +from pathlib import Path import typer @@ -104,12 +105,60 @@ def delete_logs( webapp.delete_log(log_type, int(i)) else: webapp.delete_log(log_type, int(log_index)) - typer.echo(snakesay('All done!')) + typer.echo(snakesay("All done!")) @app.command() -def install_ssl(): - raise NotImplementedError +def install_ssl( + domain_name: str = typer.Argument( + ..., + help="Domain name, eg www.mydomain.com", + ), + certificate_file: Path = typer.Argument( + ..., + exists=True, + file_okay=True, + readable=True, + resolve_path=True, + help="The name of the file containing the combined certificate in PEM format (normally a number of blocks, " + 'each one starting "BEGIN CERTIFICATE" and ending "END CERTIFICATE")', + ), + private_key_file: Path = typer.Argument( + ..., + exists=True, + file_okay=True, + readable=True, + resolve_path=True, + help="The name of the file containing the private key in PEM format (a file with one block, " + 'starting with something like "BEGIN PRIVATE KEY" and ending with something like "END PRIVATE KEY")', + ), + suppress_reload: bool = typer.Option( + False, + help="The website will need to be reloaded in order to activate the new certificate/key combination " + "-- this happens by default, use this option to suppress it.", + ), +): + with open(certificate_file, "r") as f: + certificate = f.read() + + with open(private_key_file, "r") as f: + private_key = f.read() + + webapp = Webapp(domain_name) + webapp.set_ssl(certificate, private_key) + if not suppress_reload: + webapp.reload() + + ssl_details = webapp.get_ssl_info() + typer.echo( + snakesay( + "That's all set up now :-)\n" + f"Your new certificate for {domain_name} will expire\n" + f"on {ssl_details['not_after'].date().isoformat()},\n" + "so shortly before then you should renew it\n" + "and install the new certificate." + ) + ) @app.command() diff --git a/tests/test_cli_webapp.py b/tests/test_cli_webapp.py index bace0de..bdb5285 100644 --- a/tests/test_cli_webapp.py +++ b/tests/test_cli_webapp.py @@ -1,7 +1,10 @@ import getpass +import tempfile +from datetime import datetime from unittest.mock import call import pytest +from dateutil.tz import tzutc from typer.testing import CliRunner from cli.webapp import app @@ -20,6 +23,17 @@ def mock_webapp(mocker): return mock_webapp +@pytest.fixture(name="file_with_content") +def fixture_file_with_content(): + def file_with_content(content): + filename = tempfile.NamedTemporaryFile(mode="w", encoding="utf8").name + with open(filename, "w") as f: + f.write(content) + return filename + + return file_with_content + + def test_create_calls_all_stuff_in_right_order(mocker): mock_project = mocker.patch("cli.webapp.Project") @@ -50,16 +64,6 @@ def test_create_calls_all_stuff_in_right_order(mocker): ) -def test_reload(mock_webapp): - domain_name = "foo.bar.baz" - - result = runner.invoke(app, ["reload", "-d", domain_name]) - - assert f"{domain_name} has been reloaded" in result.stdout - mock_webapp.assert_called_once_with(domain_name) - assert mock_webapp.return_value.method_calls == [call.reload()] - - def test_delete_all_logs(mock_webapp): domain_name = "foo.bar.baz" @@ -134,3 +138,58 @@ def test_delete_all_current_logs(mock_webapp): call("server", 0), ] assert "All done!" in result.stdout + + +def test_install_ssl_with_default_reload(mock_webapp, file_with_content): + mock_webapp.return_value.get_ssl_info.return_value = { + "not_after": datetime(2018, 8, 24, 17, 16, 23, tzinfo=tzutc()) + } + domain_name = "foo.bar.baz" + certificate = "certificate" + certificate_file = file_with_content(certificate) + private_key = "private_key" + private_key_file = file_with_content(private_key) + + result = runner.invoke( + app, + ["install-ssl", "foo.bar.baz", certificate_file, private_key_file], + ) + + mock_webapp.assert_called_once_with(domain_name) + mock_webapp.return_value.set_ssl.assert_called_once_with(certificate, private_key) + mock_webapp.return_value.reload.assert_called_once() + assert f"for {domain_name}" in result.stdout + assert "2018-08-24," in result.stdout + + +def test_install_ssl_with_reload_suppressed(mock_webapp, file_with_content): + domain_name = "foo.bar.baz" + certificate = "certificate" + certificate_file = file_with_content(certificate) + private_key = "private_key" + private_key_file = file_with_content(private_key) + + runner.invoke( + app, + [ + "install-ssl", + "foo.bar.baz", + certificate_file, + private_key_file, + "--suppress-reload", + ], + ) + + mock_webapp.assert_called_once_with(domain_name) + mock_webapp.return_value.set_ssl.assert_called_once_with(certificate, private_key) + mock_webapp.return_value.reload.assert_not_called() + + +def test_reload(mock_webapp): + domain_name = "foo.bar.baz" + + result = runner.invoke(app, ["reload", "-d", domain_name]) + + assert f"{domain_name} has been reloaded" in result.stdout + mock_webapp.assert_called_once_with(domain_name) + assert mock_webapp.return_value.method_calls == [call.reload()]