test(deploy): cover configuration, setup and backups
Check rejected settings, administrator replacement and restorable database/media backups.
This commit is contained in:
1 parent
e20850a498
commit
51db1e4329
1 file changed
+108
@@ -0,0 +1,108 @@
|
|||||||
|
from contextlib import ExitStack, closing
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from unittest.mock import patch
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
import manage
|
||||||
|
import server_config
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.stack = ExitStack()
|
||||||
|
self.directory = Path(self.stack.enter_context(TemporaryDirectory()))
|
||||||
|
self.stack.enter_context(patch.dict(os.environ, {"REXIMEMO_CONFIG": str(self.directory / "settings.json")}))
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.stack.close()
|
||||||
|
|
||||||
|
def test_defaults_and_partial_save_preserve_unrelated_settings(self):
|
||||||
|
self.assertEqual(server_config.load_config()["port"], 8080)
|
||||||
|
saved = server_config.save_config({"site_name": "My Flipnotes", "port": 8088, "unknown_secret": "not saved"})
|
||||||
|
self.assertEqual(saved["site_name"], "My Flipnotes")
|
||||||
|
self.assertNotIn("unknown_secret", saved)
|
||||||
|
saved = server_config.save_config({"welcome_message": "Hello!", "guest_posting": "0"})
|
||||||
|
self.assertEqual(saved["site_name"], "My Flipnotes")
|
||||||
|
self.assertEqual(saved["port"], 8088)
|
||||||
|
self.assertFalse(saved["guest_posting"])
|
||||||
|
self.assertEqual(list(self.directory.glob("*.tmp")), [])
|
||||||
|
|
||||||
|
def test_bad_settings_leave_existing_configuration_intact(self):
|
||||||
|
original = server_config.save_config({"site_name": "Keep this"})
|
||||||
|
for field in ({"accent_colour": "red;background:url(evil)"}, {"port": 80},
|
||||||
|
{"host": "someone else's host"}, {"max_upload_bytes": 1},
|
||||||
|
{"logo_url": "https://other.example/picture.png"},
|
||||||
|
{"logo_url": "/static/../private.png"},
|
||||||
|
{"logo_url": "/static/%2e%2e/private.png"}):
|
||||||
|
with self.subTest(field=field), self.assertRaises(ValueError):
|
||||||
|
server_config.save_config(field)
|
||||||
|
self.assertEqual(server_config.load_config(), original)
|
||||||
|
self.assertEqual(server_config.save_config({"logo_url": "/branding/logo.png"})["logo_url"], "/branding/logo.png")
|
||||||
|
|
||||||
|
def test_corrupt_settings_do_not_silently_reset_the_server(self):
|
||||||
|
server_config.config_path().write_text("not json", encoding="utf-8")
|
||||||
|
with self.assertRaisesRegex(ValueError, "valid JSON"):
|
||||||
|
server_config.load_config()
|
||||||
|
|
||||||
|
def test_backup_contains_snapshot_media_and_branding_without_logs(self):
|
||||||
|
database_path = self.directory / "database" / "reximemo.sqlite3"
|
||||||
|
database_path.parent.mkdir()
|
||||||
|
with closing(sqlite3.connect(database_path)) as database:
|
||||||
|
with database:
|
||||||
|
database.execute("CREATE TABLE example(value TEXT)")
|
||||||
|
database.execute("INSERT INTO example VALUES('preserved')")
|
||||||
|
for name, data in (("database/Creators/SAMPLE/note.ppm", b"PARA"),
|
||||||
|
("database/Comments/note.ppm", b"mini"),
|
||||||
|
("web/static/logo.png", b"logo"),
|
||||||
|
("database/site-logo.png", b"custom logo"),
|
||||||
|
("logs/private.log", b"do not export")):
|
||||||
|
path = self.directory / name
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_bytes(data)
|
||||||
|
server_config.save_config({"site_name": "Backup server"})
|
||||||
|
destination = self.directory / "copy.zip"
|
||||||
|
with patch.object(manage, "ROOT", self.directory), patch.object(manage, "database_path", return_value=database_path):
|
||||||
|
self.assertEqual(manage.backup_server(destination), destination)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
manage.backup_server(destination)
|
||||||
|
with zipfile.ZipFile(destination) as archive:
|
||||||
|
self.assertEqual(archive.read("database/Creators/SAMPLE/note.ppm"), b"PARA")
|
||||||
|
self.assertEqual(archive.read("database/Comments/note.ppm"), b"mini")
|
||||||
|
self.assertEqual(archive.read("database/site-logo.png"), b"custom logo")
|
||||||
|
self.assertEqual(json.loads(archive.read("server-settings.json"))["site_name"], "Backup server")
|
||||||
|
self.assertFalse(any(name.startswith("logs/") for name in archive.namelist()))
|
||||||
|
restored = self.directory / "restored.sqlite3"
|
||||||
|
restored.write_bytes(archive.read("database/reximemo.sqlite3"))
|
||||||
|
with closing(sqlite3.connect(restored)) as database:
|
||||||
|
self.assertEqual(database.execute("SELECT value FROM example").fetchone()[0], "preserved")
|
||||||
|
|
||||||
|
def test_setup_chooses_an_admin_and_disables_the_default_login(self):
|
||||||
|
script = '''
|
||||||
|
import json
|
||||||
|
import manage
|
||||||
|
answers = iter(["A friendly server", "8099", "127.0.0.1", "friendlyadmin"])
|
||||||
|
manage.setup_server(input_fn=lambda prompt: next(answers), password_fn=lambda prompt: "a strong setup password")
|
||||||
|
from database import Database
|
||||||
|
assert Database.verify_account("friendlyadmin", "a strong setup password")["is_admin"] == 1
|
||||||
|
assert Database.verify_account("reximemo", "alpine") is None
|
||||||
|
from server_config import load_config
|
||||||
|
assert load_config()["setup_complete"] is True
|
||||||
|
assert load_config()["port"] == 8099
|
||||||
|
Database.close()
|
||||||
|
'''
|
||||||
|
environment = {**os.environ, "REXIMEMO_DB": str(self.directory / "setup.sqlite3")}
|
||||||
|
result = subprocess.run([sys.executable, "-c", script], cwd=Path(manage.__file__).parent,
|
||||||
|
env=environment, capture_output=True, text=True)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertNotIn("a strong setup password", server_config.config_path().read_text())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user
