Files
HoneyOS/wetter_test.go
T
marc 3f8b98b869 Eine Biene als Wappen, an allen vier Stellen
Aus dem Quadrat links in beiden Leisten wird eine Pixelbiene von der Seite:
braune Streifen, schwarze Beine, Fluegel mit Kerbe. Sie steht in genau einem
Raster (tools/biene.py, 16x16 Zeichen); daraus entstehen SVG, PNG und eine
Fassung aus Schriftzeichen.

SVG statt PNG in den Leisten: Die HoneyBar laeuft in fremden Apps, und ob ein
<img> dort laedt, entscheidet deren CSP. Markup kann niemand blockieren.

Im System-Fenster und ueber dem Startlauf steht die Biene aus Schriftzeichen —
beides sind Textflaechen, ein Bild darin waere ein Fremdkoerper.

Das Wappen liegt an drei Stellen (Datei, Modul, HoneyBar-Zeile), weil die
Leiste nichts nachladen kann. TestWappenIstUeberallDasselbe haelt sie gleich.
2026-09-02 10:21:03 +02:00

207 lines
6.9 KiB
Go

package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"honeyos/internal/auth"
"honeyos/internal/store"
"honeyos/internal/weather"
)
// Ohne eingestellten Ort ist 204 die richtige Antwort: kein Fehler, nur nichts
// zu zeigen. Die Leiste lässt das Symbol dann weg, statt etwas zu melden.
func TestOhneOrtGibtEsKeinWetter(t *testing.T) {
s := newTestServer(t, "") // ohne Anbieter: offener Betrieb, kein Anmelden nötig
s.wetter = weather.FakeFuerTest(t, 0, 20)
rec := httptest.NewRecorder()
s.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/api/weather", nil))
if rec.Code != http.StatusNoContent {
t.Fatalf("ohne Ort erwartet 204, bekam %d", rec.Code)
}
}
func TestMitOrtKommtDasWetter(t *testing.T) {
s := newTestServer(t, "")
s.wetter = weather.FakeFuerTest(t, 3, 8.6)
if err := s.store.SetSettings(store.LocalSub, map[string]string{
"weatherCity": "Köln", "weatherLat": "50.938", "weatherLon": "6.96",
}); err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
s.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/api/weather", nil))
if rec.Code != http.StatusOK {
t.Fatalf("erwartet 200, bekam %d: %s", rec.Code, rec.Body)
}
var w weather.Wetter
json.Unmarshal(rec.Body.Bytes(), &w)
if w.Ort != "Köln" || w.Glyph != "wolke" {
t.Fatalf("falsches Wetter: %+v", w)
}
}
// Ist ein Ort eingestellt und der Dienst fällt aus, ist das etwas anderes als
// „nichts eingestellt": Es gibt etwas zu zeigen, es kommt nur gerade nicht. Die
// Oberfläche unterscheidet das — 204 verschwindet still, 502 sagt „nicht
// erreichbar". Beides als 204 zu melden hiess: Ein Ausfall sah aus wie ein
// abgeschaltetes Widget.
func TestAusfallIstNichtDasselbeWieNichts(t *testing.T) {
s := newTestServer(t, "")
s.wetter = weather.KaputtFuerTest(t)
if err := s.store.SetSettings(store.LocalSub, map[string]string{
"weatherCity": "Köln", "weatherLat": "50.938", "weatherLon": "6.96",
}); err != nil {
t.Fatal(err)
}
rec := httptest.NewRecorder()
s.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/api/weather", nil))
if rec.Code != http.StatusBadGateway {
t.Fatalf("bei Ausfall erwartet 502, bekam %d", rec.Code)
}
}
// Der Streifen in der Leiste heisst .apps und blendet seine rechten 18 Pixel
// mit einer Maske aus. Bekommt ein zweites Element dieselbe Klasse, erbt es die
// Maske — die Schublade sah rechts aus, als loese sie sich auf, und in der
// Geometrie war nichts zu sehen. Deshalb hier festgenagelt.
func TestNurDerStreifenHeisstApps(t *testing.T) {
b, err := os.ReadFile("web/honeybar/honeybar.js")
if err != nil {
t.Fatal(err)
}
src := string(b)
for _, verboten := range []string{`class="menu apps"`, `class="apps menu"`} {
if strings.Contains(src, verboten) {
t.Errorf("%s: Das Menue erbt damit die Ausblendmaske des App-Streifens", verboten)
}
}
// Genau ein Element darf die Klasse tragen: der Streifen selbst.
if n := strings.Count(src, `class="apps"`); n != 1 {
t.Errorf(`class="apps" kommt %dx vor, erwartet genau einmal (der Streifen)`, n)
}
}
// Die HoneyBar bringt ihr CSS und ihr Markup in Vorlagenzeichenketten mit.
// Ein Backtick in einem Kommentar beendet die Zeichenkette mittendrin — die
// Datei ist dann kaputt, und zwar nur in der HoneyBar, nicht auf dem
// Schreibtisch. Genau das ist zweimal passiert; deshalb dieser Wächter.
func TestHoneyBarOhneBacktickInKommentaren(t *testing.T) {
b, err := os.ReadFile("web/honeybar/honeybar.js")
if err != nil {
t.Fatal(err)
}
src := string(b)
imBlock := false
for nr, zeile := range strings.Split(src, "\n") {
rest := zeile
for {
if imBlock {
ende := strings.Index(rest, "*/")
if ende < 0 {
if strings.Contains(rest, "`") {
t.Errorf("Zeile %d: Backtick in einem Kommentar — das beendet die Vorlagenzeichenkette:\n %s", nr+1, strings.TrimSpace(zeile))
}
break
}
if strings.Contains(rest[:ende], "`") {
t.Errorf("Zeile %d: Backtick in einem Kommentar — das beendet die Vorlagenzeichenkette:\n %s", nr+1, strings.TrimSpace(zeile))
}
rest = rest[ende+2:]
imBlock = false
continue
}
start := strings.Index(rest, "/*")
if start < 0 {
break
}
rest = rest[start+2:]
imBlock = true
}
}
}
// Die Notiz ist eine Notiz, kein Dokument: Die Grenze muss der Server ziehen,
// nicht das Formular — sonst schreibt sie jeder Aufruf aus, der das Formular
// umgeht.
func TestNotizHatEineGrenze(t *testing.T) {
s := newTestServer(t, "")
h := s.routes()
put := func(text string) int {
body, _ := json.Marshal(map[string]string{"text": text})
req := httptest.NewRequest("PUT", "/api/note", strings.NewReader(string(body)))
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec.Code
}
if code := put("# Einkauf\n- [ ] Milch"); code != http.StatusNoContent {
t.Fatalf("kurze Notiz: %d, erwartet 204", code)
}
if code := put(strings.Repeat("x", 4001)); code != http.StatusBadRequest {
t.Errorf("zu lange Notiz: %d, erwartet 400", code)
}
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/api/note", nil))
var out map[string]string
json.Unmarshal(rec.Body.Bytes(), &out)
if out["text"] != "# Einkauf\n- [ ] Milch" {
t.Errorf("zurück kam %q — die zu lange Notiz darf die gute nicht überschrieben haben", out["text"])
}
}
// Fremde Notizen gehen niemanden etwas an: Der Schlüssel für die App-Liste darf
// sie nicht öffnen, auch nicht lesend.
func TestNotizNichtUeberDenSchluessel(t *testing.T) {
idp := newFakeIdP(t, "honeyos")
s := newTestServer(t, idp.URL)
s.cfg.AppsKey = "geheim123"
s.auth = auth.New(s.cfg)
req := httptest.NewRequest("GET", "/api/note", nil)
req.Header.Set("X-HoneyOS-Key", "geheim123")
rec := httptest.NewRecorder()
s.routes().ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("mit Schlüssel: %d, erwartet 401", rec.Code)
}
}
// Das Wappen steht an drei Stellen: als Datei (Favicon), als Modul für den
// Schreibtisch und als Zeile in der HoneyBar — die kann keine Datei nachladen,
// weil sie in fremden Apps läuft und deren CSP das entscheidet.
//
// Drei Kopien laufen auseinander, sobald jemand eine davon von Hand anfasst.
// Erzeugt werden alle von tools/biene.py; dieser Test hält sie zusammen.
func TestWappenIstUeberallDasselbe(t *testing.T) {
roh, err := os.ReadFile("web/img/biene.svg")
if err != nil {
t.Fatalf("web/img/biene.svg fehlt: %v", err)
}
wappen := strings.TrimSpace(string(roh))
if !strings.HasPrefix(wappen, "<svg") || len(wappen) < 500 {
t.Fatalf("web/img/biene.svg sieht nicht nach dem Wappen aus (%d Zeichen)", len(wappen))
}
for _, datei := range []string{"web/honeybar/honeybar.js", "web/js/biene.js"} {
b, err := os.ReadFile(datei)
if err != nil {
t.Fatalf("%s: %v", datei, err)
}
if !strings.Contains(string(b), wappen) {
t.Errorf("%s trägt ein anderes Wappen als web/img/biene.svg — tools/biene.py laufen lassen", datei)
}
}
}