Ihre Stile hingen als <style> im Shadow DOM, und Markup faellt unter die CSP der Gastgeber-App: Jede fremde App musste deshalb style-src 'unsafe-inline' erlauben, sonst stand die HoneyBar schwarz auf schwarz da. Jetzt kommen sie als konstruiertes Stylesheet ueber die CSSOM herein, die keine CSP kennt. Damit braucht die App gar keine style-src-Erlaubnis mehr -- auch keine fuer die HoneyOS-Domain, was der geplante Weg ueber eine zweite Datei und <link> gekostet haette, samt zweiter Anfrage. Der Strich zum Wiederausklappen hat seinen eigenen Shadow-Root und geht denselben Weg. Der Rueckfall fuer alte Browser laeuft nach innerHTML, sonst raeumt das Markup ihn gleich wieder weg. Gemessen an einer nachgebauten App mit style-src 'self': Leiste bernstein, 32 px, Strich mit Verlauf, keine CSP-Meldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
228 lines
7.8 KiB
Go
228 lines
7.8 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Die HoneyBar darf ihre Stile nicht als <style>-Element mitbringen: Markup
|
|
// faellt unter die CSP der fremden App, ueber die CSSOM gesetzte Stile nicht.
|
|
// Mit einem <style> muesste jede Gastgeber-App wieder 'unsafe-inline' erlauben,
|
|
// und ohne das stuende die HoneyBar dort schwarz auf schwarz — genau so war es
|
|
// bis zum 11.09.2026. Der Rueckfall fuer alte Browser baut das Element per
|
|
// createElement, nicht im Markup; deshalb sucht der Test nach dem Tag.
|
|
func TestHoneyBarBringtKeinStyleElementMit(t *testing.T) {
|
|
b, err := os.ReadFile("web/honeybar/honeybar.js")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
src := string(b)
|
|
|
|
if strings.Contains(src, "<style>") || strings.Contains(src, "<style ") {
|
|
t.Error("honeybar.js enthaelt ein <style>-Element im Markup — das braucht 'unsafe-inline' in der fremden App")
|
|
}
|
|
if !strings.Contains(src, "adoptedStyleSheets") {
|
|
t.Error("honeybar.js setzt keine adoptedStyleSheets mehr — wie kommen die Stile in den Shadow DOM?")
|
|
}
|
|
}
|