733 lines
26 KiB
Go
733 lines
26 KiB
Go
package releasekit_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
|
|
)
|
|
|
|
func TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
temporaryRoot := t.TempDir()
|
|
t.Setenv("TMPDIR", temporaryRoot)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
var progress strings.Builder
|
|
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Build: &releasekit.BuildSpec{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
|
},
|
|
Output: &progress,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Deliver() error = %v; result = %#v", err, result)
|
|
}
|
|
observed := remote.snapshot()
|
|
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != observed.commit {
|
|
t.Fatalf("delivery result = %#v", result)
|
|
}
|
|
if len(result.Uploaded) != 2 || len(observed.assets) != 2 {
|
|
t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(observed.assets))
|
|
}
|
|
if !strings.Contains(progress.String(), "checking fixture version output") {
|
|
t.Fatalf("build progress = %q", progress.String())
|
|
}
|
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
|
t.Fatalf("managed build output remains: entries=%v error=%v", entries, err)
|
|
}
|
|
}
|
|
|
|
func TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
var progress strings.Builder
|
|
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Build: &releasekit.BuildSpec{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{
|
|
{OS: "linux", Arch: "amd64"},
|
|
{OS: "linux", Arch: "arm64"},
|
|
},
|
|
},
|
|
Output: &progress,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Deliver() error = %v; result = %#v", err, result)
|
|
}
|
|
observed := remote.snapshot()
|
|
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != observed.commit {
|
|
t.Fatalf("delivery result = %#v", result)
|
|
}
|
|
wantAssets := []string{
|
|
"fixture-v1.2.3-linux-amd64",
|
|
"fixture-v1.2.3-linux-arm64",
|
|
"SHA256SUMS",
|
|
}
|
|
if len(result.Uploaded) != len(wantAssets) || len(observed.assets) != len(wantAssets) {
|
|
t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(observed.assets))
|
|
}
|
|
for _, name := range wantAssets {
|
|
if _, ok := observed.assets[name]; !ok {
|
|
t.Errorf("hosted assets omit %q: %v", name, mapKeys(observed.assets))
|
|
}
|
|
}
|
|
if !strings.Contains(progress.String(), "checking fixture version output") {
|
|
t.Fatalf("build progress = %q", progress.String())
|
|
}
|
|
}
|
|
|
|
func TestDeliverPrebuiltArtifactsRemainCallerOwned(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
outputDir := filepath.Join(t.TempDir(), "caller-owned-assets")
|
|
prebuilt, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
|
RepositoryDir: repository,
|
|
Version: "v1.2.3",
|
|
Spec: releasekit.BuildSpec{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
|
},
|
|
OutputDir: outputDir,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
server, _ := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Prebuilt: &prebuilt,
|
|
})
|
|
if err != nil || result.State != releasekit.DeliveryComplete {
|
|
t.Fatalf("Deliver() = %#v, %v", result, err)
|
|
}
|
|
for _, artifact := range prebuilt.Artifacts {
|
|
if _, err := os.Stat(artifact.Path); err != nil {
|
|
t.Errorf("caller-owned artifact %q was removed: %v", artifact.Path, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
prebuilt, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
|
RepositoryDir: repository,
|
|
Version: "v1.2.3",
|
|
Spec: releasekit.BuildSpec{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
|
},
|
|
OutputDir: filepath.Join(t.TempDir(), "caller-owned-assets"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
contents, err := os.ReadFile(prebuilt.Artifacts[0].Path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(contents) == 0 {
|
|
t.Fatal("built artifact is empty")
|
|
}
|
|
contents[0] ^= 0xff
|
|
if err := os.WriteFile(prebuilt.Artifacts[0].Path, contents, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Prebuilt: &prebuilt,
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "verify build assets") || result.State != releasekit.DeliveryNotAttempted {
|
|
t.Fatalf("Deliver() = %#v, %v; want local asset verification failure", result, err)
|
|
}
|
|
if remote.snapshot().requests != 0 {
|
|
t.Fatalf("hosted requests = %d, want none before local verification", remote.snapshot().requests)
|
|
}
|
|
}
|
|
|
|
func TestDeliverPageOnlySkipsBuildAndHostedAssets(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
build *releasekit.BuildSpec
|
|
prebuilt *releasekit.BuildResult
|
|
}{
|
|
{name: "no build"},
|
|
{name: "empty target set", build: &releasekit.BuildSpec{}},
|
|
{name: "empty prebuilt result", prebuilt: &releasekit.BuildResult{}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
prebuilt := test.prebuilt
|
|
if prebuilt != nil {
|
|
built, err := releasekit.Build(context.Background(), releasekit.BuildRequest{RepositoryDir: repository, Version: "v1.2.3"})
|
|
if err != nil {
|
|
t.Fatalf("Build() for page-only prebuilt identity = %#v, %v", built, err)
|
|
}
|
|
prebuilt = &built
|
|
}
|
|
temporaryRoot := t.TempDir()
|
|
t.Setenv("TMPDIR", temporaryRoot)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
var progress strings.Builder
|
|
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Build: test.build,
|
|
Prebuilt: prebuilt,
|
|
Output: &progress,
|
|
})
|
|
if err != nil || result.State != releasekit.DeliveryComplete {
|
|
t.Fatalf("Deliver() = %#v, %v", result, err)
|
|
}
|
|
if len(remote.snapshot().assets) != 0 || len(result.Uploaded) != 0 || len(result.Pending) != 0 {
|
|
t.Fatalf("page-only delivery included assets: result=%#v assets=%v", result, mapKeys(remote.snapshot().assets))
|
|
}
|
|
if progress.Len() != 0 {
|
|
t.Fatalf("page-only delivery emitted build progress: %q", progress.String())
|
|
}
|
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
|
t.Fatalf("page-only delivery allocated temporary output: entries=%v error=%v", entries, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeliverUsesCommittedModuleIdentityForSeparatePages(t *testing.T) {
|
|
repository := newTwoModuleDeliveryConsumer(t)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
|
|
wrongTag, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
ExpectedTag: "integrations/wrong/v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
})
|
|
if err == nil || wrongTag.Tag != "v1.2.3" || remote.snapshot().requests != 0 {
|
|
t.Fatalf("delivery with inconsistent expected tag = %#v, %v; hosted requests %d", wrongTag, err, remote.snapshot().requests)
|
|
}
|
|
|
|
rootResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
ExpectedTag: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
})
|
|
if err != nil || rootResult.State != releasekit.DeliveryComplete || rootResult.Tag != "v1.2.3" {
|
|
t.Fatalf("root delivery = %#v, %v", rootResult, err)
|
|
}
|
|
|
|
nestedTag := "integrations/ckconfig/v1.2.3"
|
|
remote.update(func(remote *facadeDeliveryData) {
|
|
remote.tag = nestedTag
|
|
remote.noteBody = "# Fixture v1.2.3\n\nNested delivery fixture.\n"
|
|
})
|
|
nestedResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
ModuleDir: "integrations/ckconfig",
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
ExpectedTag: nestedTag,
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
})
|
|
if err != nil || nestedResult.State != releasekit.DeliveryComplete || nestedResult.Tag != nestedTag || nestedResult.ModulePath != "example.com/ckconfig" {
|
|
t.Fatalf("nested delivery = %#v, %v", nestedResult, err)
|
|
}
|
|
if len(remote.snapshot().pages) != 2 || !remote.snapshot().pages["v1.2.3"] || !remote.snapshot().pages[nestedTag] {
|
|
t.Fatalf("created hosted pages = %#v", remote.snapshot().pages)
|
|
}
|
|
}
|
|
|
|
func TestDeliverRejectsPrebuiltArtifactsForAnotherModuleBeforeHostedRequests(t *testing.T) {
|
|
repository := newTwoModuleDeliveryConsumer(t)
|
|
prebuilt, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
|
RepositoryDir: repository,
|
|
Version: "v1.2.3",
|
|
Spec: releasekit.BuildSpec{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
|
},
|
|
OutputDir: filepath.Join(t.TempDir(), "root-artifacts"),
|
|
})
|
|
if err != nil || !prebuilt.SourceVerified {
|
|
t.Fatalf("root Build() = %#v, %v", prebuilt, err)
|
|
}
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
remote.update(func(remote *facadeDeliveryData) { remote.tag = "integrations/ckconfig/v1.2.3" })
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
ModuleDir: "integrations/ckconfig",
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
ExpectedTag: remote.snapshot().tag,
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Prebuilt: &prebuilt,
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "selected tagged module") || result.Tag != remote.snapshot().tag {
|
|
t.Fatalf("nested delivery with root artifacts = %#v, %v", result, err)
|
|
}
|
|
if remote.snapshot().requests != 0 {
|
|
t.Fatalf("hosted requests before prebuilt identity rejection = %d", remote.snapshot().requests)
|
|
}
|
|
}
|
|
|
|
func newTwoModuleDeliveryConsumer(t *testing.T) string {
|
|
t.Helper()
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
writeNestedBuildConsumer(t, repository, "integrations/ckconfig", "example.com/ckconfig", `package main
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
|
)
|
|
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("fixture", buildinfo.Version()) } }
|
|
`)
|
|
writeBuildFixture(t, repository, "integrations/ckconfig/docs/releases/v1.2.3.md", "# Fixture v1.2.3\n\nNested delivery fixture.\n")
|
|
buildGit(t, repository, "add", "--all")
|
|
buildGit(t, repository, "commit", "-m", "add nested release module")
|
|
buildGit(t, repository, "tag", "-f", "v1.2.3")
|
|
buildGit(t, repository, "tag", "integrations/ckconfig/v1.2.3")
|
|
return repository
|
|
}
|
|
|
|
func TestDeliverRejectsDirtyTaggedSourceBeforeBuildOrHostedWrites(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
writeBuildFixture(t, repository, "local-change.txt", "dirty source")
|
|
temporaryRoot := t.TempDir()
|
|
t.Setenv("TMPDIR", temporaryRoot)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
var progress strings.Builder
|
|
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Build: &releasekit.BuildSpec{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
|
},
|
|
Output: &progress,
|
|
})
|
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.snapshot().commit {
|
|
t.Fatalf("Deliver() = %#v, %v; want preflight failure for known commit", result, err)
|
|
}
|
|
if progress.Len() != 0 {
|
|
t.Fatalf("build started before source eligibility passed: %q", progress.String())
|
|
}
|
|
if remote.snapshot().requests != 0 {
|
|
t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
|
|
}
|
|
}
|
|
|
|
func TestDeliverRejectsCheckoutAwayFromTagBeforeBuildOrHostedWrites(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
tagCommit := buildGit(t, repository, "rev-parse", "HEAD")
|
|
writeBuildFixture(t, repository, "later-change.txt", "new commit")
|
|
buildGit(t, repository, "add", "--all")
|
|
buildGit(t, repository, "commit", "-m", "advance checkout after release tag")
|
|
temporaryRoot := t.TempDir()
|
|
t.Setenv("TMPDIR", temporaryRoot)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
var progress strings.Builder
|
|
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Build: &releasekit.BuildSpec{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
|
},
|
|
Output: &progress,
|
|
})
|
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit == tagCommit {
|
|
t.Fatalf("Deliver() = %#v, %v; want tag/HEAD mismatch", result, err)
|
|
}
|
|
if progress.Len() != 0 {
|
|
t.Fatalf("build started before tag/HEAD agreement: %q", progress.String())
|
|
}
|
|
if remote.snapshot().requests != 0 {
|
|
t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
|
|
}
|
|
}
|
|
|
|
func TestDeliverRejectsInvalidCommittedNoteBeforeBuildOrHostedWrites(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumerWithNote(t, "# Different Product v1.2.3\n\nWrong product.\n")
|
|
temporaryRoot := t.TempDir()
|
|
t.Setenv("TMPDIR", temporaryRoot)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
var progress strings.Builder
|
|
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Build: &releasekit.BuildSpec{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
|
},
|
|
Output: &progress,
|
|
})
|
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.snapshot().commit {
|
|
t.Fatalf("Deliver() = %#v, %v; want committed-note preflight failure", result, err)
|
|
}
|
|
if progress.Len() != 0 {
|
|
t.Fatalf("build started before committed-note validation: %q", progress.String())
|
|
}
|
|
if remote.snapshot().requests != 0 {
|
|
t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
|
|
}
|
|
}
|
|
|
|
func TestDeliverManagedBuildRejectsCheckoutMutationBeforeHostedWrites(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
temporaryRoot := t.TempDir()
|
|
t.Setenv("TMPDIR", temporaryRoot)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
writer := &sourceChangeWriter{change: func() {
|
|
source, err := os.ReadFile(filepath.Join(repository, "cmd/app/main.go"))
|
|
if err != nil {
|
|
t.Errorf("read application source: %v", err)
|
|
return
|
|
}
|
|
writeBuildFixture(t, repository, "cmd/app/main.go", string(source)+"\n// changed during delivery build\n")
|
|
}}
|
|
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Build: &releasekit.BuildSpec{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
|
},
|
|
Output: writer,
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "build source") || result.State != releasekit.DeliveryNotAttempted {
|
|
t.Fatalf("Deliver() = %#v, %v; want build-source eligibility failure", result, err)
|
|
}
|
|
if remote.snapshot().requests != 0 {
|
|
t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
|
|
}
|
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
|
t.Fatalf("ineligible managed build output remains: entries=%v error=%v", entries, err)
|
|
}
|
|
}
|
|
|
|
func TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
temporaryRoot := t.TempDir()
|
|
t.Setenv("TMPDIR", temporaryRoot)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Build: &releasekit.BuildSpec{
|
|
MainPackage: "./cmd/missing",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
|
},
|
|
})
|
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.snapshot().commit {
|
|
t.Fatalf("Deliver() = %#v, %v; want managed build failure before hosted writes", result, err)
|
|
}
|
|
if remote.snapshot().requests != 0 {
|
|
t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
|
|
}
|
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
|
t.Fatalf("failed managed build output remains: entries=%v error=%v", entries, err)
|
|
}
|
|
}
|
|
|
|
func TestDeliverManagedBuildCancellationCleansTemporaryOutput(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
temporaryRoot := t.TempDir()
|
|
t.Setenv("TMPDIR", temporaryRoot)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
writer := deliveryCancelWriter{cancel: cancel}
|
|
|
|
result, err := releasekit.Deliver(ctx, releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Build: &releasekit.BuildSpec{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
|
},
|
|
Output: writer,
|
|
})
|
|
if !errors.Is(err, context.Canceled) || result.State != releasekit.DeliveryNotAttempted {
|
|
t.Fatalf("Deliver() = %#v, %v; want cancellation before hosted writes", result, err)
|
|
}
|
|
if remote.snapshot().requests != 0 {
|
|
t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
|
|
}
|
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
|
t.Fatalf("canceled managed build output remains: entries=%v error=%v", entries, err)
|
|
}
|
|
}
|
|
|
|
func TestDeliverRejectsBothBuildSourcesBeforeHostedWrites(t *testing.T) {
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
server, remote := newFacadeDeliveryServer(t, repository)
|
|
defer server.Close()
|
|
|
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
|
RepositoryDir: repository,
|
|
Product: "Fixture",
|
|
Version: "v1.2.3",
|
|
Destination: facadeDeliveryDestination(server.URL),
|
|
Build: &releasekit.BuildSpec{},
|
|
Prebuilt: &releasekit.BuildResult{},
|
|
})
|
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || !strings.Contains(err.Error(), "both") {
|
|
t.Fatalf("Deliver() = %#v, %v; want invalid build-source combination", result, err)
|
|
}
|
|
if remote.snapshot().requests != 0 {
|
|
t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
|
|
}
|
|
}
|
|
|
|
func newTaggedDeliveryConsumer(t *testing.T) string {
|
|
return newTaggedDeliveryConsumerWithNote(t, "# Fixture v1.2.3\n\nDelivery fixture.\n")
|
|
}
|
|
|
|
func newTaggedDeliveryConsumerWithNote(t *testing.T, note string) string {
|
|
t.Helper()
|
|
repository := newBuildConsumer(t, `package main
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
|
)
|
|
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("fixture", buildinfo.Version()) } }
|
|
`)
|
|
writeBuildFixture(t, repository, "docs/releases/v1.2.3.md", note)
|
|
buildGit(t, repository, "add", "--all")
|
|
buildGit(t, repository, "commit", "-m", "add release note")
|
|
buildGit(t, repository, "tag", "v1.2.3")
|
|
return repository
|
|
}
|
|
|
|
func facadeDeliveryDestination(baseURL string) releasekit.GiteaDestination {
|
|
return releasekit.GiteaDestination{BaseURL: baseURL, Owner: "owner", Repository: "example", Token: "fixture-token"}
|
|
}
|
|
|
|
type deliveryCancelWriter struct{ cancel context.CancelFunc }
|
|
|
|
func (writer deliveryCancelWriter) Write(content []byte) (int, error) {
|
|
writer.cancel()
|
|
return len(content), nil
|
|
}
|
|
|
|
type facadeDeliveryServer struct {
|
|
mu sync.Mutex
|
|
data facadeDeliveryData
|
|
}
|
|
|
|
func (state *facadeDeliveryServer) update(change func(*facadeDeliveryData)) {
|
|
state.mu.Lock()
|
|
defer state.mu.Unlock()
|
|
change(&state.data)
|
|
}
|
|
|
|
func (state *facadeDeliveryServer) snapshot() facadeDeliveryData {
|
|
state.mu.Lock()
|
|
defer state.mu.Unlock()
|
|
result := state.data
|
|
|
|
result.assets = make(map[string][]byte, len(state.data.assets))
|
|
for key, value := range state.data.assets {
|
|
result.assets[key] = append([]byte(nil), value...)
|
|
}
|
|
|
|
result.pages = make(map[string]bool, len(state.data.pages))
|
|
for key, value := range state.data.pages {
|
|
result.pages[key] = value
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
type facadeDeliveryData struct {
|
|
t *testing.T
|
|
commit string
|
|
tag string
|
|
version string
|
|
noteBody string
|
|
pages map[string]bool
|
|
requests int
|
|
assets map[string][]byte
|
|
url string
|
|
}
|
|
|
|
func newFacadeDeliveryServer(t *testing.T, repository string) (*httptest.Server, *facadeDeliveryServer) {
|
|
t.Helper()
|
|
state := &facadeDeliveryServer{data: facadeDeliveryData{t: t, commit: buildGit(t, repository, "rev-parse", "HEAD"), tag: "v1.2.3", version: "v1.2.3", noteBody: "# Fixture v1.2.3\n\nDelivery fixture.\n", pages: make(map[string]bool), assets: make(map[string][]byte)}}
|
|
server := httptest.NewUnstartedServer(http.HandlerFunc(state.serveHTTP))
|
|
state.data.url = "http://" + server.Listener.Addr().String()
|
|
server.Start()
|
|
return server, state
|
|
}
|
|
|
|
func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
|
state.mu.Lock()
|
|
defer state.mu.Unlock()
|
|
data := &state.data
|
|
if strings.HasPrefix(r.URL.Path, "/api/") {
|
|
data.requests++
|
|
if r.Header.Get("Authorization") != "token fixture-token" {
|
|
data.t.Errorf("authorization = %q", r.Header.Get("Authorization"))
|
|
}
|
|
}
|
|
switch r.URL.EscapedPath() {
|
|
case "/api/v1/repos/owner/example/tags/" + url.PathEscape(data.tag):
|
|
_, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, data.tag, data.commit)
|
|
case "/api/v1/repos/owner/example/releases/tags/" + url.PathEscape(data.tag):
|
|
if !data.pages[data.tag] {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
state.writeRelease(w)
|
|
case "/api/v1/repos/owner/example/releases":
|
|
var payload struct {
|
|
Tag string `json:"tag_name"`
|
|
Target string `json:"target_commitish"`
|
|
Name string `json:"name"`
|
|
Body string `json:"body"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
data.t.Errorf("decode release payload: %v", err)
|
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if payload.Tag != data.tag || payload.Target != data.commit || payload.Name != "Fixture "+data.version {
|
|
data.t.Errorf("release payload = %#v", payload)
|
|
}
|
|
data.pages[data.tag] = true
|
|
state.writeRelease(w)
|
|
case "/api/v1/repos/owner/example/releases/10/assets":
|
|
if r.Method == http.MethodGet {
|
|
list := make([]map[string]any, 0, len(data.assets))
|
|
for name, content := range data.assets {
|
|
list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": data.url + "/files/" + name})
|
|
}
|
|
_ = json.NewEncoder(w).Encode(list)
|
|
return
|
|
}
|
|
name := r.URL.Query().Get("name")
|
|
file, _, err := r.FormFile("attachment")
|
|
if err != nil {
|
|
data.t.Errorf("read upload %q: %v", name, err)
|
|
http.Error(w, "invalid multipart upload", http.StatusBadRequest)
|
|
return
|
|
}
|
|
content, err := io.ReadAll(file)
|
|
_ = file.Close()
|
|
if err != nil {
|
|
data.t.Errorf("read upload %q: %v", name, err)
|
|
}
|
|
data.assets[name] = content
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": len(data.assets), "name": name, "size": len(content)})
|
|
default:
|
|
if strings.HasPrefix(r.URL.Path, "/files/") {
|
|
name := strings.TrimPrefix(r.URL.Path, "/files/")
|
|
content, ok := data.assets[name]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_, _ = w.Write(content)
|
|
return
|
|
}
|
|
data.t.Errorf("unexpected delivery request %s %s", r.Method, r.URL)
|
|
http.NotFound(w, r)
|
|
}
|
|
}
|
|
|
|
func (state *facadeDeliveryServer) writeRelease(w http.ResponseWriter) {
|
|
// The request handler holds state.mu.
|
|
data := &state.data
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": 10, "html_url": data.url + "/releases/10", "tag_name": data.tag,
|
|
"target_commitish": data.commit, "name": "Fixture " + data.version,
|
|
"body": data.noteBody, "draft": false, "prerelease": false,
|
|
})
|
|
}
|
|
|
|
func mapKeys(values map[string][]byte) []string {
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
return keys
|
|
}
|