refactor(backend): rename OpenFlare directory to lowercase openflare

This commit is contained in:
ryan
2026-08-30 17:43:23 +08:00
parent 06d5fedbfc
commit c93ff6674f
543 changed files with 819 additions and 819 deletions
@@ -0,0 +1,794 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package pages
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"mime/multipart"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"Wavelet/openflare/plugins/server/kernel/model"
"Wavelet/openflare/plugins/server/kernel/ofupload"
"Wavelet/openflare/plugins/server/kernel/repository"
oftask "Wavelet/openflare/plugins/server/kernel/task"
"Wavelet/openflare/plugins/server/kernel/testhelper"
"Wavelet/pkg/idgen"
uploadshared "Wavelet/plugins/domain/upload/shared"
db "Wavelet/plugins/infra/database"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func setupPagesTestDB(t *testing.T) func() {
t.Helper()
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
require.NoError(t, err)
require.NoError(t, sqliteDB.AutoMigrate(
&model.User{},
&model.Upload{},
&model.UploadStat{},
&model.TaskExecution{},
&model.PagesProject{},
&model.PagesDeployment{},
&model.PagesDeploymentFile{},
&model.PagesProjectSource{},
&model.PagesProjectSourceRuntime{},
&model.ConfigVersion{},
&model.SystemConfig{},
))
require.NoError(t, sqliteDB.Create(&model.User{
ID: 999,
Username: "system",
Password: "*",
Nickname: "系统",
IsActive: true,
}).Error)
require.NoError(t, sqliteDB.Create([]model.SystemConfig{
{
Key: model.ConfigKeyPagesMaxPackageSizeMB,
Value: "100",
Type: "business",
Description: "Pages 部署包上传大小上限(MiB)",
},
{
Key: model.ConfigKeyPagesMaxHistoryCount,
Value: "0", // unlimited for existing tests
Type: "business",
Description: "Pages 每个项目最大历史部署保留数(0 表示不限制)",
},
}).Error)
db.SetDB(sqliteDB)
require.NoError(t, idgen.Init(1))
oftask.SetService(&testhelper.NoopTaskService{})
mockStorage := uploadshared.NewMockStorageService()
uploadshared.SetDBService(db.NewService(sqliteDB))
uploadshared.SetStorageService(mockStorage)
ofupload.SetStorage(mockStorage)
_ = repository.InvalidateSystemConfigCache(context.Background(), model.ConfigKeyPagesMaxPackageSizeMB)
_ = repository.InvalidateSystemConfigCache(context.Background(), model.ConfigKeyPagesMaxHistoryCount)
return func() {
ofupload.SetStorage(nil)
uploadshared.ResetServices()
db.SetDB(nil)
}
}
func setupPagesStorageMock(t *testing.T) (restore func(), disable func()) {
t.Helper()
mock := uploadshared.NewMockStorageService()
uploadshared.SetStorageService(mock)
ofupload.SetStorage(mock)
restore = func() {
ofupload.SetStorage(nil)
uploadshared.ResetServices()
}
disable = restore
return restore, disable
}
func TestCreateProject(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
ctx := context.Background()
project, err := CreateProject(ctx, Input{
Name: "Marketing Site",
Slug: "marketing-site",
Description: "public site",
Enabled: true,
SPAFallbackEnabled: true,
SPAFallbackPath: "/index.html",
EntryFile: "index.html",
})
require.NoError(t, err)
assert.NotZero(t, project.ID)
assert.Equal(t, "Marketing Site", project.Name)
assert.Equal(t, "marketing-site", project.Slug)
assert.Equal(t, "public site", project.Description)
assert.True(t, project.Enabled)
assert.True(t, project.SPAFallbackEnabled)
assert.Equal(t, "/index.html", project.SPAFallbackPath)
assert.Equal(t, "index.html", project.EntryFile)
assert.Equal(t, int64(0), project.DeploymentCount)
_, err = CreateProject(ctx, Input{
Name: "Duplicate Slug",
Slug: "marketing-site",
})
require.Error(t, err)
assert.Equal(t, errPagesSlugExists, err.Error())
}
func TestCreateProjectRejectsUnsafeFallbackPath(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
ctx := context.Background()
_, err := CreateProject(ctx, Input{
Name: "Unsafe Fallback",
Slug: "unsafe-fallback",
Enabled: true,
SPAFallbackEnabled: true,
SPAFallbackPath: "/index.html; proxy_pass http://evil",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "回退路径")
}
func TestCreateProjectRejectsUnsafeContentPaths(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
ctx := context.Background()
rootDirs := []string{"/public", "public/../dist", "C:/public", `public\\dist`, "./public", "public\x00dist"}
for index, rootDir := range rootDirs {
_, err := CreateProject(ctx, Input{
Name: fmt.Sprintf("Unsafe Root %d", index),
Slug: fmt.Sprintf("unsafe-root-%d", index),
RootDir: rootDir,
EntryFile: "index.html",
})
require.Error(t, err, rootDir)
}
entryFiles := []string{"/index.html", "../index.html", "C:/index.html", `public\\index.html`, "./index.html", "index.html;bad"}
for index, entryFile := range entryFiles {
_, err := CreateProject(ctx, Input{
Name: fmt.Sprintf("Unsafe Entry %d", index),
Slug: fmt.Sprintf("unsafe-entry-%d", index),
EntryFile: entryFile,
})
require.Error(t, err, entryFile)
}
}
func TestUpdateProjectValidatesActiveDeploymentEntry(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
project, err := CreateProject(ctx, Input{
Name: "Content Root",
Slug: "content-root",
Enabled: true,
EntryFile: "index.html",
})
require.NoError(t, err)
deployment, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "site.zip", testPagesZip(t, map[string]string{
"index.html": "root",
"dist/index.html": "dist",
})), "user:1")
require.NoError(t, err)
staleCandidate, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "stale.zip", testPagesZip(t, map[string]string{
"index.html": "stale",
})), "user:1")
require.NoError(t, err)
_, err = ActivateDeployment(ctx, project.ID, deployment.ID)
require.NoError(t, err)
updated, err := UpdateProject(ctx, project.ID, Input{
Name: project.Name,
Slug: project.Slug,
Enabled: true,
RootDir: "dist",
EntryFile: "index.html",
})
require.NoError(t, err)
assert.Equal(t, "dist", updated.RootDir)
_, err = ActivateDeployment(ctx, project.ID, staleCandidate.ID)
require.Error(t, err)
assert.Contains(t, err.Error(), errPagesEntryFileMissing)
_, err = UpdateProject(ctx, project.ID, Input{
Name: project.Name,
Slug: project.Slug,
Enabled: true,
RootDir: "missing",
EntryFile: "index.html",
})
require.Error(t, err)
assert.Contains(t, err.Error(), errPagesEntryFileMissing)
stored, err := repository.GetPagesProjectByID(ctx, project.ID)
require.NoError(t, err)
assert.Equal(t, "dist", stored.RootDir)
assert.Equal(t, "index.html", stored.EntryFile)
}
func TestUploadDeploymentAcceptsZeroByteFiles(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
project, err := CreateProject(ctx, Input{
Name: "Zero Byte Site",
Slug: "zero-byte-site",
Enabled: true,
})
require.NoError(t, err)
deployment, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "site.zip", testPagesZip(t, map[string]string{
"index.html": "ok",
".gitkeep": "",
})), "root")
require.NoError(t, err)
assert.Equal(t, 2, deployment.FileCount)
}
func TestUploadDeploymentStoresPackageInUploadFramework(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
project, err := CreateProject(ctx, Input{
Name: "Upload Framework Site",
Slug: "upload-framework-site",
Enabled: true,
})
require.NoError(t, err)
deployment, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "site.zip", testPagesZip(t, map[string]string{
"index.html": "ok",
})), "root")
require.NoError(t, err)
assert.NotZero(t, deployment.UploadID)
storedDeployment, err := repository.GetPagesDeploymentByID(ctx, deployment.ID)
require.NoError(t, err)
assert.NotZero(t, storedDeployment.UploadID)
assert.Empty(t, storedDeployment.ArtifactPath)
var uploadCount int64
require.NoError(t, db.DB(ctx).Model(&model.Upload{}).Count(&uploadCount).Error)
assert.Equal(t, int64(1), uploadCount)
var uploadRecord model.Upload
require.NoError(t, db.DB(ctx).First(&uploadRecord, storedDeployment.UploadID).Error)
assert.Equal(t, ofupload.ReservedPagesDeploymentType, uploadRecord.Type)
assert.Equal(t, pagesIngestMarkerV2, uploadRecord.Metadata.Extra[pagesIngestMarkerKey])
assert.Equal(t, fmt.Sprint(project.ID), uploadRecord.Metadata.Extra[pagesProjectIDMetadataKey])
assert.NotContains(t, uploadRecord.Metadata.Extra, "project_slug")
assert.NotContains(t, uploadRecord.Metadata.Extra, "format")
}
func TestOpenDeploymentPackageHydratesLegacyArtifactPath(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
project, err := CreateProject(ctx, Input{
Name: "Legacy Site",
Slug: "openspeedtest",
Enabled: true,
})
require.NoError(t, err)
artifactDir := filepath.Join(t.TempDir(), "pages", "artifacts", project.Slug)
require.NoError(t, os.MkdirAll(artifactDir, 0o755))
artifactPath := filepath.Join(artifactDir, "legacy-checksum.zip")
require.NoError(t, os.WriteFile(artifactPath, testPagesZip(t, map[string]string{"index.html": "legacy"}), 0o644))
deployment := &model.PagesDeployment{
ProjectID: project.ID,
DeploymentNumber: 1,
Checksum: "legacy-checksum",
Status: model.PagesDeploymentStatusUploaded,
ArtifactPath: artifactPath,
FileCount: 1,
TotalSize: 10,
CreatedBy: "test",
}
require.NoError(t, db.DB(ctx).Create(deployment).Error)
require.NoError(t, db.DB(ctx).Create(&model.PagesDeploymentFile{
DeploymentID: deployment.ID,
Path: "index.html",
Size: 6,
Checksum: "legacy-checksum",
}).Error)
_, err = ActivateDeployment(ctx, project.ID, deployment.ID)
require.NoError(t, err)
require.NoError(t, db.DB(ctx).Create(&model.ConfigVersion{
Version: "v2026-legacy",
SnapshotJSON: fmt.Sprintf(`{"routes":[{"upstream_type":"pages","pages_deployment":{"deployment_id":%d}}]}`, deployment.ID),
MainConfig: "",
RenderedConfig: "",
SupportFilesJSON: "[]",
Checksum: "legacy-config-checksum",
IsActive: true,
CreatedBy: "test",
}).Error)
packageObj, err := OpenDeploymentPackage(ctx, deployment.ID)
require.NoError(t, err)
defer packageObj.Body.Close()
assert.Equal(t, fmt.Sprintf("pages-deployment-%d.zip", deployment.ID), packageObj.FileName)
body, err := io.ReadAll(packageObj.Body)
require.NoError(t, err)
reader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
require.NoError(t, err)
require.Len(t, reader.File, 1)
assert.Equal(t, "index.html", reader.File[0].Name)
storedDeployment, err := repository.GetPagesDeploymentByID(ctx, deployment.ID)
require.NoError(t, err)
assert.NotZero(t, storedDeployment.UploadID)
assert.Empty(t, storedDeployment.ArtifactPath)
var uploadCount int64
require.NoError(t, db.DB(ctx).Model(&model.Upload{}).Count(&uploadCount).Error)
assert.Equal(t, int64(1), uploadCount)
packageObj2, err := OpenDeploymentPackage(ctx, deployment.ID)
require.NoError(t, err)
defer packageObj2.Body.Close()
body2, err := io.ReadAll(packageObj2.Body)
require.NoError(t, err)
assert.Equal(t, body, body2)
}
func TestOpenDeploymentPackageRequiresActiveConfigSnapshot(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
project, err := CreateProject(ctx, Input{
Name: "Published Site",
Slug: "published-site",
Enabled: true,
})
require.NoError(t, err)
deployment, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "site.zip", testPagesZip(t, map[string]string{
"index.html": "ok",
})), "root")
require.NoError(t, err)
_, err = ActivateDeployment(ctx, project.ID, deployment.ID)
require.NoError(t, err)
_, err = OpenDeploymentPackage(ctx, deployment.ID)
require.Error(t, err)
assert.Contains(t, err.Error(), "激活配置")
require.NoError(t, db.DB(ctx).Create(&model.ConfigVersion{
Version: "v2026-001",
SnapshotJSON: fmt.Sprintf(`{"routes":[{"upstream_type":"pages","pages_deployment":{"deployment_id":%d}}]}`, deployment.ID),
MainConfig: "",
RenderedConfig: "",
SupportFilesJSON: "[]",
Checksum: "test-checksum",
IsActive: true,
CreatedBy: "test",
}).Error)
packageObj, err := OpenDeploymentPackage(ctx, deployment.ID)
require.NoError(t, err)
defer packageObj.Body.Close()
assert.Equal(t, fmt.Sprintf("pages-deployment-%d.zip", deployment.ID), packageObj.FileName)
// Latest-by-project resolves the active package once the project is on active config.
depID, hash, err := GetProjectLatestPackageHash(ctx, project.ID)
require.NoError(t, err)
assert.Equal(t, deployment.ID, depID)
assert.NotEmpty(t, hash)
latestPkg, err := OpenProjectLatestPackage(ctx, project.ID)
require.NoError(t, err)
defer latestPkg.Body.Close()
body, err := io.ReadAll(packageObj.Body)
require.NoError(t, err)
reader, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
require.NoError(t, err)
require.Len(t, reader.File, 1)
assert.Equal(t, "index.html", reader.File[0].Name)
}
func TestProjectLatestRejectsWhenNotOnActiveConfigOrNotActive(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
project, err := CreateProject(ctx, Input{Name: "Gate", Slug: "gate", Enabled: true})
require.NoError(t, err)
d1, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "a.zip", testPagesZip(t, map[string]string{"index.html": "a"})), "root")
require.NoError(t, err)
d2, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "b.zip", testPagesZip(t, map[string]string{"index.html": "b"})), "root")
require.NoError(t, err)
_, err = ActivateDeployment(ctx, project.ID, d2.ID)
require.NoError(t, err)
// No active main config → reject.
_, _, err = GetProjectLatestPackageHash(ctx, project.ID)
require.Error(t, err)
require.NoError(t, db.DB(ctx).Create(&model.ConfigVersion{
Version: "v-gate",
SnapshotJSON: fmt.Sprintf(
`{"routes":[{"upstream_type":"pages","pages_project_id":%d,"pages_deployment":{"project_id":%d,"deployment_id":%d}}]}`,
project.ID, project.ID, d2.ID,
),
SupportFilesJSON: "[]",
Checksum: "c-gate",
IsActive: true,
CreatedBy: "test",
}).Error)
// Non-active historical deployment must not be downloadable.
_, err = OpenDeploymentPackage(ctx, d1.ID)
require.Error(t, err)
// Active latest works.
_, hash, err := GetProjectLatestPackageHash(ctx, project.ID)
require.NoError(t, err)
assert.NotEmpty(t, hash)
// Disabled project rejects.
require.NoError(t, db.DB(ctx).Model(&model.PagesProject{}).Where("id = ?", project.ID).Update("enabled", false).Error)
_, _, err = GetProjectLatestPackageHash(ctx, project.ID)
require.Error(t, err)
}
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)
require.NoError(t, err)
_, err = file.Write([]byte(content))
require.NoError(t, err)
}
require.NoError(t, writer.Close())
return buffer.Bytes()
}
func TestUploadDeploymentAcceptsTarGz(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
project, err := CreateProject(ctx, Input{
Name: "TarGz Site",
Slug: "tar-gz-site",
Enabled: true,
})
require.NoError(t, err)
deployment, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "site.tar.gz", testPagesTarGz(t, map[string]string{
"index.html": "tar-ok",
"app.js": "1",
})), "root")
require.NoError(t, err)
assert.Equal(t, 2, deployment.FileCount)
assert.NotZero(t, deployment.UploadID)
}
func TestSelectDeploymentsToPruneKeepsActiveAndNewest(t *testing.T) {
// ids 4(newest) ... 1(oldest); active is oldest id=1; keep=2 → keep {1,4}, prune {3,2}
deployments := []model.PagesDeployment{
{ID: 4, ProjectID: 1},
{ID: 3, ProjectID: 1},
{ID: 2, ProjectID: 1},
{ID: 1, ProjectID: 1},
}
toDelete := selectDeploymentsToPrune(deployments, 1, 0, 2)
require.Len(t, toDelete, 2)
assert.Equal(t, uint(3), toDelete[0].ID)
assert.Equal(t, uint(2), toDelete[1].ID)
// active is newest; keep=2 → keep {4,3}, prune {2,1}
toDelete = selectDeploymentsToPrune(deployments, 4, 0, 2)
require.Len(t, toDelete, 2)
assert.Equal(t, uint(2), toDelete[0].ID)
assert.Equal(t, uint(1), toDelete[1].ID)
// no active; keep=2 → keep {4,3}
toDelete = selectDeploymentsToPrune(deployments, 0, 0, 2)
require.Len(t, toDelete, 2)
assert.Equal(t, uint(2), toDelete[0].ID)
assert.Equal(t, uint(1), toDelete[1].ID)
// keep=1 with active → only active, prune the rest
toDelete = selectDeploymentsToPrune(deployments, 2, 0, 1)
require.Len(t, toDelete, 3)
for _, item := range toDelete {
assert.NotEqual(t, uint(2), item.ID)
}
// already within limit
assert.Nil(t, selectDeploymentsToPrune(deployments[:2], 4, 0, 2))
// unlimited
assert.Nil(t, selectDeploymentsToPrune(deployments, 1, 0, 0))
// history=1 temporarily preserves active plus the freshly uploaded candidate.
toDelete = selectDeploymentsToPrune(deployments, 2, 4, 1)
require.Len(t, toDelete, 2)
assert.Equal(t, uint(3), toDelete[0].ID)
assert.Equal(t, uint(1), toDelete[1].ID)
assert.Equal(t, uint(4), resolveLatestCandidateID(deployments, 2, true))
assert.Zero(t, resolveLatestCandidateID(deployments, 2, false))
}
func TestPruneProjectDeploymentHistory(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
require.NoError(t, db.DB(ctx).Model(&model.SystemConfig{}).
Where("key = ?", model.ConfigKeyPagesMaxHistoryCount).
Update("value", "2").Error)
require.NoError(t, repository.InvalidateSystemConfigCache(ctx, model.ConfigKeyPagesMaxHistoryCount))
project, err := CreateProject(ctx, Input{
Name: "History Site",
Slug: "history-site",
Enabled: true,
})
require.NoError(t, err)
var ids []uint
for i := 0; i < 3; i++ {
deployment, uploadErr := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "site.zip", testPagesZip(t, map[string]string{
"index.html": fmt.Sprintf("v%d", i),
})), "root")
require.NoError(t, uploadErr)
ids = append(ids, deployment.ID)
}
// After 3 uploads with keep=2 and no active: only 2 newest remain.
deployments, err := repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 2)
assert.Equal(t, ids[2], deployments[0].ID)
assert.Equal(t, ids[1], deployments[1].ID)
// Activate the older of the remaining two, then upload again.
_, err = ActivateDeployment(ctx, project.ID, ids[1])
require.NoError(t, err)
latest, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "site.zip", testPagesZip(t, map[string]string{
"index.html": "v-latest",
})), "root")
require.NoError(t, err)
deployments, err = repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 2, "must be at most N=2, not active+N newest")
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
require.NoError(t, err)
require.NotNil(t, storedProject.ActiveDeploymentID)
assert.Equal(t, ids[1], *storedProject.ActiveDeploymentID)
kept := map[uint]struct{}{}
for _, item := range deployments {
kept[item.ID] = struct{}{}
}
_, hasActive := kept[ids[1]]
_, hasLatest := kept[latest.ID]
assert.True(t, hasActive, "active deployment must be retained")
assert.True(t, hasLatest, "newest deployment must fill remaining slot")
}
func TestHistoryCountOnePreservesFreshCandidateUntilActivation(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
require.NoError(t, db.DB(ctx).Model(&model.SystemConfig{}).
Where("key = ?", model.ConfigKeyPagesMaxHistoryCount).
Update("value", "1").Error)
require.NoError(t, repository.InvalidateSystemConfigCache(ctx, model.ConfigKeyPagesMaxHistoryCount))
project, err := CreateProject(ctx, Input{Name: "Single History", Slug: "single-history", Enabled: true})
require.NoError(t, err)
active, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "v1.zip", testPagesZip(t, map[string]string{
"index.html": "v1",
})), "user:1")
require.NoError(t, err)
_, err = ActivateDeployment(ctx, project.ID, active.ID)
require.NoError(t, err)
oldCandidate, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "v2.zip", testPagesZip(t, map[string]string{
"index.html": "v2",
})), "user:1")
require.NoError(t, err)
deployments, err := repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 2)
newCandidate, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "v3.zip", testPagesZip(t, map[string]string{
"index.html": "v3",
})), "user:1")
require.NoError(t, err)
deployments, err = repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 2)
kept := map[uint]bool{}
for _, deployment := range deployments {
kept[deployment.ID] = true
}
assert.True(t, kept[active.ID])
assert.True(t, kept[newCandidate.ID])
assert.False(t, kept[oldCandidate.ID])
var removedUpload model.Upload
require.NoError(t, db.DB(ctx).First(&removedUpload, oldCandidate.UploadID).Error)
assert.Equal(t, model.UploadStatusDeleted, removedUpload.Status)
_, err = ActivateDeployment(ctx, project.ID, newCandidate.ID)
require.NoError(t, err)
deployments, err = repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 1)
assert.Equal(t, newCandidate.ID, deployments[0].ID)
}
func TestPruneUsesLockTimeNewestCandidateInsteadOfStaleCaller(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
project, err := CreateProject(ctx, Input{Name: "Concurrent Candidate", Slug: "concurrent-candidate", Enabled: true})
require.NoError(t, err)
active, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "v1.zip", testPagesZip(t, map[string]string{
"index.html": "v1",
})), "user:1")
require.NoError(t, err)
_, err = ActivateDeployment(ctx, project.ID, active.ID)
require.NoError(t, err)
staleCandidate, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "v2.zip", testPagesZip(t, map[string]string{
"index.html": "v2",
})), "user:1")
require.NoError(t, err)
newCandidate, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "v3.zip", testPagesZip(t, map[string]string{
"index.html": "v3",
})), "user:1")
require.NoError(t, err)
deleted, err := pruneProjectDeploymentHistoryOnce(ctx, project.ID, 1, staleCandidate.ID)
require.NoError(t, err)
assert.Equal(t, 1, deleted)
deployments, err := repository.ListPagesDeployments(ctx, project.ID)
require.NoError(t, err)
require.Len(t, deployments, 2)
kept := map[uint]bool{}
for _, deployment := range deployments {
kept[deployment.ID] = true
}
assert.True(t, kept[active.ID])
assert.True(t, kept[newCandidate.ID])
assert.False(t, kept[staleCandidate.ID])
}
func TestDeleteDeploymentAndProjectSoftDeleteUnreferencedArtifacts(t *testing.T) {
cleanup := setupPagesTestDB(t)
defer cleanup()
_, disableStorage := setupPagesStorageMock(t)
defer disableStorage()
ctx := context.Background()
project, err := CreateProject(ctx, Input{Name: "Delete Artifacts", Slug: "delete-artifacts", Enabled: true})
require.NoError(t, err)
first, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "first.zip", testPagesZip(t, map[string]string{
"index.html": "first",
})), "user:1")
require.NoError(t, err)
second, err := UploadDeployment(ctx, project.ID, testPagesMultipartFile(t, "second.zip", testPagesZip(t, map[string]string{
"index.html": "second",
})), "user:1")
require.NoError(t, err)
require.NoError(t, DeleteDeployment(ctx, project.ID, second.ID))
var secondUpload model.Upload
require.NoError(t, db.DB(ctx).First(&secondUpload, second.UploadID).Error)
assert.Equal(t, model.UploadStatusDeleted, secondUpload.Status)
require.NoError(t, DeleteProject(ctx, project.ID))
var firstUpload model.Upload
require.NoError(t, db.DB(ctx).First(&firstUpload, first.UploadID).Error)
assert.Equal(t, model.UploadStatusDeleted, firstUpload.Status)
_, err = repository.GetPagesProjectByID(ctx, project.ID)
assert.Error(t, err)
}
func testPagesTarGz(t *testing.T, files map[string]string) []byte {
t.Helper()
var buffer bytes.Buffer
gzWriter := gzip.NewWriter(&buffer)
tarWriter := tar.NewWriter(gzWriter)
for name, content := range files {
require.NoError(t, tarWriter.WriteHeader(&tar.Header{
Name: name,
Mode: 0o644,
Size: int64(len(content)),
}))
_, err := tarWriter.Write([]byte(content))
require.NoError(t, err)
}
require.NoError(t, tarWriter.Close())
require.NoError(t, gzWriter.Close())
return buffer.Bytes()
}
func testPagesMultipartFile(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)
require.NoError(t, err)
_, err = part.Write(content)
require.NoError(t, err)
require.NoError(t, writer.Close())
req := httptest.NewRequest("POST", "/", &body)
req.Header.Set("Content-Type", writer.FormDataContentType())
require.NoError(t, req.ParseMultipartForm(int64(len(content))+1024))
file, header, err := req.FormFile("package")
require.NoError(t, err)
file.Close()
return header
}