test: stabilize backend tests in CI

- Avoid requiring built frontend assets in cache header tests.
- Skip Testcontainers-backed store tests when Docker is unavailable.
pull/5942/head
boojack 3 weeks ago
parent b6f42cbe6b
commit 02096836c3

@ -39,6 +39,7 @@ func NewFrontendService(profile *profile.Profile, store *store.Store) *FrontendS
}
func (s *FrontendService) Serve(_ context.Context, e *echo.Echo) {
frontendFS := getFileSystem("dist")
skipper := func(c *echo.Context) bool {
requestPath := c.Request().URL.Path
if shouldSkipFrontendStatic(requestPath) {
@ -49,16 +50,35 @@ func (s *FrontendService) Serve(_ context.Context, e *echo.Echo) {
return false
}
// Route to serve the main app with HTML5 fallback for SPA behavior.
// Route to serve the frontend static assets.
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Filesystem: getFileSystem("dist"),
HTML5: true, // Enable fallback to index.html
Filesystem: frontendFS,
Skipper: skipper,
}))
e.Use(spaFallbackMiddleware(frontendFS))
s.registerRoutes(e)
}
func spaFallbackMiddleware(frontendFS fs.FS) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
err := next(c)
if err == nil {
return nil
}
requestPath := c.Request().URL.Path
if shouldSkipFrontendStatic(requestPath) || !shouldServeFrontendHTML(requestPath) || echo.StatusCode(err) != http.StatusNotFound {
return err
}
setFrontendCacheHeaders(c, requestPath)
return c.FileFS("index.html", frontendFS)
}
}
}
func shouldSkipFrontendStatic(requestPath string) bool {
if requestPath == "/robots.txt" || requestPath == "/sitemap.xml" || strings.HasSuffix(requestPath, "/rss.xml") {
return true

@ -2,10 +2,8 @@ package frontend
import (
"context"
"io/fs"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/labstack/echo/v5"
@ -16,15 +14,7 @@ import (
teststore "github.com/usememos/memos/store/test"
)
func TestFrontendService_StaticCacheHeaders(t *testing.T) {
ctx := context.Background()
testStore := teststore.NewTestingStore(ctx, t)
e := echo.New()
NewFrontendService(&profile.Profile{}, testStore).Serve(ctx, e)
hashedAssetPath := firstEmbeddedAssetPath(t, ".js")
func TestFrontendService_CacheHeaderRules(t *testing.T) {
tests := []struct {
name string
path string
@ -55,7 +45,7 @@ func TestFrontendService_StaticCacheHeaders(t *testing.T) {
},
{
name: "hashed asset is immutable",
path: hashedAssetPath,
path: "/assets/index-deadbeef.js",
cacheControl: frontendHashedAssetCacheControl,
},
{
@ -65,6 +55,59 @@ func TestFrontendService_StaticCacheHeaders(t *testing.T) {
},
}
e := echo.New()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
setFrontendCacheHeaders(c, tt.path)
require.Equal(t, tt.cacheControl, rec.Header().Get(echo.HeaderCacheControl))
require.Equal(t, tt.pragma, rec.Header().Get("Pragma"))
require.Equal(t, tt.expires, rec.Header().Get("Expires"))
})
}
}
func TestFrontendService_StaticCacheHeaders(t *testing.T) {
ctx := context.Background()
testStore := teststore.NewTestingStore(ctx, t)
e := echo.New()
NewFrontendService(&profile.Profile{}, testStore).Serve(ctx, e)
tests := []struct {
name string
path string
cacheControl string
pragma string
expires string
}{
{
name: "root html is not stored",
path: "/",
cacheControl: frontendHTMLCacheControl,
pragma: "no-cache",
expires: "0",
},
{
name: "index html is not stored",
path: "/index.html",
cacheControl: frontendHTMLCacheControl,
pragma: "no-cache",
expires: "0",
},
{
name: "spa fallback html is not stored",
path: "/memos/publicmemo",
cacheControl: frontendHTMLCacheControl,
pragma: "no-cache",
expires: "0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
@ -79,6 +122,20 @@ func TestFrontendService_StaticCacheHeaders(t *testing.T) {
}
}
func TestFrontendService_MissingAssetDoesNotFallbackToIndex(t *testing.T) {
ctx := context.Background()
testStore := teststore.NewTestingStore(ctx, t)
e := echo.New()
NewFrontendService(&profile.Profile{}, testStore).Serve(ctx, e)
req := httptest.NewRequest(http.MethodGet, "/assets/missing.js", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
require.Equal(t, http.StatusNotFound, rec.Code)
}
func TestFrontendService_SkipsDynamicRoutes(t *testing.T) {
ctx := context.Background()
testStore := teststore.NewTestingStore(ctx, t)
@ -174,18 +231,3 @@ func TestFrontendService_SitemapRoutesRequireInstanceURL(t *testing.T) {
require.Equal(t, http.StatusNotFound, rec.Code)
}
}
func firstEmbeddedAssetPath(t *testing.T, suffix string) string {
t.Helper()
var assetPath string
require.NoError(t, fs.WalkDir(getFileSystem("dist"), "assets", func(path string, d fs.DirEntry, err error) error {
require.NoError(t, err)
if assetPath == "" && !d.IsDir() && strings.HasSuffix(path, suffix) {
assetPath = "/" + path
}
return nil
}))
require.NotEmpty(t, assetPath)
return assetPath
}

@ -78,8 +78,18 @@ func requireTestNetwork(ctx context.Context) (*testcontainers.DockerNetwork, err
return nw, nil
}
func skipIfContainerProviderUnavailable(t *testing.T) {
t.Helper()
if os.Getenv("SKIP_CONTAINER_TESTS") == "1" {
t.Skip("skipping container-based test (SKIP_CONTAINER_TESTS=1)")
}
testcontainers.SkipIfProviderIsNotHealthy(t)
}
// GetMySQLDSN starts a MySQL container (if not already running) and creates a fresh database for this test.
func GetMySQLDSN(t *testing.T) string {
skipIfContainerProviderUnavailable(t)
ctx := context.Background()
mysqlOnce.Do(func() {
@ -180,6 +190,8 @@ func waitForDB(driver, dsn string, timeout time.Duration) error {
// GetPostgresDSN starts a PostgreSQL container (if not already running) and creates a fresh database for this test.
func GetPostgresDSN(t *testing.T) string {
skipIfContainerProviderUnavailable(t)
ctx := context.Background()
postgresOnce.Do(func() {

@ -3,7 +3,6 @@ package test
import (
"context"
"fmt"
"os"
"testing"
"time"
@ -128,10 +127,7 @@ func TestMigrationFromStableVersion(t *testing.T) {
t.Skip("skipping upgrade test for non-sqlite driver")
}
// Skip if explicitly disabled (e.g., in environments without Docker)
if os.Getenv("SKIP_CONTAINER_TESTS") == "1" {
t.Skip("skipping container-based test (SKIP_CONTAINER_TESTS=1)")
}
skipIfContainerProviderUnavailable(t)
ctx := context.Background()
dataDir := t.TempDir()

@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"fmt"
"os"
"strings"
"testing"
"time"
@ -20,9 +19,7 @@ func TestMigrationFromV0262PreservesLegacyData(t *testing.T) {
if testing.Short() {
t.Skip("skipping container-based upgrade test in short mode")
}
if os.Getenv("SKIP_CONTAINER_TESTS") == "1" {
t.Skip("skipping container-based test (SKIP_CONTAINER_TESTS=1)")
}
skipIfContainerProviderUnavailable(t)
ctx := context.Background()
driver := getDriverFromEnv()

Loading…
Cancel
Save