refactor: aufgeraeumt — Pakete hinten, Widgets vorne

Go: store, auth, weather und config sind eigene Pakete unter internal/. Vorher
lagen fuenf Dateien in einem Topf, und jede kannte jede.

Web: os.js hatte 1425 Zeilen und alles darin. Jetzt core.js (Zustand, Server,
Einstellungen), glyphs.js (die Pixelraster), js/widgets/ mit einem Widget je
Datei, os.js nur noch die Ansichten. Stylesheet nach css/.

Die HoneyBar bleibt eine einzelne Datei ohne Importe — sie wird von fremden
Seiten mit einer Zeile eingebunden, da darf nichts nachladen.

Nebenbei: Das gebaute Binary lag mit 9,8 MB im Repo und ist jetzt ignoriert.
Und der Test gegen eine zweite Versionsnummer geht nicht mehr auf os.js, sondern
durch alle .js — sonst haette ihn dieser Umbau still ausgehebelt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-08-29 20:51:21 +02:00
1 parent d08b0a229c
commit 52deffa30e
20 files changed
+780 -624

No files matched your search

+49 -34
View File
@@ -1,6 +1,11 @@
package main
import (
"honeyos/internal/auth"
"honeyos/internal/config"
"honeyos/internal/store"
"io/fs"
"crypto"
"crypto/rand"
"crypto/rsa"
@@ -98,18 +103,18 @@ func (f *fakeIdP) idToken(t *testing.T) string {
func newTestServer(t *testing.T, issuer string) *Server {
t.Helper()
store, err := OpenStore(dataPath(t.TempDir()))
st, err := store.Open(store.DataPath(t.TempDir()))
if err != nil {
t.Fatal(err)
}
cfg := Config{
cfg := config.Config{
PublicURL: "https://os.example.test",
SessionSecret: strings.Repeat("k", 40),
OIDCIssuer: issuer,
OIDCClientID: "honeyos",
OIDCClientSecret: "geheim",
}
return &Server{cfg: cfg, auth: NewAuth(cfg), store: store}
return &Server{cfg: cfg, auth: auth.New(cfg), store: st}
}
// TestAnmeldeablauf geht den Weg einmal ganz durch: Umleitung zum Anbieter,
@@ -140,7 +145,7 @@ func TestAnmeldeablauf(t *testing.T) {
var stateCk *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == stateCookie {
if c.Name == auth.StateCookie {
stateCk = c
}
}
@@ -163,9 +168,9 @@ func TestAnmeldeablauf(t *testing.T) {
var sess, csrf *http.Cookie
for _, c := range rec.Result().Cookies() {
switch c.Name {
case sessionCookie:
case auth.SessionCookie:
sess = c
case csrfCookie:
case auth.CSRFCookie:
csrf = c
}
}
@@ -215,7 +220,7 @@ func TestGefälschtesCookieZähltNicht(t *testing.T) {
h := newTestServer(t, idp.URL).routes()
req := httptest.NewRequest("GET", "/api/me", nil)
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "c3ViLTk5OQ.9999999999.gefälscht"})
req.AddCookie(&http.Cookie{Name: auth.SessionCookie, Value: "c3ViLTk5OQ.9999999999.gefälscht"})
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
@@ -315,7 +320,7 @@ func TestOffenerBetrieb(t *testing.T) {
if rec.Code != http.StatusNoContent {
t.Fatalf("Schreiben im offenen Betrieb: %d", rec.Code)
}
if s.store.User(localSub).Settings["phosphor"] != "green" {
if s.store.User(store.LocalSub).Settings["phosphor"] != "green" {
t.Error("Einstellung im offenen Betrieb nicht gespeichert")
}
}
@@ -326,7 +331,7 @@ func TestLeisteNurFürEingetrageneApps(t *testing.T) {
idp := newFakeIdP(t, "honeyos")
s := newTestServer(t, idp.URL)
h := s.routes()
if err := s.store.SetApps([]App{{ID: "aether", Name: "AETHER", URL: "https://cloud.example.test/"}}); err != nil {
if err := s.store.SetApps([]store.App{{ID: "aether", Name: "AETHER", URL: "https://cloud.example.test/"}}); err != nil {
t.Fatal(err)
}
@@ -361,7 +366,7 @@ func signIn(t *testing.T, h http.Handler) (sess, csrf *http.Cookie) {
loc, _ := url.Parse(rec.Header().Get("Location"))
var stateCk *http.Cookie
for _, c := range rec.Result().Cookies() {
if c.Name == stateCookie {
if c.Name == auth.StateCookie {
stateCk = c
}
}
@@ -375,9 +380,9 @@ func signIn(t *testing.T, h http.Handler) (sess, csrf *http.Cookie) {
h.ServeHTTP(rec, req)
for _, c := range rec.Result().Cookies() {
switch c.Name {
case sessionCookie:
case auth.SessionCookie:
sess = c
case csrfCookie:
case auth.CSRFCookie:
csrf = c
}
}
@@ -390,18 +395,18 @@ func signIn(t *testing.T, h http.Handler) (sess, csrf *http.Cookie) {
// ── Betrieb hinter einem Anmelde-Proxy ─────────────────────────────────────
// Kein eigener Anmeldeschirm: Pangolin o. ä. hat die Tür schon zugemacht.
func proxyServer(t *testing.T, cfg Config) *Server {
func proxyServer(t *testing.T, cfg config.Config) *Server {
t.Helper()
store, err := OpenStore(dataPath(t.TempDir()))
st, err := store.Open(store.DataPath(t.TempDir()))
if err != nil {
t.Fatal(err)
}
cfg.SessionSecret = strings.Repeat("k", 40)
return &Server{cfg: cfg, auth: NewAuth(cfg), store: store}
return &Server{cfg: cfg, auth: auth.New(cfg), store: st}
}
func TestProxyBetriebLässtDurch(t *testing.T) {
h := proxyServer(t, Config{ProxyAuth: true}).routes()
h := proxyServer(t, config.Config{ProxyAuth: true}).routes()
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest("GET", "/api/me", nil))
@@ -420,7 +425,7 @@ func TestProxyBetriebLässtDurch(t *testing.T) {
// Nennt der Proxy einen Namen, bekommt jeder Name seine eigenen Einstellungen.
func TestProxyKopfzeileTrenntEinstellungen(t *testing.T) {
s := proxyServer(t, Config{ProxyAuth: true, UserHeader: "Remote-User"})
s := proxyServer(t, config.Config{ProxyAuth: true, UserHeader: "Remote-User"})
h := s.routes()
put := func(user, body string) {
@@ -451,7 +456,7 @@ func TestAppListeBrauchtDenSchlüssel(t *testing.T) {
idp := newFakeIdP(t, "honeyos")
s := newTestServer(t, idp.URL)
s.cfg.AppsKey = "geheim123"
s.auth = NewAuth(s.cfg)
s.auth = auth.New(s.cfg)
h := s.routes()
get := func(mod func(*http.Request)) int {
@@ -483,7 +488,7 @@ func TestSchlüsselDarfNichtSchreiben(t *testing.T) {
idp := newFakeIdP(t, "honeyos")
s := newTestServer(t, idp.URL)
s.cfg.AppsKey = "geheim123"
s.auth = NewAuth(s.cfg)
s.auth = auth.New(s.cfg)
h := s.routes()
req := httptest.NewRequest("PUT", "/api/apps", strings.NewReader(`[{"id":"x","url":"https://a.test"}]`))
@@ -497,7 +502,7 @@ func TestSchlüsselDarfNichtSchreiben(t *testing.T) {
// Die Oberfläche muss den Schlüssel mitbekommen, sonst fragt sie sich selbst aus.
func TestStartseiteTrägtDenSchlüssel(t *testing.T) {
s := proxyServer(t, Config{ProxyAuth: true, AppsKey: "geheim123"})
s := proxyServer(t, config.Config{ProxyAuth: true, AppsKey: "geheim123"})
rec := httptest.NewRecorder()
s.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
@@ -506,7 +511,7 @@ func TestStartseiteTrägtDenSchlüssel(t *testing.T) {
}
// Ohne Schlüssel darf auch nichts drinstehen.
s2 := proxyServer(t, Config{ProxyAuth: true})
s2 := proxyServer(t, config.Config{ProxyAuth: true})
rec = httptest.NewRecorder()
s2.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
if strings.Contains(rec.Body.String(), "honeyos-key") {
@@ -518,7 +523,7 @@ func TestStartseiteTrägtDenSchlüssel(t *testing.T) {
// jede Anfrage als angemeldet. Der Schlüssel darf dann NICHT auf diese Prüfung
// zurückfallen, sonst gibt der freigegebene Pfad die Liste an jeden heraus.
func TestSchlüsselGiltAuchHinterDemProxy(t *testing.T) {
h := proxyServer(t, Config{ProxyAuth: true, AppsKey: "geheim123"}).routes()
h := proxyServer(t, config.Config{ProxyAuth: true, AppsKey: "geheim123"}).routes()
get := func(key string) int {
req := httptest.NewRequest("GET", "/api/apps", nil)
@@ -543,11 +548,11 @@ func TestSchlüsselGiltAuchHinterDemProxy(t *testing.T) {
// Die Leiste stellt genau eine Anfrage — sie muss Liste und Aussehen bringen.
func TestAppListeBringtDasAussehenMit(t *testing.T) {
s := proxyServer(t, Config{ProxyAuth: true, AppsKey: "geheim123"})
if err := s.store.SetSettings(localSub, map[string]string{"phosphor": "green", "glow": "off"}); err != nil {
s := proxyServer(t, config.Config{ProxyAuth: true, AppsKey: "geheim123"})
if err := s.store.SetSettings(store.LocalSub, map[string]string{"phosphor": "green", "glow": "off"}); err != nil {
t.Fatal(err)
}
if err := s.store.SetApps([]App{{ID: "aether", Name: "AETHER", URL: "https://cloud.example.test"}}); err != nil {
if err := s.store.SetApps([]store.App{{ID: "aether", Name: "AETHER", URL: "https://cloud.example.test"}}); err != nil {
t.Fatal(err)
}
@@ -557,7 +562,7 @@ func TestAppListeBringtDasAussehenMit(t *testing.T) {
s.routes().ServeHTTP(rec, req)
var got struct {
Apps []App
Apps []store.App
Settings map[string]string
}
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
@@ -574,7 +579,7 @@ func TestAppListeBringtDasAussehenMit(t *testing.T) {
// Die angezeigte Version muss vom Server kommen. Vorher stand sie zusätzlich in
// os.js und blieb bei 0.2 stehen, während die Images schon 0.4 hiessen.
func TestVersionKommtVomServer(t *testing.T) {
h := proxyServer(t, Config{ProxyAuth: true}).routes()
h := proxyServer(t, config.Config{ProxyAuth: true}).routes()
for _, pfad := range []string{"/api/public", "/api/me"} {
rec := httptest.NewRecorder()
@@ -586,14 +591,24 @@ func TestVersionKommtVomServer(t *testing.T) {
}
}
// Und in der Oberfläche darf keine zweite fest eingetragen sein.
raw, err := webFS.ReadFile("web/os.js")
// Und in der Oberfläche darf keine zweite fest eingetragen sein — in keiner
// Datei, egal wie die Oberfläche später aufgeteilt wird.
err := fs.WalkDir(webFS, ".", func(pfad string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(pfad, ".js") {
return err
}
raw, err := webFS.ReadFile(pfad)
if err != nil {
return err
}
if strings.Contains(string(raw), `const VERSION = "`) {
t.Errorf("%s trägt wieder eine eigene Version — die läuft mit den Image-Tags auseinander", pfad)
}
return nil
})
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), `const VERSION = "`) {
t.Error("os.js trägt wieder eine eigene Version — die läuft mit den Image-Tags auseinander")
}
}
// Der Fall, nach dem Marc gefragt hat: Einstellungen am Rechner setzen, und die
@@ -601,7 +616,7 @@ func TestVersionKommtVomServer(t *testing.T) {
// also beim Standardnutzer — die Einstellungen können aber unter einem Namen
// aus der Proxy-Kopfzeile liegen.
func TestLeisteFindetDasAussehenAuchÜberDieSiteGrenze(t *testing.T) {
s := proxyServer(t, Config{ProxyAuth: true, UserHeader: "Remote-User", AppsKey: "geheim123"})
s := proxyServer(t, config.Config{ProxyAuth: true, UserHeader: "Remote-User", AppsKey: "geheim123"})
h := s.routes()
// Am Rechner, durch den Proxy: grün.
@@ -628,7 +643,7 @@ func TestLeisteFindetDasAussehenAuchÜberDieSiteGrenze(t *testing.T) {
// Bei mehreren Nutzern darf nicht geraten werden, wessen Farbe gilt.
func TestBeiMehrerenNutzernWirdNichtGeraten(t *testing.T) {
s := proxyServer(t, Config{ProxyAuth: true, UserHeader: "Remote-User", AppsKey: "geheim123"})
s := proxyServer(t, config.Config{ProxyAuth: true, UserHeader: "Remote-User", AppsKey: "geheim123"})
for name, farbe := range map[string]string{"marc": "green", "gast": "ice"} {
if err := s.store.SetSettings("proxy:"+name, map[string]string{"phosphor": farbe}); err != nil {
t.Fatal(err)