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"", strona, re.S)[0].count(' 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 @@
- 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. #}
-
+
{% 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)"
)