From 338ec24e90ed37dfccf008a147b1da75dbf76a11 Mon Sep 17 00:00:00 2001 From: migatu Date: Thu, 27 Aug 2026 16:37:03 +0200 Subject: [PATCH] =?UTF-8?q?astroklient:=20w=C5=82asna=20nazwa=20produktu?= =?UTF-8?q?=20zamiast=20odziedziczonej=20po=20rdzeniu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Astroklient przedstawiał się na stronie głównej jako „astrololo" — w tytule karty i w nagłówku. To najpoważniejszy z możliwych śladów: mówi klientowi wprost, że trzyma okrojoną wersję czegoś większego, i podaje frazę, po której da się wyszukać, czego mu nie dano. Cała reszta izolacji przestaje mieć znaczenie, gdy produkt sam się przedstawia nazwą rodzica. Wzięło się stąd, że nazwa była wpisana na sztywno w szablony, a szablony są WSPÓLNE — dokładnie ta sama przyczyna, co przy zaszytym w base.html warunku na administratora. Nazwa mieszka teraz w module `produkt.py`, który nakładka produktu nadpisuje; szablony o nią pytają, zamiast ją znać. astrodemo NIE było dotknięte: ma własne kopie szablonów i pokazuje „astrodemo". DRUGIE ŹRÓDŁO WYCIEKU, mniej oczywiste: nazwa siedziała też w identyfikatorach JavaScriptu, które widzi każdy, kto otworzy narzędzia deweloperskie — zdarzenie `astrololo:coords` i klucz `astrololo.form.v1` w localStorage. Przemianowane na neutralne `kosmogram:*`; te identyfikatory nie mają powodu nosić nazwy rodziny produktów. SKUTEK UBOCZNY: zmiana klucza w localStorage kasuje raz zapamiętane wersje robocze formularza. Sprawdzone, że NIE wyciekają: nagłówki X-Astrololo-* jadą wyłącznie między warstwami, a dokumentacja OpenAPI (gdzie zostaje tytuł aplikacji FastAPI) oddaje 404 także zalogowanemu. TEST rozdziela to, co widzi człowiek, od identyfikatorów wewnętrznych. Nazwa rdzenia wolno występować w nazwach loggerów, materiale do wyprowadzania kluczy i nagłówkach międzywarstwowych; nie wolno w szablonach, zasobach ani w treści odpowiedzi. Zakaz na wszystko byłby albo obejściem przez zmianę nazw kluczy kryptograficznych, albo martwą regułą wyłączaną przy pierwszej okazji. Sprawdzony w obie strony: po cofnięciu nakładki test pada. Testy: astroklient 15, presentation 368, astrodemo 28, logic 342, data 42, render 41. Co-Authored-By: Claude Opus 5 --- services/astroklient/app/produkt.py | 8 +++ services/astroklient/tests/conftest.py | 57 +++++++++++++++++++ .../tests/test_slownik_zakazany.py | 49 ++++++++++++++++ services/astroklient/tests/test_zlozenie.py | 55 ------------------ .../app/moduly/dodatki/static/natal.js | 2 +- .../app/moduly/dodatki/static/predictions.js | 2 +- services/presentation/app/podstawa.py | 4 ++ services/presentation/app/produkt.py | 12 ++++ services/presentation/app/static/compile.js | 4 +- services/presentation/app/static/formsync.js | 4 +- services/presentation/app/static/geo.js | 4 +- services/presentation/app/static/now.js | 2 +- services/presentation/app/templates/base.html | 4 +- .../presentation/app/templates/logowanie.html | 4 +- services/presentation/tests/test_compile.py | 4 +- .../presentation/tests/test_predictions.py | 2 +- services/presentation/tests/test_static_js.py | 6 +- 17 files changed, 149 insertions(+), 74 deletions(-) create mode 100644 services/astroklient/app/produkt.py create mode 100644 services/presentation/app/produkt.py diff --git a/services/astroklient/app/produkt.py b/services/astroklient/app/produkt.py new file mode 100644 index 0000000..f47f522 --- /dev/null +++ b/services/astroklient/app/produkt.py @@ -0,0 +1,8 @@ +"""Nazwa produktu — nakładka astroklienta. + +Nadpisuje wersję z rdzenia. Test słownikowy pilnuje, żeby po nadpisaniu nazwa +tamtego produktu nie została nigdzie w szablonach, zasobach ani w odpowiedziach. +""" +from __future__ import annotations + +NAZWA = "astroklient" diff --git a/services/astroklient/tests/conftest.py b/services/astroklient/tests/conftest.py index 2d0974a..b72c519 100644 --- a/services/astroklient/tests/conftest.py +++ b/services/astroklient/tests/conftest.py @@ -7,8 +7,12 @@ inaczej sprawdzałyby garść plików, a nie produkt. """ from __future__ import annotations +import json import pathlib +import re import shutil +import subprocess +import sys import pytest @@ -35,3 +39,56 @@ def zloz(cel: pathlib.Path) -> pathlib.Path: @pytest.fixture(scope="session") def drzewo(tmp_path_factory) -> pathlib.Path: return zloz(tmp_path_factory.mktemp("astroklient")) + + +SONDA = ''' +import json, os, hashlib, secrets, re +sol = secrets.token_bytes(16) +dk = hashlib.scrypt(b"tajne", salt=sol, n=2**14, r=8, p=1, dklen=32) +os.environ.update(ASTROKLIENT_USERS="klient:scrypt$%s$%s" % (sol.hex(), dk.hex()), + SESSION_SECRET="t"*64, COOKIES_INSECURE="1", RATE_LIMIT_PER_MIN="0") +from starlette.testclient import TestClient +from app.main import app +from app import security, session, features +from app.clients import logic_client +c = TestClient(app, follow_redirects=False) +H = {"Cookie": f"{session.COOKIE}={security.issue_session('klient')}"} +strona = c.get("/", headers=H).text +zebrane = [] +logic_client.link_crypto.call_json = lambda *a, **kw: (zebrane.append(kw.get("payload")), {})[1] +c.post("/interpret", headers=H, data={"date":"1984-04-30","time":"11:20", + "tz_offset":"2","lat":"50","lon":"19","tenant":"ktos-inny"}) +print("WYNIK:" + json.dumps({ + "ekrany": [f.label for f in features.SCREENS], + "uprawnienia": sorted(security._uprawnienia()), + "dziala": {a: c.get(a, headers=H).status_code + for a in ("/", "/interpret", "/timeline", "/synastry", + "/significators", "/pliki")}, + "nie_ma": {a: c.get(a, headers=H).status_code + for a in ("/compile", "/settings", "/accounts", "/files", + "/horoscope/stream", "/compile/pdf")}, + "nawigacja": re.findall(r"", strona, re.S)[0].count('(.*?)", strona) or [None, "?"])[1], + "nazwa_w_tytule": (re.search(r"(.*?) ·", strona) or [None, "?"])[1], + "rdzen_w_tresci": "astrololo" in strona.lower(), + "dokumentacja": {a: c.get(a, headers=H).status_code + for a in ("/docs", "/openapi.json", "/redoc")}, + "logowanie_dziala": bool(security.verify("klient", "tajne")), + "zle_haslo_odmowa": security.verify("klient", "inne") is None, +})) +''' + + +@pytest.fixture(scope="module") +def wynik(drzewo): + """Aplikacja uruchomiona w OSOBNYM PROCESIE. + + Importy Pythona są zapamiętywane w procesie, więc sprawdzanie „produktu bez + modułu" tam, gdzie moduł już się zaimportował, dałoby wynik fałszywie + pozytywny.""" + r = subprocess.run([sys.executable, "-c", SONDA], cwd=drzewo, capture_output=True, + text=True, env={"PYTHONPATH": str(drzewo), "PATH": "/usr/bin:/bin"}) + linia = next((l for l in r.stdout.splitlines() if l.startswith("WYNIK:")), None) + assert linia, f"sonda nie wystartowała:\n{r.stdout[-2000:]}\n{r.stderr[-2000:]}" + return json.loads(linia[len("WYNIK:"):]) diff --git a/services/astroklient/tests/test_slownik_zakazany.py b/services/astroklient/tests/test_slownik_zakazany.py index 730f0d9..851a500 100644 --- a/services/astroklient/tests/test_slownik_zakazany.py +++ b/services/astroklient/tests/test_slownik_zakazany.py @@ -86,3 +86,52 @@ def test_the_removal_list_matches_what_is_actually_composed(drzewo): def test_no_optional_module_survived(drzewo): """Katalogu modułów opcjonalnych nie ma wcale — most odkrywa wtedy zero.""" assert not (drzewo / "app" / "moduly").exists() + + +# ── nazwa produktu, z którego ten produkt powstał ───────────────────────── +# Osobno od listy wyżej, bo granica przebiega gdzie indziej: „astrololo" wolno +# występować w identyfikatorach WEWNĘTRZNYCH (nazwy loggerów, materiał do +# wyprowadzania kluczy, nagłówki między warstwami), a nie wolno w niczym, co +# dociera do przeglądarki. Zakaz na wszystko byłby albo obejściem przez zmianę +# nazw kluczy kryptograficznych, albo martwą regułą wyłączaną przy pierwszej +# okazji. +NAZWA_RDZENIA = "astrololo" +WIDOCZNE_DLA_KLIENTA = ("templates", "static") + + +def test_the_parent_product_is_not_named_in_anything_the_browser_receives(drzewo): + """Szablony i zasoby są WSPÓLNE z pełną aplikacją, więc jej nazwa jechała + z nimi do tego obrazu — i astroklient przedstawiał się jako astrololo. + + To jest najpoważniejszy z możliwych śladów: mówi klientowi wprost, że trzyma + okrojoną wersję czegoś większego, i podaje frazę, po której da się wyszukać, + czego mu nie dano.""" + grzechy = [] + for katalog in WIDOCZNE_DLA_KLIENTA: + for f in sorted((drzewo / "app" / katalog).rglob("*")): + if not f.is_file() or "__pycache__" in str(f): + continue + try: + tresc = f.read_text(encoding="utf-8") + except UnicodeDecodeError: + continue + if NAZWA_RDZENIA in tresc.lower(): + grzechy.append(str(f.relative_to(drzewo))) + assert not grzechy, ( + f"\nNazwa „{NAZWA_RDZENIA}” pada w plikach, które dostaje " + f"przeglądarka:\n " + "\n ".join(grzechy)) + + +def test_the_rendered_pages_carry_this_products_own_name(wynik): + """Sprawdzane na ODPOWIEDZI, nie na szablonie: liczy się to, co dociera do + człowieka, a nie to, co jest w pliku przed złożeniem.""" + assert wynik["nazwa_w_naglowku"] == "astroklient" + assert wynik["nazwa_w_tytule"] == "astroklient" + assert not wynik["rdzen_w_tresci"], ( + "nazwa pełnej aplikacji pada w treści strony oddawanej klientowi") + + +def test_the_api_documentation_is_not_reachable(wynik): + """Tytuł aplikacji FastAPI wciąż niesie nazwę rdzenia, więc dokumentacja + OpenAPI byłaby wyciekiem tylnymi drzwiami — nawet gdy szablony są czyste.""" + assert all(k == 404 for k in wynik["dokumentacja"].values()), wynik["dokumentacja"] diff --git a/services/astroklient/tests/test_zlozenie.py b/services/astroklient/tests/test_zlozenie.py index 23f3c34..eb7cd81 100644 --- a/services/astroklient/tests/test_zlozenie.py +++ b/services/astroklient/tests/test_zlozenie.py @@ -5,61 +5,6 @@ czego w nim nie ma, że to, co zostało, działa, i że nie zostało po niczym s """ from __future__ import annotations -import json -import re -import subprocess -import sys - -import pytest - -SONDA = ''' -import json, os, hashlib, secrets, re -sol = secrets.token_bytes(16) -dk = hashlib.scrypt(b"tajne", salt=sol, n=2**14, r=8, p=1, dklen=32) -os.environ.update(ASTROKLIENT_USERS="klient:scrypt$%s$%s" % (sol.hex(), dk.hex()), - SESSION_SECRET="t"*64, COOKIES_INSECURE="1", RATE_LIMIT_PER_MIN="0") -from starlette.testclient import TestClient -from app.main import app -from app import security, session, features -from app.clients import logic_client -c = TestClient(app, follow_redirects=False) -H = {"Cookie": f"{session.COOKIE}={security.issue_session('klient')}"} -strona = c.get("/", headers=H).text -zebrane = [] -logic_client.link_crypto.call_json = lambda *a, **kw: (zebrane.append(kw.get("payload")), {})[1] -c.post("/interpret", headers=H, data={"date":"1984-04-30","time":"11:20", - "tz_offset":"2","lat":"50","lon":"19","tenant":"ktos-inny"}) -print("WYNIK:" + json.dumps({ - "ekrany": [f.label for f in features.SCREENS], - "uprawnienia": sorted(security._uprawnienia()), - "dziala": {a: c.get(a, headers=H).status_code - for a in ("/", "/interpret", "/timeline", "/synastry", - "/significators", "/pliki")}, - "nie_ma": {a: c.get(a, headers=H).status_code - for a in ("/compile", "/settings", "/accounts", "/files", - "/horoscope/stream", "/compile/pdf")}, - "nawigacja": re.findall(r"<nav>(.*?)</nav>", strona, re.S)[0].count('<a href="'), - "pula": (zebrane[0] or {}).get("tenant"), - "logowanie_dziala": bool(security.verify("klient", "tajne")), - "zle_haslo_odmowa": security.verify("klient", "inne") is None, -})) -''' - - -@pytest.fixture(scope="module") -def wynik(drzewo): - """Aplikacja uruchomiona w OSOBNYM PROCESIE. - - Importy Pythona są zapamiętywane w procesie, więc sprawdzanie „produktu bez - modułu" tam, gdzie moduł już się zaimportował, dałoby wynik fałszywie - pozytywny.""" - r = subprocess.run([sys.executable, "-c", SONDA], cwd=drzewo, capture_output=True, - text=True, env={"PYTHONPATH": str(drzewo), "PATH": "/usr/bin:/bin"}) - linia = next((l for l in r.stdout.splitlines() if l.startswith("WYNIK:")), None) - assert linia, f"sonda nie wystartowała:\n{r.stdout[-2000:]}\n{r.stderr[-2000:]}" - return json.loads(linia[len("WYNIK:"):]) - - def test_the_build_starts_at_all(wynik): assert wynik["dziala"]["/"] == 200 diff --git a/services/presentation/app/moduly/dodatki/static/natal.js b/services/presentation/app/moduly/dodatki/static/natal.js index 10abacb..fe97526 100644 --- a/services/presentation/app/moduly/dodatki/static/natal.js +++ b/services/presentation/app/moduly/dodatki/static/natal.js @@ -93,5 +93,5 @@ } }); - window.astrololoNatal = { read: read, clear: clear, key: KEY }; + window.kosmogramNatal = { read: read, clear: clear, key: KEY }; })(); diff --git a/services/presentation/app/moduly/dodatki/static/predictions.js b/services/presentation/app/moduly/dodatki/static/predictions.js index 8f8e73a..ee93dd6 100644 --- a/services/presentation/app/moduly/dodatki/static/predictions.js +++ b/services/presentation/app/moduly/dodatki/static/predictions.js @@ -146,5 +146,5 @@ }); // Udostępniamy magazyn zakładce „Skompiluj" (PRE-23) — jedno źródło prawdy. - window.astrololoPredictions = { read: read, remove: remove, key: KEY }; + window.kosmogramPredictions = { read: read, remove: remove, key: KEY }; })(); diff --git a/services/presentation/app/podstawa.py b/services/presentation/app/podstawa.py index 6299104..41428d3 100644 --- a/services/presentation/app/podstawa.py +++ b/services/presentation/app/podstawa.py @@ -27,6 +27,7 @@ from fastapi.templating import Jinja2Templates from app import rozszerzenia from app import chartwheel as chartwheel_mod from app import features +from app import produkt from app import session from app.house_systems import HOUSE_SYSTEMS, LIMITED as HOUSE_LIMITED, label as house_label @@ -142,6 +143,9 @@ def static_file(nazwa: str): ) +# Nazwa produktu z modułu, nie z szablonu: szablony są wspólne dla kilku +# produktów, a nazwa nie. +templates.env.globals["produkt"] = produkt.NAZWA templates.env.globals["static"] = static_url # Pozycje specjalne przefiltrowane uprawnieniem — szablon dostaje gotową listę # i nie musi wiedzieć, czego która wymaga. diff --git a/services/presentation/app/produkt.py b/services/presentation/app/produkt.py new file mode 100644 index 0000000..5d9a0df --- /dev/null +++ b/services/presentation/app/produkt.py @@ -0,0 +1,12 @@ +"""Nazwa produktu — to, co widzi człowiek w tytule i nagłówku. + +Osobny moduł, bo szablony są WSPÓLNE dla kilku produktów, a nazwa nie. Wpisana +w szablon na sztywno jechała z nim do każdego obrazu, który go bierze — i tak +astroklient przedstawiał się jako astrololo, czyli mówił klientowi wprost, że +trzyma okrojoną wersję czegoś większego, i podawał frazę do wyszukania. + +Produkt składający się z tego rdzenia NADPISUJE ten plik swoją nakładką. +""" +from __future__ import annotations + +NAZWA = "astrololo" diff --git a/services/presentation/app/static/compile.js b/services/presentation/app/static/compile.js index 33f8cfc..c89d0e2 100644 --- a/services/presentation/app/static/compile.js +++ b/services/presentation/app/static/compile.js @@ -22,11 +22,11 @@ } function readNatal() { - return (window.astrololoNatal && window.astrololoNatal.read()) || null; + return (window.kosmogramNatal && window.kosmogramNatal.read()) || null; } function readPredictions() { - return (window.astrololoPredictions && window.astrololoPredictions.read()) || []; + return (window.kosmogramPredictions && window.kosmogramPredictions.read()) || []; } function item(ok, label, hint) { diff --git a/services/presentation/app/static/formsync.js b/services/presentation/app/static/formsync.js index f72d998..aa612cb 100644 --- a/services/presentation/app/static/formsync.js +++ b/services/presentation/app/static/formsync.js @@ -16,7 +16,7 @@ (function () { 'use strict'; - var KEY = 'astrololo.form.v1'; + var KEY = 'kosmogram.form.v1'; // Pola wspólne dla zakładek. Nazwa pola formularza -> jak je znaleźć. // `geoSearch` (nazwa miejsca) nie ma atrybutu name — szukamy po id. @@ -122,7 +122,7 @@ // 5) geo.js ustawia lat/lon z mapy poza polami formularza — po takiej zmianie // też chcemy zapamiętać (zdarzenie leci z „Tu i teraz" oraz z mapy) - document.addEventListener('astrololo:coords', function () { + document.addEventListener('kosmogram:coords', function () { setTimeout(function () { save(collect()); }, 0); }); })(); diff --git a/services/presentation/app/static/geo.js b/services/presentation/app/static/geo.js index bcfa07c..bcf7d6b 100644 --- a/services/presentation/app/static/geo.js +++ b/services/presentation/app/static/geo.js @@ -2,7 +2,7 @@ // Wpina się do formularza z polami input[name=lat] / input[name=lon]: // - wpisanie nazwy/adresu/POI -> /geocode -> lista trafień -> klik ustawia lat/lon, // - klik na mapie lub przeciągnięcie pineski -> ustawia lat/lon + /reverse pokazuje nazwę, -// - „Tu i teraz" (now.js) emituje event 'astrololo:coords' -> mapa się synchronizuje. +// - „Tu i teraz" (now.js) emituje event 'kosmogram:coords' -> mapa się synchronizuje. // Bez internetu wyszukiwarka/kafelki nie działają, ale ręczne lat/lon dalej tak. (function () { function ready(fn) { @@ -163,7 +163,7 @@ // zostawała w nim stara, ręcznie wpisana nazwa, więc formularz pokazywał jedno // miejsce, a współrzędne wskazywały już inne. reverseName pyta o nazwę punktu, // na który faktycznie ustawiła się mapa. - document.addEventListener('astrololo:coords', function () { + document.addEventListener('kosmogram:coords', function () { marker.setLatLng([curLat(), curLon()]); map.setView([curLat(), curLon()], Math.max(map.getZoom(), 12)); reverseName(curLat(), curLon()); diff --git a/services/presentation/app/static/now.js b/services/presentation/app/static/now.js index 70bc7f1..269f144 100644 --- a/services/presentation/app/static/now.js +++ b/services/presentation/app/static/now.js @@ -32,7 +32,7 @@ document.addEventListener('DOMContentLoaded', function () { function (pos) { document.querySelector('input[name=lat]').value = pos.coords.latitude.toFixed(4); document.querySelector('input[name=lon]').value = pos.coords.longitude.toFixed(4); - document.dispatchEvent(new CustomEvent('astrololo:coords')); // zsynchronizuj mapę (geo.js) + document.dispatchEvent(new CustomEvent('kosmogram:coords')); // zsynchronizuj mapę (geo.js) say('Lokalizacja pobrana ✓'); }, function (err) { diff --git a/services/presentation/app/templates/base.html b/services/presentation/app/templates/base.html index ad1a3f1..d125f1f 100644 --- a/services/presentation/app/templates/base.html +++ b/services/presentation/app/templates/base.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> - <title>astrololo · {% block title %}{% endblock %} + {{ produkt }} · {% block title %}{% endblock %} {# Arkusz wspólny niesie WYŁĄCZNIE powłokę i logowanie — wszystko, co nazywa jakąkolwiek funkcję, mieszka w arkuszu tej funkcji i dokłada je `styles`. Inaczej jeden plik, pobierany też przed zalogowaniem, byłby spisem treści @@ -20,7 +20,7 @@
-

