arnaucube
arnaucube/jellyfin-toolsPublic Self-hosted repository
jellyfin-tools/jellyfin-year-review/scripts/test_generate_review.py
jellyfin-year-review/scripts/test_generate_review.pyRaw
  1"""Exercise helper startup and export failures without production services."""
  2
  3import os
  4from pathlib import Path
  5import shutil
  6import subprocess
  7import tempfile
  8import unittest
  9
 10
 11class GenerateHelperTests(unittest.TestCase):
 12    def setUp(self):
 13        self.temp = tempfile.TemporaryDirectory(prefix="reel-helper-")
 14        self.addCleanup(self.temp.cleanup)
 15        self.root = Path(self.temp.name)
 16        (self.root / "scripts").mkdir()
 17        (self.root / "bin").mkdir()
 18        for name in ("generate-review.sh", "export-jellystat.sh", "jellystat-export.sql"):
 19            shutil.copy2(Path(__file__).parent / name, self.root / "scripts" / name)
 20        for name in ("dirname", "mkdir", "date", "mktemp", "mv", "rm"):
 21            (self.root / "bin" / name).symlink_to(shutil.which(name))
 22        (self.root / ".env").write_text(
 23            "JELLYFIN_URL=http://fixture.invalid\n"
 24            "JELLYFIN_API_KEY=synthetic-key-do-not-log\n"
 25            "PGPASSWORD='synthetic password $dollar #hash !'\n"
 26        )
 27        self.args = self.root / "args"
 28        self.env = {**os.environ, "PATH": str(self.root / "bin"), "REEL_CHECK_ARGS": str(self.args)}
 29        self.command("go", 'printf "%s\\n" "$@" > "$REEL_CHECK_ARGS"\n')
 30        self.command("psql", 'printf "%s\\n" "{}"\n')
 31
 32    def command(self, name, script):
 33        path = self.root / "bin" / name
 34        path.write_text("#!/bin/sh\n" + script)
 35        path.chmod(0o755)
 36
 37    def run_helper(self, year="2026"):
 38        result = subprocess.run(
 39            ["/bin/sh", str(self.root / "scripts/generate-review.sh"), year],
 40            env=self.env, capture_output=True, text=True, timeout=5,
 41        )
 42        self.assertNotIn("synthetic-key-do-not-log", result.stdout + result.stderr)
 43        self.assertNotIn("synthetic password", result.stdout + result.stderr)
 44        return result
 45
 46    def test_missing_dependencies_report_error_before_creating_data(self):
 47        for name in ("go", "psql"):
 48            with self.subTest(command=name):
 49                path = self.root / "bin" / name
 50                original = path.read_text()
 51                path.unlink()
 52                result = self.run_helper()
 53                self.assertEqual(result.returncode, 127)
 54                self.assertIn(f"Missing required command: {name}", result.stderr)
 55                self.assertFalse((self.root / "data").exists())
 56                self.assertFalse(self.args.exists())
 57                path.write_text(original)
 58                path.chmod(0o755)
 59
 60    def test_success_reports_stages_and_forwards_year(self):
 61        result = self.run_helper()
 62        self.assertEqual(result.returncode, 0, result.stderr)
 63        self.assertIn("Preparing recap generation for 2026", result.stderr)
 64        self.assertIn("Exporting Jellystat history", result.stderr)
 65        self.assertIn("downloading Jellyfin posters", result.stderr)
 66        self.assertEqual(self.args.read_text().splitlines()[:5], ["run", "./cmd/review", "generate", "--year", "2026"])
 67        self.assertEqual((self.root / "input/jellystat.jsonl").read_text(), "{}\n")
 68        self.assertTrue((self.root / "data").is_dir())
 69        self.assertEqual((self.root / "data").stat().st_mode & 0o777, 0o700)
 70
 71    def test_export_failure_preserves_history_and_stops_generation(self):
 72        (self.root / "input").mkdir()
 73        history = self.root / "input/jellystat.jsonl"
 74        history.write_text("previous successful export\n")
 75        self.command("psql", 'echo "synthetic database error" >&2\nexit 7\n')
 76        result = self.run_helper()
 77        self.assertEqual(result.returncode, 7)
 78        self.assertIn("History export failed", result.stderr)
 79        self.assertFalse(self.args.exists())
 80        self.assertEqual(history.read_text(), "previous successful export\n")
 81        self.assertFalse(list((self.root / "input").glob("*.tmp.*")))
 82
 83    def test_postgres_never_waits_for_a_password_prompt(self):
 84        self.command("psql", 'if [ "$1" != "--no-password" ]; then exit 9; fi\nprintf "%s\\n" "{}"\n')
 85        result = self.run_helper()
 86        self.assertEqual(result.returncode, 0, result.stderr)
 87
 88    def test_password_reaches_postgres_through_environment_only(self):
 89        self.command("psql", '''
 90if [ "$PGPASSWORD" != 'synthetic password $dollar #hash !' ]; then exit 8; fi
 91for argument do
 92  case "$argument" in *"$PGPASSWORD"*) exit 9 ;; esac
 93done
 94printf '%s\\n' '{}'
 95''')
 96        result = self.run_helper()
 97        self.assertEqual(result.returncode, 0, result.stderr)
 98
 99    def test_empty_password_stops_export_without_replacing_history(self):
100        with (self.root / ".env").open("a") as config:
101            config.write("PGPASSWORD=''\n")
102        (self.root / "input").mkdir()
103        history = self.root / "input/jellystat.jsonl"
104        history.write_text("previous successful export\n")
105        self.command("psql", 'echo "psql should not run" >&2\nexit 9\n')
106        result = self.run_helper()
107        self.assertNotEqual(result.returncode, 0)
108        self.assertIn("Set PGPASSWORD", result.stderr)
109        self.assertNotIn("psql should not run", result.stderr)
110        self.assertFalse(self.args.exists())
111        self.assertEqual(history.read_text(), "previous successful export\n")
112        self.assertFalse(list((self.root / "input").glob("*.tmp.*")))
113
114    def test_invalid_year_does_not_start_work(self):
115        result = self.run_helper("bad-year")
116        self.assertNotEqual(result.returncode, 0)
117        self.assertIn("Year must be", result.stderr)
118        self.assertFalse(self.args.exists())
119        self.assertFalse((self.root / "data").exists())
120
121
122if __name__ == "__main__":
123    unittest.main()