astroklient: własna nazwa produktu zamiast odziedziczonej po rdzeniu #85
@@ -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"
|
||||||
@@ -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 };
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,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());
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user