# -*- coding: utf-8 -*- """Tests: Bewertungen oeffentlicher Doku-Vorlagen + Update-Badge-Ergaenzungen.""" from __future__ import annotations import inspect import os import sqlite3 import tempfile import unittest class TestRatingSchemaAndValues(unittest.TestCase): def test_schema_idempotent(self): import aza_doku_prompt_sync as sync td = tempfile.mkdtemp() try: db = os.path.join(td, "t.db") con = sqlite3.connect(db) sync.ensure_published_doku_prompts_schema(con) sync.ensure_published_doku_prompt_ratings_schema(con) sync.ensure_published_doku_prompt_ratings_schema(con) tables = { r[0] for r in con.execute( "SELECT name FROM sqlite_master WHERE type='table'" ).fetchall() } con.close() self.assertIn("published_doku_prompt_ratings", tables) finally: import shutil shutil.rmtree(td, ignore_errors=True) def test_valid_half_steps(self): from aza_doku_prompt_sync import VALID_HALF_STAR_RATINGS, normalize_rating_value for v in VALID_HALF_STAR_RATINGS: self.assertEqual(normalize_rating_value(v), v) for bad in (0, 5.5, 4.3, -1, None, "x"): self.assertIsNone(normalize_rating_value(bad)) class TestRatingBackendLogic(unittest.TestCase): def setUp(self): self._tmpdir = tempfile.mkdtemp() self._db = os.path.join(self._tmpdir, "test.db") import aza_doku_prompt_sync as sync self.sync = sync con = sqlite3.connect(self._db) sync.ensure_published_doku_prompts_schema(con) now = sync._now_ts() con.execute( """ INSERT INTO published_doku_prompts (id, user_id, practice_id, doc_type, title, description, content, author_display, revision, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( "pub_a", "author_u", "p1", "brief", "Titel A", "", "Prompt A", "Dr. A", 1, "published", now, now, ), ) con.execute( """ INSERT INTO published_doku_prompts (id, user_id, practice_id, doc_type, title, description, content, author_display, revision, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( "pub_b", "author_u", "p1", "verlauf", "Titel B", "", "Prompt B", "Dr. A", 1, "unpublished", now, now, ), ) con.commit() con.close() def tearDown(self): import shutil import gc gc.collect() shutil.rmtree(self._tmpdir, ignore_errors=True) def _rate(self, pub_id, user, val): con = sqlite3.connect(self._db) try: return self.sync.upsert_public_prompt_rating( con, public_prompt_id=pub_id, rater_user_id=user, rating_value=val, ) finally: con.close() def test_foreign_rate_and_average(self): r1 = self._rate("pub_a", "user_b", 0.5) self.assertEqual(r1["my_rating"], 0.5) self.assertEqual(r1["rating_count"], 1) self.assertEqual(r1["rating_average"], 0.5) r2 = self._rate("pub_a", "user_c", 5.0) self.assertEqual(r2["rating_count"], 2) self.assertEqual(r2["rating_average"], 2.8) def test_own_template_rejected(self): con = sqlite3.connect(self._db) with self.assertRaises(PermissionError): self.sync.upsert_public_prompt_rating( con, public_prompt_id="pub_a", rater_user_id="author_u", rating_value=4.0, ) con.close() def test_unknown_template(self): con = sqlite3.connect(self._db) with self.assertRaises(LookupError): self.sync.upsert_public_prompt_rating( con, public_prompt_id="missing", rater_user_id="user_x", rating_value=3.0, ) con.close() def test_unpublished_rejected(self): con = sqlite3.connect(self._db) with self.assertRaises(PermissionError): self.sync.upsert_public_prompt_rating( con, public_prompt_id="pub_b", rater_user_id="user_x", rating_value=3.0, ) con.close() def test_update_keeps_count(self): self._rate("pub_a", "user_b", 3.5) r = self._rate("pub_a", "user_b", 4.5) self.assertEqual(r["rating_count"], 1) self.assertEqual(r["my_rating"], 4.5) self.assertEqual(r["rating_average"], 4.5) def test_batch_aggregates_no_n_plus_one(self): self._rate("pub_a", "user_b", 4.0) self._rate("pub_a", "user_c", 2.0) con = sqlite3.connect(self._db) agg = self.sync.fetch_rating_aggregates_batch(con, ["pub_a"], "user_b") con.close() self.assertEqual(agg["pub_a"]["rating_count"], 2) self.assertEqual(agg["pub_a"]["my_rating"], 4.0) self.assertEqual(agg["pub_a"]["rating_average"], 3.0) def test_can_rate_flags(self): item = {"author_user_id": "author_u", "status": "published"} self.assertFalse( self.sync.compute_can_rate( author_user_id="author_u", current_user_id="author_u", published=True, ) ) self.assertTrue( self.sync.compute_can_rate( author_user_id="author_u", current_user_id="user_x", published=True, ) ) self.assertFalse( self.sync.compute_can_rate( author_user_id="author_u", current_user_id="user_x", published=False, ) ) class TestRatingClientUI(unittest.TestCase): def test_normalize_includes_rating_fields(self): from aza_doku_prompt_sync import normalize_public_doku_item item = normalize_public_doku_item({ "id": "x", "user_id": "u1", "doc_type": "brief", "title": "T", "content": "C", "rating_average": 4.5, "rating_count": 2, "my_rating": 4.5, "can_rate": True, }) self.assertEqual(item["rating_average"], 4.5) self.assertEqual(item["rating_count"], 2) self.assertTrue(item["can_rate"]) def test_star_formatting(self): import aza_doku_vorlagen as dv self.assertIn("★", dv.render_star_unicode(4.5)) self.assertIn("½", dv.render_star_unicode(4.5)) self.assertEqual(dv.format_rating_count_label(1), "1 Bewertung") self.assertEqual(dv.format_rating_count_label(2), "2 Bewertungen") self.assertIn("Noch keine", dv.format_public_rating_summary(None, 0)) def test_public_window_has_rating_column(self): path = os.path.join(os.path.dirname(__file__), "aza_doku_vorlagen.py") with open(path, encoding="utf-8") as fh: src = fh.read() block = src.split("def open_public_doku_vorlagen_window", 1)[-1].split("\ndef ", 1)[0] self.assertIn('"rating"', block) self.assertIn("build_half_star_rating_bar", block) self.assertIn("Die eigene Vorlage kann nicht bewertet werden", block) def test_rate_route_registered(self): src = inspect.getsource(__import__("aza_doku_prompt_sync").register_doku_prompt_routes) self.assertIn("/v1/doku-prompts/rate/{pub_id}", src) self.assertIn("fetch_rating_aggregates_batch", src) class TestUpdateBadgeExtended(unittest.TestCase): def test_basis14_no_startup_prompt_update(self): import basis14 src = inspect.getsource(basis14.KGDesktopApp) self.assertNotIn("prompt_update_if_available", src) def test_office_shell_optional_only_badge(self): import aza_office_shell_v1 as shell src = inspect.getsource(shell._OfficeShellV12._poll_update_button_visibility) self.assertIn("maybe_show_startup_update_dialog", src) self.assertIn("below_min_required", src) def test_chat_host_required_only(self): src = inspect.getsource(__import__("aza_chat_desktop_host").ChatDesktopHost._bootstrap_chat_host) self.assertIn("prompt_chat_update_if_required", src) self.assertNotIn("prompt_update_if_available", src) def test_incoming_popup_no_update_badge(self): src = open( os.path.join(os.path.dirname(__file__), "aza_empfang_incoming_popup.py"), encoding="utf-8", ).read() self.assertNotIn("update-badge", src.lower()) self.assertNotIn("Aktuell", src) def test_chat_badge_not_aktuell(self): import aza_empfang_webview as ew src = inspect.getsource(ew.EmpfangWebviewApi._inject_kontakt_update_badge) self.assertIn("Update", src) self.assertNotIn("Aktuell", src) def test_update_install_guard(self): import desktop_update_check as duc self.assertTrue(hasattr(duc, "_chat_update_install_inflight")) self.assertTrue(hasattr(duc, "_update_dialog_open")) if __name__ == "__main__": unittest.main()