mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
[优化] go 引用调整
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rain-kl/openflare/openflare-server/model"
|
||||
)
|
||||
|
||||
func TestPagesUploadActivateAndPublishStaticRoute(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
project, err := CreatePagesProject(PagesProjectInput{
|
||||
Name: "Marketing Site",
|
||||
Slug: "marketing-site",
|
||||
Enabled: true,
|
||||
SPAFallbackEnabled: true,
|
||||
SPAFallbackPath: "/app.html",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePagesProject failed: %v", err)
|
||||
}
|
||||
uploadHeader := multipartFileHeader(t, "site.zip", testPagesZip(t, map[string]string{
|
||||
"index.html": "<h1>Hello Pages</h1>",
|
||||
"assets/app.js": "console.log('pages')",
|
||||
"assets/style.css": "body{color:#111}",
|
||||
}))
|
||||
deployment, err := UploadPagesDeployment(project.ID, uploadHeader, "", "index.html", "root")
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPagesDeployment failed: %v", err)
|
||||
}
|
||||
if deployment.FileCount != 3 || deployment.TotalSize == 0 {
|
||||
t.Fatalf("unexpected deployment manifest: %+v", deployment)
|
||||
}
|
||||
project, err = ActivatePagesDeployment(project.ID, deployment.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ActivatePagesDeployment failed: %v", err)
|
||||
}
|
||||
if project.ActiveDeploymentID == nil || *project.ActiveDeploymentID != deployment.ID {
|
||||
t.Fatalf("expected active deployment %d, got %+v", deployment.ID, project.ActiveDeploymentID)
|
||||
}
|
||||
|
||||
route, err := CreateProxyRoute(ProxyRouteInput{
|
||||
Domain: "pages.example.com",
|
||||
Enabled: true,
|
||||
UpstreamType: "pages",
|
||||
PagesProjectID: &project.ID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProxyRoute failed: %v", err)
|
||||
}
|
||||
if route.UpstreamType != "pages" || route.PagesProjectID == nil || *route.PagesProjectID != project.ID {
|
||||
t.Fatalf("expected route to bind Pages project, got %+v", route)
|
||||
}
|
||||
|
||||
result, err := PublishConfigVersion("root", false)
|
||||
if err != nil {
|
||||
t.Fatalf("PublishConfigVersion failed: %v", err)
|
||||
}
|
||||
if !strings.Contains(result.Version.SnapshotJSON, `"upstream_type":"pages"`) {
|
||||
t.Fatalf("expected snapshot to include pages route, got %s", result.Version.SnapshotJSON)
|
||||
}
|
||||
if !strings.Contains(result.Version.SnapshotJSON, `"deployment_id":`) {
|
||||
t.Fatalf("expected snapshot to include pages deployment, got %s", result.Version.SnapshotJSON)
|
||||
}
|
||||
if !strings.Contains(result.Version.RenderedConfig, "root \"__OPENFLARE_PAGES_DIR__/deployments/") {
|
||||
t.Fatalf("expected rendered config to use pages dir placeholder, got:\n%s", result.Version.RenderedConfig)
|
||||
}
|
||||
if !strings.Contains(result.Version.SnapshotJSON, `"spa_fallback_path":"/app.html"`) {
|
||||
t.Fatalf("expected snapshot to include custom SPA fallback path, got %s", result.Version.SnapshotJSON)
|
||||
}
|
||||
if !strings.Contains(result.Version.RenderedConfig, "try_files $uri $uri/ /app.html;") {
|
||||
t.Fatalf("expected SPA fallback try_files, got:\n%s", result.Version.RenderedConfig)
|
||||
}
|
||||
if strings.Contains(result.Version.RenderedConfig, "proxy_pass") {
|
||||
t.Fatalf("Pages route must not render proxy_pass, got:\n%s", result.Version.RenderedConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPagesProjectRejectsUnsafeFallbackPath(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
_, err := CreatePagesProject(PagesProjectInput{
|
||||
Name: "Unsafe Fallback",
|
||||
Slug: "unsafe-fallback",
|
||||
Enabled: true,
|
||||
SPAFallbackEnabled: true,
|
||||
SPAFallbackPath: "/index.html; proxy_pass http://evil",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "回退路径") {
|
||||
t.Fatalf("expected unsafe SPA fallback path rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadPagesDeploymentRejectsZipSlip(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
project, err := CreatePagesProject(PagesProjectInput{
|
||||
Name: "Unsafe Site",
|
||||
Slug: "unsafe-site",
|
||||
Enabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePagesProject failed: %v", err)
|
||||
}
|
||||
_, err = UploadPagesDeployment(project.ID, multipartFileHeader(t, "bad.zip", testPagesZip(t, map[string]string{
|
||||
"../escape.html": "bad",
|
||||
"index.html": "ok",
|
||||
})), "", "index.html", "root")
|
||||
if err == nil || !strings.Contains(err.Error(), "逃逸目录") {
|
||||
t.Fatalf("expected zip-slip rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPagesRouteRequiresActiveDeployment(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
project, err := CreatePagesProject(PagesProjectInput{
|
||||
Name: "Draft Site",
|
||||
Slug: "draft-site",
|
||||
Enabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePagesProject failed: %v", err)
|
||||
}
|
||||
if _, err = CreateProxyRoute(ProxyRouteInput{
|
||||
Domain: "draft.example.com",
|
||||
Enabled: true,
|
||||
UpstreamType: "pages",
|
||||
PagesProjectID: &project.ID,
|
||||
}); err == nil || !strings.Contains(err.Error(), "没有激活部署") {
|
||||
t.Fatalf("expected active deployment validation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPagesDeploymentPackageRequiresActiveConfigSnapshot(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
project, err := CreatePagesProject(PagesProjectInput{Name: "Published Site", Slug: "published-site", Enabled: true})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePagesProject failed: %v", err)
|
||||
}
|
||||
deployment, err := UploadPagesDeployment(project.ID, multipartFileHeader(t, "site.zip", testPagesZip(t, map[string]string{
|
||||
"index.html": "ok",
|
||||
})), "", "index.html", "root")
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPagesDeployment failed: %v", err)
|
||||
}
|
||||
if _, err = ActivatePagesDeployment(project.ID, deployment.ID); err != nil {
|
||||
t.Fatalf("ActivatePagesDeployment failed: %v", err)
|
||||
}
|
||||
if _, _, err = GetPagesDeploymentPackagePath(deployment.ID); err == nil || !strings.Contains(err.Error(), "激活配置") {
|
||||
t.Fatalf("expected package download to require active config, got %v", err)
|
||||
}
|
||||
if _, err = CreateProxyRoute(ProxyRouteInput{
|
||||
Domain: "published.example.com",
|
||||
Enabled: true,
|
||||
UpstreamType: "pages",
|
||||
PagesProjectID: &project.ID,
|
||||
}); err != nil {
|
||||
t.Fatalf("CreateProxyRoute failed: %v", err)
|
||||
}
|
||||
if _, err = PublishConfigVersion("root", false); err != nil {
|
||||
t.Fatalf("PublishConfigVersion failed: %v", err)
|
||||
}
|
||||
filePath, fileName, err := GetPagesDeploymentPackagePath(deployment.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPagesDeploymentPackagePath failed after publish: %v", err)
|
||||
}
|
||||
if filePath == "" || fileName == "" {
|
||||
t.Fatalf("expected package path and file name, got path=%q name=%q", filePath, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
func testPagesZip(t *testing.T, files map[string]string) []byte {
|
||||
t.Helper()
|
||||
var buffer bytes.Buffer
|
||||
writer := zip.NewWriter(&buffer)
|
||||
for name, content := range files {
|
||||
file, err := writer.Create(name)
|
||||
if err != nil {
|
||||
t.Fatalf("create zip entry failed: %v", err)
|
||||
}
|
||||
if _, err := file.Write([]byte(content)); err != nil {
|
||||
t.Fatalf("write zip entry failed: %v", err)
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatalf("close zip failed: %v", err)
|
||||
}
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func multipartFileHeader(t *testing.T, fileName string, content []byte) *multipart.FileHeader {
|
||||
t.Helper()
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
part, err := writer.CreateFormFile("package", fileName)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateFormFile failed: %v", err)
|
||||
}
|
||||
if _, err = part.Write(content); err != nil {
|
||||
t.Fatalf("write multipart file failed: %v", err)
|
||||
}
|
||||
if err = writer.Close(); err != nil {
|
||||
t.Fatalf("close multipart writer failed: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest("POST", "/", &body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
if err = req.ParseMultipartForm(int64(len(content)) + 1024); err != nil {
|
||||
t.Fatalf("ParseMultipartForm failed: %v", err)
|
||||
}
|
||||
file, header, err := req.FormFile("package")
|
||||
if err != nil {
|
||||
t.Fatalf("FormFile failed: %v", err)
|
||||
}
|
||||
file.Close()
|
||||
return header
|
||||
}
|
||||
|
||||
func TestDeletePagesDeploymentRejectsActiveDeployment(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
project, err := CreatePagesProject(PagesProjectInput{Name: "Active", Slug: "active", Enabled: true})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePagesProject failed: %v", err)
|
||||
}
|
||||
deployment, err := UploadPagesDeployment(project.ID, multipartFileHeader(t, "site.zip", testPagesZip(t, map[string]string{"index.html": "ok"})), "", "index.html", "root")
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPagesDeployment failed: %v", err)
|
||||
}
|
||||
if _, err = ActivatePagesDeployment(project.ID, deployment.ID); err != nil {
|
||||
t.Fatalf("ActivatePagesDeployment failed: %v", err)
|
||||
}
|
||||
if err = DeletePagesDeployment(project.ID, deployment.ID); err == nil {
|
||||
t.Fatal("expected active deployment deletion to fail")
|
||||
}
|
||||
var stored model.PagesDeployment
|
||||
if err = model.DB.First(&stored, deployment.ID).Error; err != nil {
|
||||
t.Fatalf("expected active deployment to remain: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadPagesDeploymentWithTopLevelFolder(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
project, err := CreatePagesProject(PagesProjectInput{
|
||||
Name: "Folder Site",
|
||||
Slug: "folder-site",
|
||||
Enabled: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePagesProject failed: %v", err)
|
||||
}
|
||||
// Upload a zip with all files inside a top-level directory "Speed-Test-source/"
|
||||
uploadHeader := multipartFileHeader(t, "site.zip", testPagesZip(t, map[string]string{
|
||||
"Speed-Test-source/index.html": "<h1>Hello Pages</h1>",
|
||||
"Speed-Test-source/assets/app.js": "console.log('pages')",
|
||||
}))
|
||||
deployment, err := UploadPagesDeployment(project.ID, uploadHeader, "", "index.html", "root")
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPagesDeployment with folder failed: %v", err)
|
||||
}
|
||||
if deployment.FileCount != 2 {
|
||||
t.Fatalf("expected 2 files, got %d", deployment.FileCount)
|
||||
}
|
||||
if project.EntryFile != "index.html" {
|
||||
t.Fatalf("expected EntryFile to be index.html, got %q", project.EntryFile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPagesProjectAPIProxyValidation(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
// 1. Invalid configuration: enabled but empty fields
|
||||
_, err := CreatePagesProject(PagesProjectInput{
|
||||
Name: "API Proxy 1",
|
||||
Enabled: true,
|
||||
APIProxyEnabled: true,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "匹配路径不能为空") {
|
||||
t.Fatalf("expected error for empty match path, got: %v", err)
|
||||
}
|
||||
|
||||
// 2. Invalid path: must start with '/'
|
||||
_, err = CreatePagesProject(PagesProjectInput{
|
||||
Name: "API Proxy 2",
|
||||
Enabled: true,
|
||||
APIProxyEnabled: true,
|
||||
APIProxyPath: "api",
|
||||
APIProxyPass: "http://127.0.0.1:8080",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "必须以 '/' 开头") {
|
||||
t.Fatalf("expected error for path not starting with /, got: %v", err)
|
||||
}
|
||||
|
||||
// 3. Invalid target URL
|
||||
_, err = CreatePagesProject(PagesProjectInput{
|
||||
Name: "API Proxy 3",
|
||||
Enabled: true,
|
||||
APIProxyEnabled: true,
|
||||
APIProxyPath: "/api",
|
||||
APIProxyPass: "127.0.0.1:8080",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "有效的 HTTP/HTTPS URL") {
|
||||
t.Fatalf("expected error for invalid pass URL, got: %v", err)
|
||||
}
|
||||
|
||||
// 4. Valid configuration
|
||||
project, err := CreatePagesProject(PagesProjectInput{
|
||||
Name: "API Proxy Valid",
|
||||
Enabled: true,
|
||||
APIProxyEnabled: true,
|
||||
APIProxyPath: "/api",
|
||||
APIProxyPass: "http://127.0.0.1:8080",
|
||||
APIProxyRewrite: "/",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating valid project: %v", err)
|
||||
}
|
||||
if !project.APIProxyEnabled || project.APIProxyPath != "/api" || project.APIProxyPass != "http://127.0.0.1:8080" || project.APIProxyRewrite != "/" {
|
||||
t.Fatalf("unexpected project state: %+v", project)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadPagesDeploymentWithRootDir(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
project, err := CreatePagesProject(PagesProjectInput{
|
||||
Name: "App Site",
|
||||
Slug: "app-site",
|
||||
Enabled: true,
|
||||
RootDir: "build",
|
||||
EntryFile: "index.html",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePagesProject failed: %v", err)
|
||||
}
|
||||
|
||||
// 1. Upload a zip with files inside a subfolder.
|
||||
uploadHeader := multipartFileHeader(t, "site.zip", testPagesZip(t, map[string]string{
|
||||
"build/index.html": "<h1>App Root</h1>",
|
||||
"build/static/bundle.js": "console.log('app')",
|
||||
"README.md": "README info",
|
||||
}))
|
||||
deployment, err := UploadPagesDeployment(project.ID, uploadHeader, "build", "index.html", "root")
|
||||
if err != nil {
|
||||
t.Fatalf("UploadPagesDeployment with rootDir failed: %v", err)
|
||||
}
|
||||
if deployment.FileCount != 3 {
|
||||
t.Fatalf("expected 3 files, got %d", deployment.FileCount)
|
||||
}
|
||||
if project.RootDir != "build" {
|
||||
t.Fatalf("expected RootDir to be 'build', got %q", project.RootDir)
|
||||
}
|
||||
if project.EntryFile != "index.html" {
|
||||
t.Fatalf("expected EntryFile to be 'index.html', got %q", project.EntryFile)
|
||||
}
|
||||
|
||||
// 2. Update project configuration to a wrong entry file relative to root directory, upload should fail
|
||||
project, err = UpdatePagesProject(project.ID, PagesProjectInput{
|
||||
Name: "App Site",
|
||||
Slug: "app-site",
|
||||
Enabled: true,
|
||||
RootDir: "build",
|
||||
EntryFile: "missing.html",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePagesProject failed: %v", err)
|
||||
}
|
||||
_, err = UploadPagesDeployment(project.ID, uploadHeader, "build", "missing.html", "root")
|
||||
if err == nil || !strings.Contains(err.Error(), "缺少入口文件") {
|
||||
t.Fatalf("expected failure for missing entry file, got %v", err)
|
||||
}
|
||||
|
||||
// Revert to correct config for snapshot check
|
||||
project, err = UpdatePagesProject(project.ID, PagesProjectInput{
|
||||
Name: "App Site",
|
||||
Slug: "app-site",
|
||||
Enabled: true,
|
||||
RootDir: "build",
|
||||
EntryFile: "index.html",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePagesProject failed: %v", err)
|
||||
}
|
||||
|
||||
// 3. Test config snapshot LocalRoot path rendering
|
||||
project, err = ActivatePagesDeployment(project.ID, deployment.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ActivatePagesDeployment failed: %v", err)
|
||||
}
|
||||
_, err = CreateProxyRoute(ProxyRouteInput{
|
||||
Domain: "app.example.com",
|
||||
Enabled: true,
|
||||
UpstreamType: "pages",
|
||||
PagesProjectID: &project.ID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateProxyRoute failed: %v", err)
|
||||
}
|
||||
result, err := PublishConfigVersion("root", false)
|
||||
if err != nil {
|
||||
t.Fatalf("PublishConfigVersion failed: %v", err)
|
||||
}
|
||||
// Verify LocalRoot contains the rootDir
|
||||
expectedLocalRoot := fmt.Sprintf("deployments/%d/current/build", deployment.ID)
|
||||
if !strings.Contains(result.Version.SnapshotJSON, expectedLocalRoot) {
|
||||
t.Fatalf("expected snapshot JSON to include %q, got %s", expectedLocalRoot, result.Version.SnapshotJSON)
|
||||
}
|
||||
|
||||
if !strings.Contains(result.Version.RenderedConfig, "current/build") {
|
||||
t.Fatalf("expected rendered config to point to current/build, got:\n%s", result.Version.RenderedConfig)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user