Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 27 additions & 21 deletions cmd/dapp-fm-app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/base64"
"fmt"
"io/fs"
"mime"
"net/http"
"strconv"
"strings"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
125 changes: 125 additions & 0 deletions cmd/dapp-fm-app/main_test.go
Original file line number Diff line number Diff line change
@@ -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("<html><body>Hello</body></html>")},
"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: "<html><body>Hello</body></html>",
},
{
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)
}
}
Binary file added dapp-fm-app
Binary file not shown.
1 change: 0 additions & 1 deletion pkg/player/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down