test: stabilize backend tests in CI
- Avoid requiring built frontend assets in cache header tests. - Skip Testcontainers-backed store tests when Docker is unavailable.
This commit is contained in:
parent
b6f42cbe6b
commit
02096836c3
5 changed files with 106 additions and 39 deletions
|
|
@ -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…
Reference in a new issue