astrololo

+

{{ produkt }}

{# Zakładki WYŁĄCZNIE z uprawnień konta. Nie wyszarzamy niedostępnych — ich po prostu nie ma, bo sama obecność wyszarzonej pozycji mówi, że program umie więcej, niż pokazuje (PRE-26). #} diff --git a/services/presentation/app/templates/logowanie.html b/services/presentation/app/templates/logowanie.html index 89dd9a4..9019677 100644 --- a/services/presentation/app/templates/logowanie.html +++ b/services/presentation/app/templates/logowanie.html @@ -3,14 +3,14 @@ - astrololo · logowanie + {{ produkt }} · logowanie {# Ekran logowania nie dziedziczy z base.html (nie ma paska zakładek), więc linkuje arkusz sam. To JEDYNY arkusz publiczny — patrz features.STATIC. #}
-

astrololo

+

{{ produkt }}

{% if request.query_params.get('wylogowano') %}

Wylogowano.

diff --git a/services/presentation/tests/test_compile.py b/services/presentation/tests/test_compile.py index df906d3..c5b0f98 100644 --- a/services/presentation/tests/test_compile.py +++ b/services/presentation/tests/test_compile.py @@ -137,8 +137,8 @@ def _pdf_sample_chart() -> dict: def test_reads_stores_through_their_api_not_raw_storage(): """Format danych ma jednego właściciela — moduł, który je zapisuje. Sięganie tu wprost do localStorage rozjechałoby się przy pierwszej zmianie.""" - assert "window.astrololoNatal" in JS - assert "window.astrololoPredictions" in JS + assert "window.kosmogramNatal" in JS + assert "window.kosmogramPredictions" in JS # chodzi o brak BEZPOŚREDNIEGO dostępu, nie o samo słowo (pada w komentarzu) assert "localStorage.getItem" not in JS assert "localStorage.setItem" not in JS diff --git a/services/presentation/tests/test_predictions.py b/services/presentation/tests/test_predictions.py index b72d8c2..d5d91e8 100644 --- a/services/presentation/tests/test_predictions.py +++ b/services/presentation/tests/test_predictions.py @@ -82,7 +82,7 @@ def test_corrupt_storage_does_not_break_the_page(): def test_storage_is_exposed_for_the_compile_tab(): """Zakładka „Skompiluj" (PRE-23) ma czytać to samo miejsce — jedno źródło prawdy.""" - assert "window.astrololoPredictions" in JS + assert "window.kosmogramPredictions" in JS def test_does_nothing_on_other_tabs(): diff --git a/services/presentation/tests/test_static_js.py b/services/presentation/tests/test_static_js.py index 6b12478..5f3cd7e 100644 --- a/services/presentation/tests/test_static_js.py +++ b/services/presentation/tests/test_static_js.py @@ -46,14 +46,14 @@ def _geo_source() -> str: def _handler_body() -> str: - """Ciało handlera zdarzenia astrololo:coords (wysyłanego przez „Tu i teraz"). + """Ciało handlera zdarzenia kosmogram:coords (wysyłanego przez „Tu i teraz"). Szukamy KONKRETNIE `addEventListener`, nie samej nazwy zdarzenia — ta pada także w komentarzu nagłówkowym pliku i wtedy wycięlibyśmy komentarz zamiast kodu (test przechodziłby albo padał z niewłaściwego powodu). """ src = _geo_source() - start = src.index("addEventListener('astrololo:coords'") + start = src.index("addEventListener('kosmogram:coords'") end = src.index("});", start) return src[start:end] @@ -63,7 +63,7 @@ def test_now_button_handler_refreshes_place_name(): a w polu tekstowym zostawała stara nazwa — formularz pokazywał jedno miejsce, a liczył dla innego.""" assert "reverseName(" in _handler_body(), ( - "handler astrololo:coords nie odświeża nazwy miejsca (reverseName)" + "handler kosmogram:coords nie odświeża nazwy miejsca (reverseName)" )