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()