UmbrelApps: community app store whatsnext
Electrum Gate, Evolu Relay en Atreides Assets, met de image-recepten in tools/ en de tests in tests/. Begonnen zonder voorgeschiedenis op 09-10-2026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
85d85a1295
63 files changed
+13987
No files matched your search
@@ -0,0 +1,549 @@
|
||||
"""Toetst dat de pagina omhoog komt zonder certificaat.
|
||||
|
||||
Waarom deze test bestaat. In 0.0.3 startte de app niet: het stream-blok van
|
||||
nginx had een 'listen 50022 ssl' met een include naar de cert.conf van de agent,
|
||||
en nginx weigert te starten als dat certificaat er niet is. Daardoor kwam ook de
|
||||
web-UI niet omhoog, en dat is precies de pagina waarop je een certificaat kiest.
|
||||
Wie geen reverse proxy draait, of twee kandidaten heeft, zag dus niets en kon
|
||||
niets. De symptomen wezen ergens anders heen: de app_proxy meldde alleen dat de
|
||||
server niet te bereiken was.
|
||||
|
||||
De reparatie is een verplaatsing, en die is niet aan de code af te lezen: het
|
||||
stream-blok staat in stream.conf en wordt door entrypoint.sh pas in
|
||||
/var/lib/gate/tls/ gezet als cert.conf bestaat. nginx.conf haalt die map op met
|
||||
een jokerteken, en een jokerteken dat niets matcht is voor nginx geen fout. Eén
|
||||
iemand die dat blok "netjes" terugzet in nginx.conf en de klem is terug, zonder
|
||||
dat een van de andere toetsen iets merkt.
|
||||
|
||||
Sinds 0.1.0 zitten deze bestanden in een eigen image en staan ze in
|
||||
tools/electrum-gate/; tot dan waren het *.template-bestanden in de app-map en
|
||||
was het startscript een command-blok in de compose. De toetsen zijn mee
|
||||
verhuisd en toetsen hetzelfde gedrag, plus wat er bij die verhuizing nieuw
|
||||
kapot kan: de Dockerfile die een bestand niet meeneemt, en de plaatshouders in
|
||||
stream.conf die entrypoint.sh moet invullen.
|
||||
|
||||
Draaien:
|
||||
|
||||
python tests/test_server_start_zonder_certificaat.py
|
||||
|
||||
Er staan een paar toetsen bij die niet over het starten gaan maar wel over
|
||||
dezelfde soort fout: een die niets meldt en pas opvalt als iemand het toevallig
|
||||
ziet. De verbindingsteller die stil nul telt, een mount die weer alleen-lezen wordt,
|
||||
en twee taglines die uit elkaar lopen. Ze staan hier en niet in een derde bestand,
|
||||
want de suite heeft geen runner en een bestand dat niemand aanroept toetst niets.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
import os # noqa: E402
|
||||
import re # noqa: E402
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
APP = os.path.join(HERE, os.pardir, "whatsnext-electrum-gate")
|
||||
RECEPT = os.path.join(HERE, os.pardir, "tools", "electrum-gate")
|
||||
|
||||
NGINX_CONF = os.path.join(RECEPT, "nginx.conf")
|
||||
STREAM_CONF = os.path.join(RECEPT, "stream.conf")
|
||||
ENTRYPOINT = os.path.join(RECEPT, "entrypoint.sh")
|
||||
DOCKERFILE = os.path.join(RECEPT, "Dockerfile")
|
||||
BUILD = os.path.join(RECEPT, "build.sh")
|
||||
PAGINA = os.path.join(RECEPT, "index.html")
|
||||
COMPOSE = os.path.join(APP, "docker-compose.yml")
|
||||
MANIFEST = os.path.join(APP, "umbrel-app.yml")
|
||||
|
||||
|
||||
class Uitslag:
|
||||
def __init__(self):
|
||||
self.goed = 0
|
||||
self.fout = []
|
||||
|
||||
def check(self, naam, gelukt, uitleg=""):
|
||||
if gelukt:
|
||||
self.goed += 1
|
||||
else:
|
||||
self.fout.append(naam + ((" - " + uitleg) if uitleg else ""))
|
||||
|
||||
def rapport(self):
|
||||
print()
|
||||
print("%d goed, %d fout" % (self.goed, len(self.fout)))
|
||||
for f in self.fout:
|
||||
print(" FOUT: " + f)
|
||||
return 0 if not self.fout else 1
|
||||
|
||||
|
||||
def lees(pad):
|
||||
with open(pad, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def zonder_commentaar(tekst):
|
||||
"""De regels die nginx of de shell daadwerkelijk uitvoert.
|
||||
|
||||
Zonder dit zou elke toets hieronder afgaan op de uitleg erboven, en die
|
||||
noemt juist de dingen die er niet meer mogen staan.
|
||||
"""
|
||||
regels = []
|
||||
for regel in tekst.splitlines():
|
||||
kaal = regel.strip()
|
||||
if not kaal or kaal.startswith("#"):
|
||||
continue
|
||||
regels.append(kaal)
|
||||
return regels
|
||||
|
||||
|
||||
def test_pagina_hangt_niet_aan_een_certificaat(u):
|
||||
"""Geen TLS-directive in nginx.conf, want die blokkeert de start."""
|
||||
regels = zonder_commentaar(lees(NGINX_CONF))
|
||||
|
||||
u.check("nginx.conf heeft geen listen met ssl",
|
||||
not any("listen" in r and "ssl" in r for r in regels),
|
||||
"een 'listen ssl' zonder certificaat laat nginx niet starten, "
|
||||
"en dan is er ook geen pagina om er een te kiezen")
|
||||
|
||||
u.check("nginx.conf includet cert.conf niet rechtstreeks",
|
||||
not any("cert.conf" in r for r in regels),
|
||||
"een include van een bestand dat er nog niet is, is een startfout")
|
||||
|
||||
u.check("nginx.conf haalt het stream-blok met een jokerteken op",
|
||||
any(r.startswith("include") and "/var/lib/gate/tls/*" in r
|
||||
for r in regels),
|
||||
"zonder jokerteken is een lege map alsnog een startfout")
|
||||
|
||||
u.check("de pagina zelf luistert nog wel",
|
||||
any(r.startswith("listen 80") for r in regels))
|
||||
|
||||
|
||||
def test_stream_blok_staat_apart(u):
|
||||
"""Het TLS-deel is compleet, en het backend-adres komt uit de omgeving."""
|
||||
u.check("stream.conf bestaat", os.path.isfile(STREAM_CONF))
|
||||
if not os.path.isfile(STREAM_CONF):
|
||||
return
|
||||
|
||||
regels = zonder_commentaar(lees(STREAM_CONF))
|
||||
|
||||
u.check("stream.conf luistert op 50022 met ssl",
|
||||
any("listen 50022 ssl" in r for r in regels))
|
||||
u.check("stream.conf includet de cert.conf van de agent",
|
||||
any("include /var/lib/gate/cert.conf" in r for r in regels))
|
||||
u.check("stream.conf heeft zijn log_format zelf",
|
||||
any(r.startswith("log_format gate") for r in regels),
|
||||
"tot 0.0.29 schreef het command-blok dat in een los bestand, omdat "
|
||||
"een dollarteken in een template leeggemaakt werd; dat hoeft niet meer")
|
||||
u.check("en schrijft de sessielog waar de agent hem leest",
|
||||
any(r.startswith("access_log /var/lib/gate/stream.log") for r in regels))
|
||||
|
||||
# Het adres van de backend staat niet in dit bestand maar komt uit de
|
||||
# omgeving, en de weg daarheen is een plaatshouder die entrypoint.sh invult.
|
||||
# Geen dollarteken in die plaatshouder: dan kan geen envsubst-stap, van
|
||||
# umbreld of van wie dan ook, hem stil leegmaken.
|
||||
proxy = [r for r in regels if r.startswith("proxy_pass")]
|
||||
u.check("stream.conf heeft precies één proxy_pass", len(proxy) == 1,
|
||||
"gevonden: %r" % proxy)
|
||||
u.check("en die verwijst naar de plaatshouders voor host en poort",
|
||||
proxy and "__GATE_ELECTRUM_HOST__:__GATE_ELECTRUM_PORT__" in proxy[0],
|
||||
"gevonden: %r" % proxy)
|
||||
|
||||
u.check("stream.conf bevat geen accolade-variabele meer",
|
||||
"${" not in lees(STREAM_CONF),
|
||||
"die wordt sinds de eigen image door niemand meer ingevuld en zou "
|
||||
"letterlijk in de configuratie belanden")
|
||||
|
||||
|
||||
def test_entrypoint_wacht_niet_op_de_agent(u):
|
||||
"""entrypoint.sh start nginx altijd, en zet TLS erbij als het kan."""
|
||||
u.check("entrypoint.sh bestaat", os.path.isfile(ENTRYPOINT))
|
||||
if not os.path.isfile(ENTRYPOINT):
|
||||
return
|
||||
|
||||
tekst = lees(ENTRYPOINT)
|
||||
regels = zonder_commentaar(tekst)
|
||||
|
||||
u.check("geen wachtlus op cert.conf",
|
||||
not any("! -f" in r and "cert.conf" in r and "while" in r
|
||||
for r in regels),
|
||||
"dat is precies de klem uit 0.0.3: geen certificaat, geen nginx, "
|
||||
"geen pagina, dus geen manier om een certificaat te kiezen")
|
||||
|
||||
u.check("het stream-blok wordt ingevuld uit de omgeving",
|
||||
any("__GATE_ELECTRUM_HOST__" in r and "GATE_ELECTRUM_HOST" in r
|
||||
for r in regels)
|
||||
and any("__GATE_ELECTRUM_PORT__" in r for r in regels),
|
||||
"anders staat de plaatshouder letterlijk in de proxy_pass")
|
||||
|
||||
u.check("en wordt uit stream.conf.in gelezen, zoals de Dockerfile hem neerzet",
|
||||
any("/etc/nginx/stream.conf.in" in r for r in regels))
|
||||
|
||||
u.check("het stream-blok wordt weggezet als cert.conf bestaat",
|
||||
any(r.startswith("if cp /etc/nginx/stream.conf") and "/tls/" in r
|
||||
for r in regels))
|
||||
|
||||
u.check("en weggehaald als cert.conf er niet is",
|
||||
any(r.startswith("rm -f") and "/tls/stream.conf" in r for r in regels),
|
||||
"een ingetrokken keuze moet de poort ook echt sluiten")
|
||||
|
||||
u.check("een mislukte herlading breekt de wachtlus niet af",
|
||||
any("if ! nginx -s reload" in r for r in regels),
|
||||
"onder 'set -e' zou de lus verdwijnen en daarna pikt niets meer "
|
||||
"een wijziging op")
|
||||
|
||||
u.check("nginx wordt met exec het hoofdproces",
|
||||
regels and regels[-1].startswith("exec nginx"),
|
||||
"anders blijft de shell PID 1, geeft signalen niet door, en ziet "
|
||||
"Docker een geslaagde afsluiting als nginx omvalt; laatste regel: %r"
|
||||
% (regels[-1] if regels else None))
|
||||
|
||||
u.check("het script begint met een shebang",
|
||||
tekst.startswith("#!/bin/sh"))
|
||||
|
||||
u.check("geen verdubbelde dollartekens meer",
|
||||
"$$" not in "\n".join(regels),
|
||||
"dat was de ontsnapping voor het command-blok in de compose; in een "
|
||||
"gewoon script is $$ het proces-id")
|
||||
|
||||
|
||||
def test_dockerfile_neemt_alles_mee(u):
|
||||
"""Wat entrypoint.sh en nginx.conf verwachten, zet de Dockerfile ook neer.
|
||||
|
||||
Dit is de nieuwe klasse fouten van de eigen image: een bestand dat in de
|
||||
repo staat maar niet in de image, en dan faalt de container pas op het
|
||||
apparaat. Geen Docker nodig om dat te zien; de paden staan in de tekst.
|
||||
"""
|
||||
u.check("Dockerfile bestaat", os.path.isfile(DOCKERFILE))
|
||||
if not os.path.isfile(DOCKERFILE):
|
||||
return
|
||||
|
||||
regels = zonder_commentaar(lees(DOCKERFILE))
|
||||
copies = [r for r in regels if r.startswith("COPY ")]
|
||||
|
||||
def kopieert(bron, doel):
|
||||
return any(bron in r and r.endswith(" " + doel) for r in copies)
|
||||
|
||||
u.check("nginx.conf gaat naar /etc/nginx/nginx.conf",
|
||||
kopieert("tools/electrum-gate/nginx.conf", "/etc/nginx/nginx.conf"))
|
||||
u.check("stream.conf gaat naar /etc/nginx/stream.conf.in",
|
||||
kopieert("tools/electrum-gate/stream.conf", "/etc/nginx/stream.conf.in"),
|
||||
"entrypoint.sh leest dat pad en vult het in naar stream.conf")
|
||||
u.check("de pagina gaat naar de webroot",
|
||||
kopieert("tools/electrum-gate/index.html", "/usr/share/nginx/html/index.html"))
|
||||
u.check("het icoon komt uit de app-map",
|
||||
kopieert("whatsnext-electrum-gate/icon.png", "/usr/share/nginx/html/icon.png"),
|
||||
"de app-map houdt het origineel omdat het manifest ernaar wijst; "
|
||||
"de image kopieert het, zodat er niet twee exemplaren zijn")
|
||||
u.check("de agent gaat naar /app/agent.py",
|
||||
kopieert("tools/electrum-gate/agent.py", "/app/agent.py"),
|
||||
"de compose start hem op dat pad")
|
||||
u.check("entrypoint.sh gaat mee en wordt uitvoerbaar",
|
||||
kopieert("tools/electrum-gate/entrypoint.sh", "/entrypoint.sh")
|
||||
and any(r.startswith("RUN chmod") and "/entrypoint.sh" in r for r in regels),
|
||||
"git op Windows kent geen uitvoerbit, dus die moet de Dockerfile zetten")
|
||||
u.check("python3 wordt geïnstalleerd",
|
||||
any(r.startswith("RUN apk add") and "python3" in r for r in regels))
|
||||
u.check("de basisimage is nginx op een vaste tag",
|
||||
any(re.match(r"FROM nginx:\d+\.\d+-alpine$", r) for r in regels),
|
||||
"nginx:alpine beweegt mee en is ook mét digest niet toegestaan; "
|
||||
"zie HomeGit/Docs/Referenties/UmbrelApps/Images-pinnen.md")
|
||||
u.check("het entrypoint van de nginx-image is uitgeschakeld",
|
||||
"ENTRYPOINT []" in regels,
|
||||
"anders draait het envsubst-script van die image mee, en dat is "
|
||||
"precies het soort stille invulling dat dit project vreest")
|
||||
|
||||
# De compose noemt paden in de image; die moeten er zijn.
|
||||
compose = zonder_commentaar(lees(COMPOSE))
|
||||
u.check("de compose start de agent op het pad waar de Dockerfile hem neerzet",
|
||||
any(r == "- /app/agent.py" for r in compose))
|
||||
|
||||
|
||||
def test_compose_en_recept_lopen_gelijk(u):
|
||||
"""Dezelfde image in beide services, met de tag uit build.sh.
|
||||
|
||||
Twee services die elk hun eigen tag hebben lopen uit elkaar bij de eerste
|
||||
verhoging die iemand op één plek doet. En een tag die niet gelijk is aan
|
||||
VERSION in build.sh betekent een image die nooit gebouwd is.
|
||||
"""
|
||||
compose = zonder_commentaar(lees(COMPOSE))
|
||||
images = [r.split(":", 1)[1].strip() for r in compose if r.startswith("image:")]
|
||||
u.check("de compose heeft twee images", len(images) == 2, "gevonden: %r" % images)
|
||||
|
||||
tags = set()
|
||||
for image in images:
|
||||
zonder_digest = image.split("@", 1)[0]
|
||||
tags.add(zonder_digest)
|
||||
u.check("en het is twee keer dezelfde", len(tags) == 1, "gevonden: %r" % sorted(tags))
|
||||
|
||||
versie = None
|
||||
for regel in zonder_commentaar(lees(BUILD)):
|
||||
if regel.startswith("VERSION="):
|
||||
versie = regel.split("=", 1)[1].strip().strip('"')
|
||||
u.check("build.sh heeft een VERSION", bool(versie))
|
||||
if versie and tags:
|
||||
tag = sorted(tags)[0]
|
||||
u.check("de tag in de compose is VERSION uit build.sh",
|
||||
tag.endswith(":" + versie),
|
||||
"compose %r, build.sh %r" % (tag, versie))
|
||||
|
||||
# Wat er niet meer gemount mag worden: de programmabestanden. Zit er weer
|
||||
# een mount van een los bestand uit de app-map in, dan is dat een terugval
|
||||
# naar de template-tijd en bereikt een wijziging het apparaat via twee wegen.
|
||||
losse = [r for r in compose if r.startswith("- ${APP_DATA_DIR}/")
|
||||
and "/" not in r[len("- ${APP_DATA_DIR}/"):].split(":", 1)[0]]
|
||||
u.check("geen losse bestanden uit de app-map meer gemount",
|
||||
not losse, "gevonden: %r" % losse)
|
||||
|
||||
u.check("de server krijgt het backend-adres uit de omgeving",
|
||||
compose.count("GATE_ELECTRUM_HOST: ${APP_ELECTRS_NODE_IP}") == 2
|
||||
and compose.count("GATE_ELECTRUM_PORT: ${APP_ELECTRS_NODE_PORT}") == 2,
|
||||
"agent én server hebben het nodig; entrypoint.sh vult er het "
|
||||
"stream-blok mee in")
|
||||
|
||||
u.check("de agent krijgt de versie van het manifest door",
|
||||
"GATE_APP_VERSION: ${APP_VERSION}" in compose,
|
||||
"anders toont de kop van de pagina geen versie")
|
||||
|
||||
|
||||
def test_uploadpad(u):
|
||||
"""De drie dingen buiten de agent die uploaden mogelijk maken.
|
||||
|
||||
Alle drie kunnen ze stil wegvallen: een mount die weer `:ro` wordt geeft een
|
||||
foutmelding pas als iemand iets uploadt, en een te kleine limiet in nginx
|
||||
geeft een 413 die niets uitlegt.
|
||||
"""
|
||||
compose = zonder_commentaar(lees(COMPOSE))
|
||||
nginx = zonder_commentaar(lees(NGINX_CONF))
|
||||
|
||||
eigen = [r for r in compose if "/certs/own" in r and r.startswith("-")]
|
||||
u.check("de eigen certificaatmap is twee keer gemount",
|
||||
len(eigen) == 2, "gevonden: %r" % eigen)
|
||||
u.check("en precies één daarvan is beschrijfbaar",
|
||||
len([r for r in eigen if not r.endswith(":ro")]) == 1,
|
||||
"zonder :ro kan de agent niets neerzetten, met :ro overal ook niet: %r"
|
||||
% eigen)
|
||||
|
||||
u.check("GATE_UPLOAD_DIR staat in de omgeving van de agent",
|
||||
any(r.startswith("GATE_UPLOAD_DIR:") for r in compose),
|
||||
"leeg of afwezig betekent: uploaden staat uit")
|
||||
|
||||
u.check("nginx heeft een eigen locatie voor de upload",
|
||||
any("location = /api/certificate/upload" in r for r in nginx))
|
||||
# De limiet moet groter zijn dan die van /api/, anders kapt nginx het verzoek
|
||||
# af voordat de agent er iets over kan zeggen.
|
||||
limieten = []
|
||||
for regel in nginx:
|
||||
if regel.startswith("client_max_body_size"):
|
||||
limieten.append(regel.rstrip(";").split()[-1])
|
||||
u.check("en een grotere limiet dan het gewone api-pad",
|
||||
len(limieten) == 2 and limieten[0] != limieten[1],
|
||||
"gevonden limieten: %r" % limieten)
|
||||
|
||||
|
||||
def test_verbindingsteller(u):
|
||||
"""De teller in de nginx-container, en de poort in hex.
|
||||
|
||||
Het gevaarlijke deel is de hex: de teller zoekt in /proc/net/tcp op de poort
|
||||
in hexadecimale vorm, en verandert de TLS-poort ooit, dan telt hij stil nul.
|
||||
Een teller die altijd nul zegt, ziet eruit als "geen wallet verbonden" en
|
||||
niet als een fout.
|
||||
"""
|
||||
regels = zonder_commentaar(lees(ENTRYPOINT))
|
||||
|
||||
teller = [r for r in regels if "/proc/net/tcp" in r]
|
||||
u.check("de teller leest /proc/net/tcp", bool(teller),
|
||||
"dat is de enige plek waar de open verbindingen van deze container "
|
||||
"staan; de agent zit in een andere netwerk-namespace")
|
||||
|
||||
u.check("en schrijft het aantal weg voor de agent",
|
||||
any("/sessions" in r for r in regels))
|
||||
|
||||
# De poort uit de omgeving van de agent is de waarheid; de teller moet
|
||||
# dezelfde poort in hex zoeken.
|
||||
compose = zonder_commentaar(lees(COMPOSE))
|
||||
poorten = [r for r in compose if r.startswith("GATE_TLS_PORT:")]
|
||||
u.check("GATE_TLS_PORT staat in de compose", len(poorten) == 1,
|
||||
"gevonden: %r" % poorten)
|
||||
if not poorten or not teller:
|
||||
return
|
||||
|
||||
poort = int(poorten[0].split(":", 1)[1].strip().strip('"'))
|
||||
hexpoort = "%X" % poort
|
||||
# Over de regels heen zoeken, want de awk-regel staat achter een
|
||||
# backslash-vervolg en dan valt de hex op een eigen regel.
|
||||
u.check("de teller zoekt de TLS-poort in hex (%d is %s)" % (poort, hexpoort),
|
||||
(":" + hexpoort) in " ".join(regels),
|
||||
"nergens in entrypoint.sh staat :%s, dus telt de teller nul"
|
||||
% hexpoort)
|
||||
|
||||
stream = zonder_commentaar(lees(STREAM_CONF))
|
||||
u.check("en stream.conf luistert op diezelfde poort",
|
||||
any(r == "listen %d ssl;" % poort for r in stream))
|
||||
|
||||
|
||||
def test_data_onder_data(u):
|
||||
"""Alles wat de app zelf schrijft staat onder `data/`.
|
||||
|
||||
De conventie van de officiele appstore, afgelezen aan echte apps: electrs
|
||||
mount `${APP_DATA_DIR}/data/electrs`, mempool `${APP_DATA_DIR}/data`. Deze app
|
||||
had `runtime/` en `certs/` naast de templates staan. Verplaatst op 20-08-2026
|
||||
met het oog op publicatie.
|
||||
|
||||
Waarom er een toets op staat: bij de volgende mount die iemand toevoegt is dit
|
||||
precies het detail dat je vergeet, en niets gaat er stuk van. Het valt pas op
|
||||
bij het inleveren.
|
||||
"""
|
||||
regels = zonder_commentaar(lees(COMPOSE))
|
||||
mounts = [r[1:].strip() for r in regels
|
||||
if r.startswith("-") and "${APP_DATA_DIR}" in r]
|
||||
u.check("er zijn mounts uit de app-datamap", bool(mounts))
|
||||
|
||||
fout = []
|
||||
for mount in mounts:
|
||||
host = mount.strip('"').split(":")[0].replace("${APP_DATA_DIR}/", "")
|
||||
if not host.startswith("data/"):
|
||||
fout.append(host)
|
||||
u.check("elke mount uit de app-map staat onder data/",
|
||||
not fout, "deze niet: %r" % fout)
|
||||
|
||||
u.check("de gedeelde toestand staat onder data/",
|
||||
any("${APP_DATA_DIR}/data/runtime:" in m for m in mounts))
|
||||
u.check("de eigen certificaatmap staat onder data/",
|
||||
any("${APP_DATA_DIR}/data/certs:" in m for m in mounts))
|
||||
|
||||
# En het manifest moet die paden ook noemen, anders wijst backupIgnore naar
|
||||
# iets wat niet bestaat en gaat de sessielog alsnog mee in elke back-up.
|
||||
manifest = lees(MANIFEST)
|
||||
u.check("backupIgnore staat in het manifest", "backupIgnore:" in manifest)
|
||||
for pad in ("data/runtime/stream.log", "data/runtime/status.json"):
|
||||
u.check("backupIgnore noemt %s" % pad, pad in manifest)
|
||||
|
||||
|
||||
# De volgorde die de packaging-documentatie van de officiele appstore voorschrijft.
|
||||
# Wat de spec niet noemt, zoals icon en backupIgnore, hoort daarachter en niet
|
||||
# ertussen: dan is de kop van het manifest letterlijk goed.
|
||||
MANIFEST_ORDE = [
|
||||
"manifestVersion", "id", "category", "name", "version", "tagline",
|
||||
"description", "releaseNotes", "developer", "website", "dependencies",
|
||||
"repo", "support", "port", "gallery", "path",
|
||||
]
|
||||
|
||||
|
||||
def manifest_velden():
|
||||
"""De veldnamen op het hoogste niveau, in de volgorde van het bestand."""
|
||||
velden = []
|
||||
for regel in lees(MANIFEST).splitlines():
|
||||
if not regel or regel[0] in " #-":
|
||||
continue
|
||||
if ":" in regel:
|
||||
velden.append(regel.split(":", 1)[0])
|
||||
return velden
|
||||
|
||||
|
||||
def test_manifest_volgorde(u):
|
||||
"""De velden staan in de voorgeschreven volgorde.
|
||||
|
||||
Geen smaak: de officiele appstore schrijft deze volgorde voor. Een toets
|
||||
hierop omdat een nieuw veld standaard onderaan of middenin belandt, en er
|
||||
niets van stukgaat; het valt pas op bij het inleveren.
|
||||
"""
|
||||
velden = manifest_velden()
|
||||
kern = [v for v in velden if v in MANIFEST_ORDE]
|
||||
|
||||
ontbreekt = [v for v in MANIFEST_ORDE if v not in velden]
|
||||
u.check("alle voorgeschreven velden staan in het manifest",
|
||||
not ontbreekt, "mist: %r" % ontbreekt)
|
||||
u.check("en in de voorgeschreven volgorde",
|
||||
kern == MANIFEST_ORDE, "de volgorde is nu: %r" % kern)
|
||||
|
||||
# De extra's horen achter de voorgeschreven reeks. Zo is de kop van het
|
||||
# bestand letterlijk op orde en hoeft er bij inlevering alleen iets weg.
|
||||
if kern == MANIFEST_ORDE and velden:
|
||||
laatste_kern = velden.index(MANIFEST_ORDE[-1])
|
||||
extra_ervoor = [v for v in velden[:laatste_kern] if v not in MANIFEST_ORDE]
|
||||
u.check("en wat de spec niet noemt staat erachter",
|
||||
not extra_ervoor, "deze staan ertussen: %r" % extra_ervoor)
|
||||
|
||||
u.check("de lege standaard-inloggegevens zijn eruit",
|
||||
"defaultUsername" not in velden and "defaultPassword" not in velden,
|
||||
"deze app heeft geen eigen inlog, dus een leeg veld zegt niets")
|
||||
|
||||
|
||||
def test_releasenotes_hebben_een_geschiedenis(u):
|
||||
"""Eén verhaal over deze versie, dan één regel per eerdere versie.
|
||||
|
||||
Deze toets bestaat om een echte fout: drie versies achter elkaar kwam er een
|
||||
nieuwe kop bovenop de release notes terwijl de oude tekst eronder bleef staan.
|
||||
In 0.0.9 stond er daardoor drie keer "Earlier releases" en twee keer dezelfde
|
||||
regel over 0.0.4. Dat is zichtbare tekst in de appstore, en niemand leest zijn
|
||||
eigen release notes nog een keer na.
|
||||
"""
|
||||
tekst = lees(MANIFEST)
|
||||
u.check("er staat precies een keer een geschiedenis-kop",
|
||||
tekst.count("Earlier releases:") == 1,
|
||||
"gevonden: %d keer" % tekst.count("Earlier releases:"))
|
||||
|
||||
# Elke eerdere versie mag maar een keer genoemd worden.
|
||||
dubbel = []
|
||||
for versie in ("0.0.4", "0.0.5", "0.0.6", "0.0.7", "0.0.8"):
|
||||
aantal = tekst.count("\n " + versie + " ")
|
||||
if aantal > 1:
|
||||
dubbel.append((versie, aantal))
|
||||
u.check("en geen versie wordt twee keer beschreven",
|
||||
not dubbel, "dubbel: %r" % dubbel)
|
||||
|
||||
|
||||
def test_tagline_is_overal_dezelfde(u):
|
||||
"""De regel onder de app-naam staat op twee plekken en moet gelijk zijn.
|
||||
|
||||
Besloten door de gebruiker op 20-08-2026, nadat hij zag dat de winkel en de
|
||||
pagina iets anders zeiden. Eén app, één regel. Deze toets bestaat omdat de
|
||||
twee plekken ver uit elkaar staan: niemand die de tagline in het manifest
|
||||
aanpast, opent daarna de pagina.
|
||||
"""
|
||||
tagline = None
|
||||
for regel in lees(MANIFEST).splitlines():
|
||||
if regel.startswith("tagline:"):
|
||||
tagline = regel.split(":", 1)[1].strip().strip('"')
|
||||
break
|
||||
|
||||
u.check("het manifest heeft een tagline", bool(tagline))
|
||||
if not tagline:
|
||||
return
|
||||
|
||||
u.check("en de pagina gebruikt woordelijk dezelfde regel",
|
||||
tagline in lees(PAGINA),
|
||||
"de pagina noemt hem niet: %r" % tagline)
|
||||
|
||||
|
||||
def test_pagina_zonder_invulling(u):
|
||||
"""De pagina zit in de image en wordt door niemand meer ingevuld.
|
||||
|
||||
Een ${...} die blijft staan komt letterlijk in de browser terecht. En de twee
|
||||
waarden die umbreld vroeger invulde, de versie en het backend-adres, moeten
|
||||
nu uit status.json komen; anders is de kop leeg en het adres ook.
|
||||
"""
|
||||
tekst = lees(PAGINA)
|
||||
u.check("de pagina bevat geen accolade-variabele", "${" not in tekst)
|
||||
u.check("de kop haalt de versie uit status.json",
|
||||
"s.version" in tekst and 'id="head-version"' in tekst)
|
||||
u.check("het backend-adres komt uit status.json",
|
||||
"backend.address" in tekst)
|
||||
|
||||
|
||||
def main():
|
||||
u = Uitslag()
|
||||
test_pagina_hangt_niet_aan_een_certificaat(u)
|
||||
test_stream_blok_staat_apart(u)
|
||||
test_entrypoint_wacht_niet_op_de_agent(u)
|
||||
test_dockerfile_neemt_alles_mee(u)
|
||||
test_compose_en_recept_lopen_gelijk(u)
|
||||
test_verbindingsteller(u)
|
||||
test_uploadpad(u)
|
||||
test_data_onder_data(u)
|
||||
test_manifest_volgorde(u)
|
||||
test_releasenotes_hebben_een_geschiedenis(u)
|
||||
test_tagline_is_overal_dezelfde(u)
|
||||
test_pagina_zonder_invulling(u)
|
||||
return u.rapport()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user