diff --git a/cmd/dapp-fm-app/main.go b/cmd/dapp-fm-app/main.go index 097b1e4..e0af33a 100644 --- a/cmd/dapp-fm-app/main.go +++ b/cmd/dapp-fm-app/main.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "fmt" "io/fs" + "mime" "net/http" "strconv" "strings" @@ -57,16 +58,34 @@ func (s *MediaStore) Clear() { s.media = make(map[string]*MediaItem) } +func init() { + mime.AddExtensionType(".wasm", "application/wasm") + mime.AddExtensionType(".js", "application/javascript") + mime.AddExtensionType(".css", "text/css") + mime.AddExtensionType(".html", "text/html; charset=utf-8") +} + // AssetHandler serves both static assets and decrypted media type AssetHandler struct { - assets fs.FS + assets fs.FS + fileServer http.Handler +} + +// NewAssetHandler creates a new AssetHandler +func NewAssetHandler(assets fs.FS) *AssetHandler { + sub, err := fs.Sub(assets, "frontend") + if err != nil { + // Fallback to assets if sub fails (e.g. test mock) + sub = assets + } + return &AssetHandler{ + assets: assets, + fileServer: http.FileServer(http.FS(sub)), + } } func (h *AssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { path := r.URL.Path - if path == "/" { - path = "/index.html" - } path = strings.TrimPrefix(path, "/") // Check if this is a media request @@ -112,25 +131,12 @@ func (h *AssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Serve static assets - data, err := fs.ReadFile(h.assets, "frontend/"+path) - if err != nil { + if h.fileServer == nil { + // Fallback if not initialized via NewAssetHandler (should not happen in prod) http.NotFound(w, r) return } - - // Set content type - switch { - case strings.HasSuffix(path, ".html"): - w.Header().Set("Content-Type", "text/html; charset=utf-8") - case strings.HasSuffix(path, ".js"): - w.Header().Set("Content-Type", "application/javascript") - case strings.HasSuffix(path, ".css"): - w.Header().Set("Content-Type", "text/css") - case strings.HasSuffix(path, ".wasm"): - w.Header().Set("Content-Type", "application/wasm") - } - - w.Write(data) + h.fileServer.ServeHTTP(w, r) } // App wraps player functionality @@ -307,7 +313,7 @@ func main() { MinWidth: 800, MinHeight: 600, AssetServer: &assetserver.Options{ - Handler: &AssetHandler{assets: frontendAssets}, + Handler: NewAssetHandler(frontendAssets), }, BackgroundColour: &options.RGBA{R: 18, G: 18, B: 18, A: 1}, OnStartup: app.Startup, diff --git a/cmd/dapp-fm-app/main_test.go b/cmd/dapp-fm-app/main_test.go new file mode 100644 index 0000000..d26a392 --- /dev/null +++ b/cmd/dapp-fm-app/main_test.go @@ -0,0 +1,125 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "testing/fstest" +) + +func TestAssetHandler_ServeHTTP_Static(t *testing.T) { + mockFS := fstest.MapFS{ + "frontend/index.html": {Data: []byte("Hello")}, + "frontend/style.css": {Data: []byte("body { color: red; }")}, + "frontend/app.js": {Data: []byte("console.log('hi')")}, + "frontend/test.wasm": {Data: []byte{0x00, 0x61, 0x73, 0x6d}}, + } + + handler := NewAssetHandler(mockFS) + + tests := []struct { + name string + path string + wantCode int + wantType string + wantContent string + }{ + { + name: "Root", + path: "/", + wantCode: http.StatusOK, + wantType: "text/html; charset=utf-8", + wantContent: "Hello", + }, + { + name: "Index", + path: "/index.html", + wantCode: http.StatusMovedPermanently, // http.FileServer redirects index.html to / + }, + { + name: "CSS", + path: "/style.css", + wantCode: http.StatusOK, + wantType: "text/css", + wantContent: "body { color: red; }", + }, + { + name: "JS", + path: "/app.js", + wantCode: http.StatusOK, + wantType: "application/javascript", + wantContent: "console.log('hi')", + }, + { + name: "WASM", + path: "/test.wasm", + wantCode: http.StatusOK, + wantType: "application/wasm", + wantContent: "\x00\x61\x73\x6d", + }, + { + name: "NotFound", + path: "/missing.html", + wantCode: http.StatusNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("GET", tt.path, nil) + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != tt.wantCode { + t.Errorf("path %s: status code = %d, want %d", tt.path, resp.StatusCode, tt.wantCode) + } + + if tt.wantCode == http.StatusOK { + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, tt.wantType) { + t.Errorf("path %s: content type = %q, want %q", tt.path, ct, tt.wantType) + } + + // Read body + if tt.wantContent != "" { + body := w.Body.String() + if body != tt.wantContent { + t.Errorf("path %s: body = %q, want %q", tt.path, body, tt.wantContent) + } + } + } + }) + } +} + +func TestAssetHandler_ServeHTTP_Media(t *testing.T) { + // Setup test data + globalStore.Set("123", &MediaItem{ + Data: []byte("mediadata"), + MimeType: "audio/mp3", + Name: "song.mp3", + }) + defer globalStore.Clear() + + mockFS := fstest.MapFS{} + handler := NewAssetHandler(mockFS) + + req := httptest.NewRequest("GET", "/media/123", nil) + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + + resp := w.Result() + if resp.StatusCode != http.StatusOK { + t.Errorf("status code = %d, want %d", resp.StatusCode, http.StatusOK) + } + if ct := resp.Header.Get("Content-Type"); ct != "audio/mp3" { + t.Errorf("content type = %s, want audio/mp3", ct) + } + if body := w.Body.String(); body != "mediadata" { + t.Errorf("body = %s, want mediadata", body) + } +} diff --git a/dapp-fm-app b/dapp-fm-app new file mode 100755 index 0000000..6840292 Binary files /dev/null and b/dapp-fm-app differ diff --git a/pkg/player/assets.go b/pkg/player/assets.go index 9196f35..5ad364c 100644 --- a/pkg/player/assets.go +++ b/pkg/player/assets.go @@ -11,7 +11,6 @@ import ( //go:embed frontend/index.html //go:embed frontend/wasm_exec.js //go:embed frontend/stmf.wasm -//go:embed frontend/demo-track.smsg var assets embed.FS // Assets returns the embedded filesystem with frontend/ prefix stripped