astroklient: własna nazwa produktu zamiast odziedziczonej po rdzeniu
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m20s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Testy astrodemo (pull_request) Successful in 9m25s
Testy / Testy astroklient (pull_request) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 12s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astrodemo (push) Successful in 9m25s
Testy / Testy astroklient (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s

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 <noreply@anthropic.com>
This commit was merged in pull request #85.
This commit is contained in:
2026-08-27 16:37:03 +02:00
parent 54b40857d2
commit 338ec24e90
17 changed files with 149 additions and 74 deletions
+8
View File
@@ -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"
+57
View File
@@ -7,8 +7,12 @@ inaczej sprawdzałyby garść plików, a nie produkt.
""" """
from __future__ import annotations from __future__ import annotations
import json
import pathlib import pathlib
import re
import shutil import shutil
import subprocess
import sys
import pytest import pytest
@@ -35,3 +39,56 @@ def zloz(cel: pathlib.Path) -> pathlib.Path:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def drzewo(tmp_path_factory) -> pathlib.Path: def drzewo(tmp_path_factory) -> pathlib.Path:
return zloz(tmp_path_factory.mktemp("astroklient")) 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"<nav>(.*?)</nav>", strona, re.S)[0].count('<a href="'),
"pula": (zebrane[0] or {}).get("tenant"),
"nazwa_w_naglowku": (re.search(r"<h1>(.*?)</h1>", strona) or [None, "?"])[1],
"nazwa_w_tytule": (re.search(r"<title>(.*?) ·", 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:"):])
@@ -86,3 +86,52 @@ def test_the_removal_list_matches_what_is_actually_composed(drzewo):
def test_no_optional_module_survived(drzewo): def test_no_optional_module_survived(drzewo):
"""Katalogu modułów opcjonalnych nie ma wcale — most odkrywa wtedy zero.""" """Katalogu modułów opcjonalnych nie ma wcale — most odkrywa wtedy zero."""
assert not (drzewo / "app" / "moduly").exists() 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"]
@@ -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 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): def test_the_build_starts_at_all(wynik):
assert wynik["dziala"]["/"] == 200 assert wynik["dziala"]["/"] == 200
@@ -93,5 +93,5 @@
} }
}); });
window.astrololoNatal = { read: read, clear: clear, key: KEY }; window.kosmogramNatal = { read: read, clear: clear, key: KEY };
})(); })();
@@ -146,5 +146,5 @@
}); });
// Udostępniamy magazyn zakładce „Skompiluj" (PRE-23) — jedno źródło prawdy. // 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 };
})(); })();
+4
View File
@@ -27,6 +27,7 @@ from fastapi.templating import Jinja2Templates
from app import rozszerzenia from app import rozszerzenia
from app import chartwheel as chartwheel_mod from app import chartwheel as chartwheel_mod
from app import features from app import features
from app import produkt
from app import session from app import session
from app.house_systems import HOUSE_SYSTEMS, LIMITED as HOUSE_LIMITED, label as house_label 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 templates.env.globals["static"] = static_url
# Pozycje specjalne przefiltrowane uprawnieniem — szablon dostaje gotową listę # Pozycje specjalne przefiltrowane uprawnieniem — szablon dostaje gotową listę
# i nie musi wiedzieć, czego która wymaga. # i nie musi wiedzieć, czego która wymaga.
+12
View File
@@ -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"
+2 -2
View File
@@ -22,11 +22,11 @@
} }
function readNatal() { function readNatal() {
return (window.astrololoNatal && window.astrololoNatal.read()) || null; return (window.kosmogramNatal && window.kosmogramNatal.read()) || null;
} }
function readPredictions() { function readPredictions() {
return (window.astrololoPredictions && window.astrololoPredictions.read()) || []; return (window.kosmogramPredictions && window.kosmogramPredictions.read()) || [];
} }
function item(ok, label, hint) { function item(ok, label, hint) {
+2 -2
View File
@@ -16,7 +16,7 @@
(function () { (function () {
'use strict'; 'use strict';
var KEY = 'astrololo.form.v1'; var KEY = 'kosmogram.form.v1';
// Pola wspólne dla zakładek. Nazwa pola formularza -> jak je znaleźć. // Pola wspólne dla zakładek. Nazwa pola formularza -> jak je znaleźć.
// `geoSearch` (nazwa miejsca) nie ma atrybutu name — szukamy po id. // `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 // 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) // 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); setTimeout(function () { save(collect()); }, 0);
}); });
})(); })();
+2 -2
View File
@@ -2,7 +2,7 @@
// Wpina się do formularza z polami input[name=lat] / input[name=lon]: // Wpina się do formularza z polami input[name=lat] / input[name=lon]:
// - wpisanie nazwy/adresu/POI -> /geocode -> lista trafień -> klik ustawia lat/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ę, // - 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. // Bez internetu wyszukiwarka/kafelki nie działają, ale ręczne lat/lon dalej tak.
(function () { (function () {
function ready(fn) { function ready(fn) {
@@ -163,7 +163,7 @@
// zostawała w nim stara, ręcznie wpisana nazwa, więc formularz pokazywał jedno // 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, // miejsce, a współrzędne wskazywały już inne. reverseName pyta o nazwę punktu,
// na który faktycznie ustawiła się mapa. // na który faktycznie ustawiła się mapa.
document.addEventListener('astrololo:coords', function () { document.addEventListener('kosmogram:coords', function () {
marker.setLatLng([curLat(), curLon()]); marker.setLatLng([curLat(), curLon()]);
map.setView([curLat(), curLon()], Math.max(map.getZoom(), 12)); map.setView([curLat(), curLon()], Math.max(map.getZoom(), 12));
reverseName(curLat(), curLon()); reverseName(curLat(), curLon());
+1 -1
View File
@@ -32,7 +32,7 @@ document.addEventListener('DOMContentLoaded', function () {
function (pos) { function (pos) {
document.querySelector('input[name=lat]').value = pos.coords.latitude.toFixed(4); document.querySelector('input[name=lat]').value = pos.coords.latitude.toFixed(4);
document.querySelector('input[name=lon]').value = pos.coords.longitude.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 ✓'); say('Lokalizacja pobrana ✓');
}, },
function (err) { function (err) {
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>astrololo · {% block title %}{% endblock %}</title> <title>{{ produkt }} · {% block title %}{% endblock %}</title>
{# Arkusz wspólny niesie WYŁĄCZNIE powłokę i logowanie — wszystko, co nazywa {# 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`. 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 Inaczej jeden plik, pobierany też przed zalogowaniem, byłby spisem treści
@@ -20,7 +20,7 @@
<body> <body>
<main> <main>
<header class="topbar"> <header class="topbar">
<h1>astrololo</h1> <h1>{{ produkt }}</h1>
{# Zakładki WYŁĄCZNIE z uprawnień konta. Nie wyszarzamy niedostępnych — {# Zakładki WYŁĄCZNIE z uprawnień konta. Nie wyszarzamy niedostępnych —
ich po prostu nie ma, bo sama obecność wyszarzonej pozycji mówi, że ich po prostu nie ma, bo sama obecność wyszarzonej pozycji mówi, że
program umie więcej, niż pokazuje (PRE-26). #} program umie więcej, niż pokazuje (PRE-26). #}
@@ -3,14 +3,14 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>astrololo · logowanie</title> <title>{{ produkt }} · logowanie</title>
{# Ekran logowania nie dziedziczy z base.html (nie ma paska zakładek), więc {# Ekran logowania nie dziedziczy z base.html (nie ma paska zakładek), więc
linkuje arkusz sam. To JEDYNY arkusz publiczny — patrz features.STATIC. #} linkuje arkusz sam. To JEDYNY arkusz publiczny — patrz features.STATIC. #}
<link rel="stylesheet" href="{{ static('base.css') }}"> <link rel="stylesheet" href="{{ static('base.css') }}">
</head> </head>
<body> <body>
<main class="login-main"> <main class="login-main">
<header class="topbar"><h1>astrololo</h1></header> <header class="topbar"><h1>{{ produkt }}</h1></header>
{% if request.query_params.get('wylogowano') %} {% if request.query_params.get('wylogowano') %}
<p class="done-note">Wylogowano.</p> <p class="done-note">Wylogowano.</p>
+2 -2
View File
@@ -137,8 +137,8 @@ def _pdf_sample_chart() -> dict:
def test_reads_stores_through_their_api_not_raw_storage(): def test_reads_stores_through_their_api_not_raw_storage():
"""Format danych ma jednego właściciela — moduł, który je zapisuje. """Format danych ma jednego właściciela — moduł, który je zapisuje.
Sięganie tu wprost do localStorage rozjechałoby się przy pierwszej zmianie.""" Sięganie tu wprost do localStorage rozjechałoby się przy pierwszej zmianie."""
assert "window.astrololoNatal" in JS assert "window.kosmogramNatal" in JS
assert "window.astrololoPredictions" in JS assert "window.kosmogramPredictions" in JS
# chodzi o brak BEZPOŚREDNIEGO dostępu, nie o samo słowo (pada w komentarzu) # chodzi o brak BEZPOŚREDNIEGO dostępu, nie o samo słowo (pada w komentarzu)
assert "localStorage.getItem" not in JS assert "localStorage.getItem" not in JS
assert "localStorage.setItem" not in JS assert "localStorage.setItem" not in JS
@@ -82,7 +82,7 @@ def test_corrupt_storage_does_not_break_the_page():
def test_storage_is_exposed_for_the_compile_tab(): def test_storage_is_exposed_for_the_compile_tab():
"""Zakładka „Skompiluj" (PRE-23) ma czytać to samo miejsce — jedno źródło prawdy.""" """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(): def test_does_nothing_on_other_tabs():
@@ -46,14 +46,14 @@ def _geo_source() -> str:
def _handler_body() -> 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 Szukamy KONKRETNIE `addEventListener`, nie samej nazwy zdarzenia — ta pada
także w komentarzu nagłówkowym pliku i wtedy wycięlibyśmy komentarz zamiast 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). kodu (test przechodziłby albo padał z niewłaściwego powodu).
""" """
src = _geo_source() src = _geo_source()
start = src.index("addEventListener('astrololo:coords'") start = src.index("addEventListener('kosmogram:coords'")
end = src.index("});", start) end = src.index("});", start)
return src[start:end] 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 w polu tekstowym zostawała stara nazwa — formularz pokazywał jedno miejsce,
a liczył dla innego.""" a liczył dla innego."""
assert "reverseName(" in _handler_body(), ( 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)"
) )