feat: enhance Makefile commands and update README for improved development workflow

refactor: separate API and SPA handling in server routes and add static file serving options

test: add tests for SPA handling and static file serving

feat: implement embedded SPA support for production and development environments
This commit is contained in:
mlogclub
2026-05-23 22:44:06 +08:00
parent c6c1c00141
commit c80bae5106
8 changed files with 466 additions and 144 deletions
+115
View File
@@ -0,0 +1,115 @@
package ginx
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
func TestDirHandlerWithSPA(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("<html>spa</html>"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "manifest"), []byte("manifest content"), 0o644); err != nil {
t.Fatal(err)
}
tests := []struct {
path string
wantStatus int
wantBody string
}{
{path: "/assets/not-exists.css", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
{path: "/images/not-exists.png", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
{path: "/fonts/not-exists", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
{path: "/manifest", wantStatus: http.StatusOK, wantBody: "manifest content"},
{path: "/dashboard", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
}
handler := DirHandler(http.Dir(root), DirOptions{
ShowList: false,
SPA: true,
IndexName: "index.html",
})
for _, tt := range tests {
rec := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(rec)
ctx.Request = httptest.NewRequest(http.MethodGet, tt.path, nil)
handler(ctx)
if rec.Code != tt.wantStatus {
t.Fatalf("%s status=%d want %d; body=%q", tt.path, rec.Code, tt.wantStatus, rec.Body.String())
}
if tt.wantBody != "" && strings.TrimSpace(rec.Body.String()) != tt.wantBody {
t.Fatalf("%s body=%q want %q", tt.path, strings.TrimSpace(rec.Body.String()), tt.wantBody)
}
}
}
func TestStaticFilesDoesNotOpenDirectories(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "file.txt"), []byte("content"), 0o644); err != nil {
t.Fatal(err)
}
fileSystem := StaticFiles(root)
if file, err := fileSystem.Open("/file.txt"); err != nil {
t.Fatalf("Open(file.txt) err=%v", err)
} else {
_ = file.Close()
}
if file, err := fileSystem.Open("/"); err == nil {
_ = file.Close()
t.Fatal("Open(/) succeeded, want error")
} else if !os.IsNotExist(err) {
t.Fatalf("Open(/) err=%v, want not exist", err)
}
}
func TestHandleSPAKeepsNotFoundPrefixes(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "index.html"), []byte("<html>spa</html>"), 0o644); err != nil {
t.Fatal(err)
}
engine := gin.New()
HandleSPA(engine, SPAOptions{
Root: root,
DirOptions: DirOptions{
ShowList: false,
SPA: true,
IndexName: "index.html",
},
NotFoundPrefixes: []string{"/api/"},
})
tests := []struct {
path string
wantStatus int
wantBody string
}{
{path: "/", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
{path: "/dashboard", wantStatus: http.StatusOK, wantBody: "<html>spa</html>"},
{path: "/api/not-exists", wantStatus: http.StatusNotFound},
}
for _, tt := range tests {
rec := httptest.NewRecorder()
engine.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, tt.path, nil))
if rec.Code != tt.wantStatus {
t.Fatalf("%s status=%d want %d", tt.path, rec.Code, tt.wantStatus)
}
if tt.wantBody != "" && strings.TrimSpace(rec.Body.String()) != tt.wantBody {
t.Fatalf("%s body=%q want %q", tt.path, strings.TrimSpace(rec.Body.String()), tt.wantBody)
}
}
}