mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
609 lines
22 KiB
Go
609 lines
22 KiB
Go
// Copyright 2026 Arctel.net
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package pages
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"Wavelet/openflare/plugins/server/kernel/repository"
|
|
|
|
"Wavelet/openflare/plugins/server/kernel/model"
|
|
"Wavelet/openflare/plugins/server/kernel/ofupload"
|
|
"Wavelet/openflare/share/pagesarchive"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupPagesSourceSyncTest(t *testing.T) context.Context {
|
|
t.Helper()
|
|
ctx := setupPagesSourceTest(t)
|
|
_, disableStorage := setupPagesStorageMock(t)
|
|
t.Cleanup(disableStorage)
|
|
return ctx
|
|
}
|
|
|
|
func mustAcquireRemoteSyncLease(
|
|
t *testing.T,
|
|
ctx context.Context,
|
|
source *model.PagesProjectSource,
|
|
) *sourceExecutionSnapshot {
|
|
t.Helper()
|
|
snapshot, outcome, err := acquireSourceLease(ctx, source.ID, source.ConfigVersion, sourceActionSync)
|
|
if err != nil {
|
|
t.Fatalf("acquireSourceLease(source=%d) error = %v, want nil", source.ID, err)
|
|
}
|
|
if got, want := outcome, sourceLeaseAcquired; got != want {
|
|
t.Fatalf("acquireSourceLease(source=%d) outcome = %q, want %q", source.ID, got, want)
|
|
}
|
|
if snapshot == nil {
|
|
t.Fatalf("acquireSourceLease(source=%d) snapshot = nil, want non-nil", source.ID)
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
func mustCreateActiveManualDeployment(
|
|
t *testing.T,
|
|
ctx context.Context,
|
|
projectID uint,
|
|
content string,
|
|
) *model.PagesDeployment {
|
|
t.Helper()
|
|
view, err := UploadDeployment(
|
|
ctx,
|
|
projectID,
|
|
testPagesMultipartFile(t, "manual.zip", testPagesZip(t, map[string]string{"index.html": content})),
|
|
"user:1",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("UploadDeployment(project=%d) error = %v, want nil", projectID, err)
|
|
}
|
|
if _, err := ActivateDeploymentAs(ctx, projectID, view.ID, "user:1"); err != nil {
|
|
t.Fatalf("ActivateDeploymentAs(project=%d, deployment=%d) error = %v, want nil", projectID, view.ID, err)
|
|
}
|
|
deployment, err := repository.GetPagesDeploymentByID(ctx, view.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPagesDeploymentByID(%d) error = %v, want nil", view.ID, err)
|
|
}
|
|
return deployment
|
|
}
|
|
|
|
func newPagesArchiveServer(t *testing.T, status int, body []byte, beforeWrite func() error) *httptest.Server {
|
|
t.Helper()
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
|
if beforeWrite != nil {
|
|
if err := beforeWrite(); err != nil {
|
|
writer.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
writer.Header().Set("Content-Type", "application/zip")
|
|
writer.Header().Set("Content-Disposition", `attachment; filename="site.zip"`)
|
|
writer.WriteHeader(status)
|
|
_, _ = writer.Write(body)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
return server
|
|
}
|
|
|
|
func TestSyncRemoteSourceAtomicallyActivatesAndReusesChecksum(t *testing.T) {
|
|
ctx := setupPagesSourceSyncTest(t)
|
|
project := mustCreatePagesSourceProject(t, ctx, "sync-success")
|
|
packageBytes := testPagesZip(t, map[string]string{
|
|
"index.html": "remote-v1",
|
|
"assets/app.js": "console.log('ok')",
|
|
})
|
|
server := newPagesArchiveServer(t, http.StatusOK, packageBytes, nil)
|
|
secret := "sync-query-secret"
|
|
source, _ := mustConfigureRemoteSource(
|
|
t,
|
|
ctx,
|
|
project.ID,
|
|
server.URL+"/site.zip?token="+secret,
|
|
true,
|
|
)
|
|
|
|
firstSnapshot := mustAcquireRemoteSyncLease(t, ctx, source)
|
|
first, err := syncRemoteSource(ctx, firstSnapshot, "user:42")
|
|
if err != nil {
|
|
t.Fatalf("syncRemoteSource(first) error = %v, want nil", err)
|
|
}
|
|
if first == nil || first.Stale || first.Reused || first.Deployment == nil {
|
|
t.Fatalf("syncRemoteSource(first) = %+v, want new active deployment", first)
|
|
}
|
|
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", project.ID, err)
|
|
}
|
|
if storedProject.ActiveDeploymentID == nil || *storedProject.ActiveDeploymentID != first.Deployment.ID {
|
|
t.Fatalf("project ActiveDeploymentID = %v, want %d", storedProject.ActiveDeploymentID, first.Deployment.ID)
|
|
}
|
|
deployment, err := repository.GetPagesDeploymentByID(ctx, first.Deployment.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPagesDeploymentByID(%d) error = %v, want nil", first.Deployment.ID, err)
|
|
}
|
|
expectedHash := sha256.Sum256(packageBytes)
|
|
if got, want := deployment.Checksum, hex.EncodeToString(expectedHash[:]); got != want {
|
|
t.Errorf("deployment Checksum = %q, want %q", got, want)
|
|
}
|
|
if got, want := deployment.Status, model.PagesDeploymentStatusActive; got != want {
|
|
t.Errorf("deployment Status = %q, want %q", got, want)
|
|
}
|
|
if got, want := deployment.SourceType, PagesSourceTypeRemoteURL; got != want {
|
|
t.Errorf("deployment SourceType = %q, want %q", got, want)
|
|
}
|
|
if deployment.SourceIdentity == nil || *deployment.SourceIdentity != source.SourceIdentity {
|
|
t.Errorf("deployment SourceIdentity = %v, want %q", deployment.SourceIdentity, source.SourceIdentity)
|
|
}
|
|
if deployment.SourceRevision == nil || *deployment.SourceRevision != deployment.Checksum {
|
|
t.Errorf("deployment SourceRevision = %v, want %q", deployment.SourceRevision, deployment.Checksum)
|
|
}
|
|
if got, want := deployment.CreatedBy, "user:42"; got != want {
|
|
t.Errorf("deployment CreatedBy = %q, want %q", got, want)
|
|
}
|
|
if got, want := deployment.TriggerType, pagesSourceTriggerManualSync; got != want {
|
|
t.Errorf("deployment TriggerType = %q, want %q", got, want)
|
|
}
|
|
if strings.Contains(deployment.SourceMeta, secret) || strings.Contains(deployment.SourceLabel, secret) {
|
|
t.Errorf("deployment provenance = label:%q meta:%q, want no query secret", deployment.SourceLabel, deployment.SourceMeta)
|
|
}
|
|
var runtime model.PagesProjectSourceRuntime
|
|
if err := repository.DB(ctx).Where("source_id = ?", source.ID).First(&runtime).Error; err != nil {
|
|
t.Fatalf("load source runtime error = %v, want nil", err)
|
|
}
|
|
if got, want := runtime.SyncStatus, pagesSourceStatusIdle; got != want {
|
|
t.Errorf("runtime SyncStatus = %q, want %q", got, want)
|
|
}
|
|
if got, want := runtime.LastAppliedRevision, deployment.Checksum; got != want {
|
|
t.Errorf("runtime LastAppliedRevision = %q, want %q", got, want)
|
|
}
|
|
if runtime.LeaseToken != "" || runtime.LeaseExpiresAt != nil {
|
|
t.Errorf("runtime lease = (%q, %v), want cleared", runtime.LeaseToken, runtime.LeaseExpiresAt)
|
|
}
|
|
var uploadRecord model.Upload
|
|
if err := repository.DB(ctx).First(&uploadRecord, deployment.UploadID).Error; err != nil {
|
|
t.Fatalf("load deployment upload %d error = %v, want nil", deployment.UploadID, err)
|
|
}
|
|
if got, want := uploadRecord.Status, model.UploadStatusUsed; got != want {
|
|
t.Errorf("deployment upload Status = %q, want %q", got, want)
|
|
}
|
|
if got, want := uploadRecord.Type, ofupload.ReservedPagesDeploymentType; got != want {
|
|
t.Errorf("deployment upload Type = %q, want %q", got, want)
|
|
}
|
|
if got, want := fmt.Sprint(uploadRecord.Metadata.Extra[pagesSourceIDMetadataKey]), fmt.Sprint(source.ID); got != want {
|
|
t.Errorf("deployment upload pages_source_id = %q, want %q", got, want)
|
|
}
|
|
|
|
secondSnapshot := mustAcquireRemoteSyncLease(t, ctx, source)
|
|
second, err := syncRemoteSource(ctx, secondSnapshot, "user:42")
|
|
if err != nil {
|
|
t.Fatalf("syncRemoteSource(second) error = %v, want nil", err)
|
|
}
|
|
if second == nil || second.Stale || !second.Reused || second.Deployment == nil {
|
|
t.Fatalf("syncRemoteSource(second) = %+v, want reused active deployment", second)
|
|
}
|
|
if got, want := second.Deployment.ID, first.Deployment.ID; got != want {
|
|
t.Errorf("reused deployment ID = %d, want %d", got, want)
|
|
}
|
|
var deploymentCount, uploadCount int64
|
|
if err := repository.DB(ctx).Model(&model.PagesDeployment{}).Where("project_id = ?", project.ID).Count(&deploymentCount).Error; err != nil {
|
|
t.Fatalf("count source deployments error = %v, want nil", err)
|
|
}
|
|
if err := repository.DB(ctx).Model(&model.Upload{}).Count(&uploadCount).Error; err != nil {
|
|
t.Fatalf("count source uploads error = %v, want nil", err)
|
|
}
|
|
if got, want := deploymentCount, int64(1); got != want {
|
|
t.Errorf("deployment count after identical sync = %d, want %d", got, want)
|
|
}
|
|
if got, want := uploadCount, int64(1); got != want {
|
|
t.Errorf("upload count after identical sync = %d, want %d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestSyncRemoteSourceDownloadFailureKeepsOldActive(t *testing.T) {
|
|
ctx := setupPagesSourceSyncTest(t)
|
|
project := mustCreatePagesSourceProject(t, ctx, "sync-download-fail")
|
|
oldActive := mustCreateActiveManualDeployment(t, ctx, project.ID, "old-active")
|
|
server := newPagesArchiveServer(t, http.StatusBadGateway, []byte("upstream failed"), nil)
|
|
secret := "download-failure-secret"
|
|
source, _ := mustConfigureRemoteSource(
|
|
t,
|
|
ctx,
|
|
project.ID,
|
|
server.URL+"/site.zip?token="+secret,
|
|
true,
|
|
)
|
|
|
|
_, err := syncRemoteSource(ctx, mustAcquireRemoteSyncLease(t, ctx, source), "user:2")
|
|
if err == nil {
|
|
t.Fatal("syncRemoteSource(download failure) error = nil, want non-nil")
|
|
}
|
|
if strings.Contains(err.Error(), secret) {
|
|
t.Errorf("syncRemoteSource(download failure) error = %q, want no query secret", err)
|
|
}
|
|
assertPagesSyncFailureState(t, ctx, project.ID, source.ID, oldActive.ID, 1)
|
|
}
|
|
|
|
func TestSyncRemoteSourceArchiveFailureKeepsOldActive(t *testing.T) {
|
|
ctx := setupPagesSourceSyncTest(t)
|
|
project := mustCreatePagesSourceProject(t, ctx, "sync-archive-fail")
|
|
oldActive := mustCreateActiveManualDeployment(t, ctx, project.ID, "old-active")
|
|
server := newPagesArchiveServer(t, http.StatusOK, []byte("not-a-valid-zip"), nil)
|
|
secret := "archive-failure-secret"
|
|
source, _ := mustConfigureRemoteSource(
|
|
t,
|
|
ctx,
|
|
project.ID,
|
|
server.URL+"/site.zip?token="+secret,
|
|
true,
|
|
)
|
|
|
|
_, err := syncRemoteSource(ctx, mustAcquireRemoteSyncLease(t, ctx, source), "user:3")
|
|
if err == nil {
|
|
t.Fatal("syncRemoteSource(archive failure) error = nil, want non-nil")
|
|
}
|
|
if strings.Contains(err.Error(), secret) {
|
|
t.Errorf("syncRemoteSource(archive failure) error = %q, want no query secret", err)
|
|
}
|
|
assertPagesSyncFailureState(t, ctx, project.ID, source.ID, oldActive.ID, 1)
|
|
}
|
|
|
|
func TestSyncRemoteSourceFinalFenceCompensatesIngest(t *testing.T) {
|
|
ctx := setupPagesSourceSyncTest(t)
|
|
project := mustCreatePagesSourceProject(t, ctx, "sync-final-fence")
|
|
oldActive := mustCreateActiveManualDeployment(t, ctx, project.ID, "old-active")
|
|
packageBytes := testPagesZip(t, map[string]string{"index.html": "never-activate"})
|
|
mutationResult := make(chan error, 1)
|
|
server := newPagesArchiveServer(t, http.StatusOK, packageBytes, func() error {
|
|
err := repository.DB(context.Background()).Model(&model.PagesProject{}).
|
|
Where("id = ?", project.ID).
|
|
Update("content_config_version", gorm.Expr("content_config_version + 1")).Error
|
|
mutationResult <- err
|
|
return err
|
|
})
|
|
source, _ := mustConfigureRemoteSource(
|
|
t,
|
|
ctx,
|
|
project.ID,
|
|
server.URL+"/site.zip?token=final-fence-secret",
|
|
true,
|
|
)
|
|
|
|
outcome, err := syncRemoteSource(ctx, mustAcquireRemoteSyncLease(t, ctx, source), "user:4")
|
|
if err != nil {
|
|
t.Fatalf("syncRemoteSource(final fence) error = %v, want nil stale outcome", err)
|
|
}
|
|
select {
|
|
case mutationErr := <-mutationResult:
|
|
if mutationErr != nil {
|
|
t.Fatalf("content version mutation error = %v, want nil", mutationErr)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("content version mutation was not observed")
|
|
}
|
|
if outcome == nil || !outcome.Stale || outcome.Deployment != nil {
|
|
t.Fatalf("syncRemoteSource(final fence) = %+v, want stale outcome without deployment", outcome)
|
|
}
|
|
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", project.ID, err)
|
|
}
|
|
if storedProject.ActiveDeploymentID == nil || *storedProject.ActiveDeploymentID != oldActive.ID {
|
|
t.Errorf("ActiveDeploymentID after final fence = %v, want %d", storedProject.ActiveDeploymentID, oldActive.ID)
|
|
}
|
|
var deployments []model.PagesDeployment
|
|
if err := repository.DB(ctx).Where("project_id = ?", project.ID).Find(&deployments).Error; err != nil {
|
|
t.Fatalf("list deployments after final fence error = %v, want nil", err)
|
|
}
|
|
if got, want := len(deployments), 1; got != want {
|
|
t.Errorf("deployment count after final fence = %d, want %d", got, want)
|
|
}
|
|
var uploads []model.Upload
|
|
if err := repository.DB(ctx).Order("id asc").Find(&uploads).Error; err != nil {
|
|
t.Fatalf("list uploads after final fence error = %v, want nil", err)
|
|
}
|
|
var compensated *model.Upload
|
|
for index := range uploads {
|
|
if fmt.Sprint(uploads[index].Metadata.Extra[pagesSourceIDMetadataKey]) == fmt.Sprint(source.ID) {
|
|
compensated = &uploads[index]
|
|
break
|
|
}
|
|
}
|
|
if compensated == nil {
|
|
t.Fatalf("source upload after final fence = nil, want compensated upload record")
|
|
}
|
|
if got, want := compensated.Status, model.UploadStatusDeleted; got != want {
|
|
t.Errorf("compensated upload Status = %q, want %q", got, want)
|
|
}
|
|
var danglingCount int64
|
|
if err := repository.DB(ctx).Model(&model.PagesDeployment{}).
|
|
Where("upload_id = ?", compensated.ID).
|
|
Count(&danglingCount).Error; err != nil {
|
|
t.Fatalf("count compensated upload references error = %v, want nil", err)
|
|
}
|
|
if got, want := danglingCount, int64(0); got != want {
|
|
t.Errorf("deployments referencing compensated upload = %d, want %d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestCommitSourceDeploymentRechecksLeaseAfterUploadLocks(t *testing.T) {
|
|
ctx := setupPagesSourceSyncTest(t)
|
|
project := mustCreatePagesSourceProject(t, ctx, "sync-expiry-recheck")
|
|
packageBytes := testPagesZip(t, map[string]string{"index.html": "expiry-recheck"})
|
|
server := newPagesArchiveServer(t, http.StatusOK, packageBytes, nil)
|
|
source, _ := mustConfigureRemoteSource(
|
|
t,
|
|
ctx,
|
|
project.ID,
|
|
server.URL+"/site.zip",
|
|
true,
|
|
)
|
|
first, err := syncRemoteSource(ctx, mustAcquireRemoteSyncLease(t, ctx, source), "user:5")
|
|
if err != nil || first == nil || first.Deployment == nil {
|
|
t.Fatalf("syncRemoteSource(seed) = (%+v, %v), want deployment", first, err)
|
|
}
|
|
deployment, err := repository.GetPagesDeploymentByID(ctx, first.Deployment.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPagesDeploymentByID(%d) error = %v, want nil", first.Deployment.ID, err)
|
|
}
|
|
if err := repository.DB(ctx).Model(&model.PagesProject{}).
|
|
Where("id = ?", project.ID).
|
|
Update("active_deployment_id", nil).Error; err != nil {
|
|
t.Fatalf("clear active deployment error = %v, want nil", err)
|
|
}
|
|
if err := repository.DB(ctx).Model(&model.PagesDeployment{}).
|
|
Where("id = ?", deployment.ID).
|
|
Update("status", model.PagesDeploymentStatusUploaded).Error; err != nil {
|
|
t.Fatalf("reset deployment status error = %v, want nil", err)
|
|
}
|
|
|
|
snapshot := mustAcquireRemoteSyncLease(t, ctx, source)
|
|
expiresAt := time.Now().Add(time.Hour)
|
|
if err := repository.DB(ctx).Model(&model.PagesProjectSourceRuntime{}).
|
|
Where("source_id = ? AND lease_token = ?", source.ID, snapshot.LeaseToken).
|
|
Update("lease_expires_at", &expiresAt).Error; err != nil {
|
|
t.Fatalf("set deterministic lease expiry error = %v, want nil", err)
|
|
}
|
|
originalNow := sourceCommitNow
|
|
nowCalls := 0
|
|
sourceCommitNow = func() time.Time {
|
|
nowCalls++
|
|
if nowCalls == 1 {
|
|
return expiresAt.Add(-time.Second)
|
|
}
|
|
return expiresAt.Add(time.Second)
|
|
}
|
|
t.Cleanup(func() { sourceCommitNow = originalNow })
|
|
|
|
_, _, _, err = commitSourceDeployment(
|
|
ctx,
|
|
snapshot,
|
|
deployment.Checksum,
|
|
deployment.Checksum,
|
|
sourceDetail{Provider: PagesSourceTypeRemoteURL, DisplayName: deployment.SourceLabel},
|
|
deployment.SourceMeta,
|
|
"user:5",
|
|
&deploymentManifest{},
|
|
ofupload.IngestResult{},
|
|
false,
|
|
nil,
|
|
)
|
|
if !errors.Is(err, errSourceFinalFence) {
|
|
t.Fatalf("commitSourceDeployment(expired after upload lock) error = %v, want %v", err, errSourceFinalFence)
|
|
}
|
|
if nowCalls != 2 {
|
|
t.Fatalf("sourceCommitNow calls = %d, want runtime-lock and post-upload checks", nowCalls)
|
|
}
|
|
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", project.ID, err)
|
|
}
|
|
if storedProject.ActiveDeploymentID != nil {
|
|
t.Errorf("ActiveDeploymentID after expiry recheck = %v, want nil", storedProject.ActiveDeploymentID)
|
|
}
|
|
storedDeployment, err := repository.GetPagesDeploymentByID(ctx, deployment.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPagesDeploymentByID(%d) after expiry error = %v, want nil", deployment.ID, err)
|
|
}
|
|
if got, want := storedDeployment.Status, model.PagesDeploymentStatusUploaded; got != want {
|
|
t.Errorf("deployment status after expiry recheck = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestCompensateSourceIngestSurvivesCanceledParentContext(t *testing.T) {
|
|
ctx := setupPagesSourceSyncTest(t)
|
|
project := mustCreatePagesSourceProject(t, ctx, "sync-canceled-compensation")
|
|
source, _ := mustConfigureRemoteSource(
|
|
t,
|
|
ctx,
|
|
project.ID,
|
|
"https://example.com/site.zip",
|
|
false,
|
|
)
|
|
packageBytes := testPagesZip(t, map[string]string{"index.html": "cancel-compensation"})
|
|
packagePath := filepath.Join(t.TempDir(), "site.zip")
|
|
if err := os.WriteFile(packagePath, packageBytes, 0o600); err != nil {
|
|
t.Fatalf("write test package error = %v, want nil", err)
|
|
}
|
|
digest := sha256.Sum256(packageBytes)
|
|
result, err := ingestPagesDeploymentPackageWithSource(
|
|
ctx,
|
|
packagePath,
|
|
hex.EncodeToString(digest[:]),
|
|
project.ID,
|
|
source.ID,
|
|
"site.zip",
|
|
pagesarchive.FormatZip,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ingestPagesDeploymentPackageWithSource() error = %v, want nil", err)
|
|
}
|
|
if !result.Created {
|
|
t.Fatal("ingest result Created = false, want a compensatable record")
|
|
}
|
|
|
|
canceledCtx, cancel := context.WithCancel(ctx)
|
|
cancel()
|
|
compensateSourceIngest(canceledCtx, &sourceExecutionSnapshot{
|
|
ProjectID: project.ID,
|
|
SourceID: source.ID,
|
|
}, &sourceIngestState{
|
|
Result: result,
|
|
HasIngest: true,
|
|
})
|
|
|
|
var uploadRecord model.Upload
|
|
if err := repository.DB(ctx).Where("id = ?", result.Upload.ID).First(&uploadRecord).Error; err != nil {
|
|
t.Fatalf("load compensated upload error = %v, want nil", err)
|
|
}
|
|
if got, want := uploadRecord.Status, model.UploadStatusDeleted; got != want {
|
|
t.Errorf("compensated upload status = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func assertPagesSyncFailureState(
|
|
t *testing.T,
|
|
ctx context.Context,
|
|
projectID uint,
|
|
sourceID uint,
|
|
oldActiveID uint,
|
|
wantDeploymentCount int64,
|
|
) {
|
|
t.Helper()
|
|
project, err := repository.GetPagesProjectByID(ctx, projectID)
|
|
if err != nil {
|
|
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", projectID, err)
|
|
}
|
|
if project.ActiveDeploymentID == nil || *project.ActiveDeploymentID != oldActiveID {
|
|
t.Errorf("project %d ActiveDeploymentID = %v, want %d", projectID, project.ActiveDeploymentID, oldActiveID)
|
|
}
|
|
var deploymentCount int64
|
|
if err := repository.DB(ctx).Model(&model.PagesDeployment{}).
|
|
Where("project_id = ?", projectID).
|
|
Count(&deploymentCount).Error; err != nil {
|
|
t.Fatalf("count project %d deployments error = %v, want nil", projectID, err)
|
|
}
|
|
if got, want := deploymentCount, wantDeploymentCount; got != want {
|
|
t.Errorf("project %d deployment count = %d, want %d", projectID, got, want)
|
|
}
|
|
var runtime model.PagesProjectSourceRuntime
|
|
if err := repository.DB(ctx).Where("source_id = ?", sourceID).First(&runtime).Error; err != nil {
|
|
t.Fatalf("load source %d runtime error = %v, want nil", sourceID, err)
|
|
}
|
|
if got, want := runtime.SyncStatus, pagesSourceStatusFailed; got != want {
|
|
t.Errorf("source %d runtime SyncStatus = %q, want %q", sourceID, got, want)
|
|
}
|
|
if runtime.LeaseToken != "" || runtime.LeaseExpiresAt != nil {
|
|
t.Errorf("source %d runtime lease = (%q, %v), want cleared", sourceID, runtime.LeaseToken, runtime.LeaseExpiresAt)
|
|
}
|
|
}
|
|
|
|
func TestCommitSourceDeploymentRejectsDeletedTargetUpload(t *testing.T) {
|
|
ctx := setupPagesSourceSyncTest(t)
|
|
project := mustCreatePagesSourceProject(t, ctx, "sync-deleted-upload")
|
|
packageBytes := testPagesZip(t, map[string]string{"index.html": "content"})
|
|
server := newPagesArchiveServer(t, http.StatusOK, packageBytes, nil)
|
|
source, _ := mustConfigureRemoteSource(
|
|
t,
|
|
ctx,
|
|
project.ID,
|
|
server.URL+"/site.zip",
|
|
true,
|
|
)
|
|
snapshot := mustAcquireRemoteSyncLease(t, ctx, source)
|
|
|
|
// A pre-existing source deployment whose upload was removed must never be
|
|
// reactivated into a dangling active pointer.
|
|
identity := source.SourceIdentity
|
|
revision := strings.Repeat("d", 64)
|
|
uploadRecord := &model.Upload{
|
|
ID: 987654321,
|
|
UserID: 999,
|
|
FileName: "deleted.zip",
|
|
FilePath: "deleted.zip",
|
|
Size: 1,
|
|
MimeType: "application/zip",
|
|
Hash: revision,
|
|
Type: ofupload.ReservedPagesDeploymentType,
|
|
Status: model.UploadStatusDeleted,
|
|
}
|
|
if err := repository.DB(ctx).Table("w_uploads").Create(uploadRecord).Error; err != nil {
|
|
t.Fatalf("create deleted upload error = %v, want nil", err)
|
|
}
|
|
deployment := &model.PagesDeployment{
|
|
ProjectID: project.ID,
|
|
DeploymentNumber: 1,
|
|
Checksum: revision,
|
|
Status: model.PagesDeploymentStatusUploaded,
|
|
UploadID: uploadRecord.ID,
|
|
FileCount: 1,
|
|
TotalSize: 1,
|
|
CreatedBy: "user:1",
|
|
SourceType: PagesSourceTypeRemoteURL,
|
|
SourceIdentity: &identity,
|
|
SourceRevision: &revision,
|
|
SourceLabel: "deleted.zip",
|
|
SourceMeta: `{"provider":"remote_url","display_name":"deleted.zip"}`,
|
|
TriggerType: pagesSourceTriggerManualSync,
|
|
}
|
|
if err := repository.DB(ctx).Create(deployment).Error; err != nil {
|
|
t.Fatalf("create source deployment error = %v, want nil", err)
|
|
}
|
|
if err := repository.DB(ctx).Create(&model.PagesDeploymentFile{
|
|
DeploymentID: deployment.ID,
|
|
Path: "index.html",
|
|
Size: 1,
|
|
Checksum: revision,
|
|
}).Error; err != nil {
|
|
t.Fatalf("create source deployment file error = %v, want nil", err)
|
|
}
|
|
manifest := &deploymentManifest{
|
|
FileCount: 1,
|
|
TotalSize: 1,
|
|
EntryFile: "index.html",
|
|
}
|
|
_, _, _, err := commitSourceDeployment(
|
|
ctx,
|
|
snapshot,
|
|
revision,
|
|
revision,
|
|
sourceDetail{Provider: PagesSourceTypeRemoteURL, DisplayName: "deleted.zip"},
|
|
`{"provider":"remote_url","display_name":"deleted.zip"}`,
|
|
"user:1",
|
|
manifest,
|
|
ofupload.IngestResult{},
|
|
false,
|
|
nil,
|
|
)
|
|
if !errors.Is(err, errSourceFinalFence) {
|
|
t.Errorf("commitSourceDeployment(deleted upload) error = %v, want %v", err, errSourceFinalFence)
|
|
}
|
|
storedProject, err := repository.GetPagesProjectByID(ctx, project.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetPagesProjectByID(%d) error = %v, want nil", project.ID, err)
|
|
}
|
|
if storedProject.ActiveDeploymentID != nil {
|
|
t.Errorf("ActiveDeploymentID after deleted upload rejection = %v, want nil", storedProject.ActiveDeploymentID)
|
|
}
|
|
var activeCount int64
|
|
if err := repository.DB(ctx).Model(&model.PagesDeployment{}).
|
|
Where("project_id = ? AND status = ?", project.ID, model.PagesDeploymentStatusActive).
|
|
Count(&activeCount).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
t.Fatalf("count active deployments error = %v, want nil", err)
|
|
}
|
|
if got, want := activeCount, int64(0); got != want {
|
|
t.Errorf("active deployment count after deleted upload rejection = %d, want %d", got, want)
|
|
}
|
|
}
|