Publisher integration tests (48 tests): dry-run verification for all 8 publishers (GitHub, Docker, Homebrew, Scoop, AUR, Chocolatey, npm, LinuxKit), command building, config parsing, repository detection, artifact handling, cross-publisher name uniqueness, nil relCfg handling, checksum mapping, interface compliance. SDK generation tests (38 tests): orchestration, generator registry, interface compliance for all 4 languages (TypeScript, Python, Go, PHP), config defaults, SetVersion, spec detection priority across all 8 common paths. Breaking change detection tests (30 tests): oasdiff integration covering add/remove endpoints, required/optional params, response type changes, HTTP method removal, identical specs, multiple breaking changes, JSON format support, error handling, DiffExitCode, DiffResult structure. Co-Authored-By: Virgil <virgil@lethean.io>
989 lines
28 KiB
Go
989 lines
28 KiB
Go
package publishers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"forge.lthn.ai/core/go-devops/build"
|
|
"forge.lthn.ai/core/go/pkg/io"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- GitHub Publisher Integration Tests ---
|
|
|
|
func TestGitHubPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("dry run creates no files on disk", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "## v1.0.0\n\n- feat: initial release",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{
|
|
{Path: filepath.Join(tmpDir, "app-linux-amd64.tar.gz")},
|
|
{Path: filepath.Join(tmpDir, "app-darwin-arm64.tar.gz")},
|
|
{Path: filepath.Join(tmpDir, "CHECKSUMS.txt")},
|
|
},
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: true,
|
|
Prerelease: true,
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "test-org/test-repo", projectName: "testapp"}
|
|
|
|
// Capture stdout
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
// Verify dry run output contains expected information
|
|
assert.Contains(t, output, "DRY RUN: GitHub Release")
|
|
assert.Contains(t, output, "Repository: test-org/test-repo")
|
|
assert.Contains(t, output, "Version: v1.0.0")
|
|
assert.Contains(t, output, "Draft: true")
|
|
assert.Contains(t, output, "Prerelease: true")
|
|
assert.Contains(t, output, "Would upload artifacts:")
|
|
assert.Contains(t, output, "app-linux-amd64.tar.gz")
|
|
assert.Contains(t, output, "app-darwin-arm64.tar.gz")
|
|
assert.Contains(t, output, "CHECKSUMS.txt")
|
|
assert.Contains(t, output, "gh release create")
|
|
assert.Contains(t, output, "--draft")
|
|
assert.Contains(t, output, "--prerelease")
|
|
|
|
// Verify no files were created in the temp directory
|
|
entries, err := os.ReadDir(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, entries, "dry run should not create any files")
|
|
})
|
|
|
|
t.Run("dry run builds correct gh CLI command for standard release", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v2.3.0",
|
|
Changelog: "## v2.3.0\n\n### Features\n\n- new feature",
|
|
ProjectDir: "/tmp",
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/dist/app-linux-amd64.tar.gz"},
|
|
},
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: false,
|
|
Prerelease: false,
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, pubCfg, "owner/repo")
|
|
|
|
// Verify exact argument structure
|
|
assert.Equal(t, "release", args[0])
|
|
assert.Equal(t, "create", args[1])
|
|
assert.Equal(t, "v2.3.0", args[2])
|
|
|
|
// Should have --repo
|
|
repoIdx := indexOf(args, "--repo")
|
|
assert.Greater(t, repoIdx, -1)
|
|
assert.Equal(t, "owner/repo", args[repoIdx+1])
|
|
|
|
// Should have --title
|
|
titleIdx := indexOf(args, "--title")
|
|
assert.Greater(t, titleIdx, -1)
|
|
assert.Equal(t, "v2.3.0", args[titleIdx+1])
|
|
|
|
// Should have --notes (since changelog is non-empty)
|
|
assert.Contains(t, args, "--notes")
|
|
|
|
// Should NOT have --draft or --prerelease
|
|
assert.NotContains(t, args, "--draft")
|
|
assert.NotContains(t, args, "--prerelease")
|
|
})
|
|
|
|
t.Run("dry run uses generate-notes when changelog empty", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "",
|
|
ProjectDir: "/tmp",
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
|
|
args := p.buildCreateArgs(release, pubCfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "--generate-notes")
|
|
assert.NotContains(t, args, "--notes")
|
|
})
|
|
}
|
|
|
|
func TestGitHubPublisher_Integration_RepositoryDetection_Good(t *testing.T) {
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("uses relCfg repository when provided", func(t *testing.T) {
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: "/tmp",
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
relCfg := &mockReleaseConfig{repository: "explicit/repo"}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, buf.String(), "Repository: explicit/repo")
|
|
})
|
|
|
|
t.Run("detects repository from git remote when relCfg empty", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
cmd := exec.Command("git", "init")
|
|
cmd.Dir = tmpDir
|
|
require.NoError(t, cmd.Run())
|
|
|
|
cmd = exec.Command("git", "remote", "add", "origin", "https://github.com/detected/from-git.git")
|
|
cmd.Dir = tmpDir
|
|
require.NoError(t, cmd.Run())
|
|
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
relCfg := &mockReleaseConfig{repository: ""}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, buf.String(), "Repository: detected/from-git")
|
|
})
|
|
|
|
t.Run("fails when no repository available", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
relCfg := &mockReleaseConfig{repository: ""}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "could not determine repository")
|
|
})
|
|
}
|
|
|
|
func TestGitHubPublisher_Integration_ArtifactUpload_Good(t *testing.T) {
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("dry run lists all artifact types", func(t *testing.T) {
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Release notes",
|
|
ProjectDir: "/tmp",
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/dist/app-linux-amd64.tar.gz", Checksum: "abc123"},
|
|
{Path: "/dist/app-darwin-arm64.tar.gz", Checksum: "def456"},
|
|
{Path: "/dist/app-windows-amd64.zip", Checksum: "ghi789"},
|
|
{Path: "/dist/CHECKSUMS.txt"},
|
|
{Path: "/dist/app-linux-amd64.tar.gz.sig"},
|
|
},
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
|
|
err := p.dryRunPublish(release, pubCfg, "owner/repo")
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
assert.Contains(t, output, "Would upload artifacts:")
|
|
assert.Contains(t, output, "app-linux-amd64.tar.gz")
|
|
assert.Contains(t, output, "app-darwin-arm64.tar.gz")
|
|
assert.Contains(t, output, "app-windows-amd64.zip")
|
|
assert.Contains(t, output, "CHECKSUMS.txt")
|
|
assert.Contains(t, output, "app-linux-amd64.tar.gz.sig")
|
|
})
|
|
|
|
t.Run("executePublish appends artifact paths to gh command", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: "/tmp",
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/dist/file1.tar.gz"},
|
|
{Path: "/dist/file2.zip"},
|
|
},
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
|
|
args := p.buildCreateArgs(release, pubCfg, "owner/repo")
|
|
|
|
// The executePublish method appends artifact paths after these base args
|
|
for _, a := range release.Artifacts {
|
|
args = append(args, a.Path)
|
|
}
|
|
|
|
// Verify artifacts are at end of args
|
|
assert.Equal(t, "/dist/file1.tar.gz", args[len(args)-2])
|
|
assert.Equal(t, "/dist/file2.zip", args[len(args)-1])
|
|
})
|
|
}
|
|
|
|
// --- Docker Publisher Integration Tests ---
|
|
|
|
func TestDockerPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|
if err := validateDockerCli(); err != nil {
|
|
t.Skip("skipping: docker CLI not available")
|
|
}
|
|
|
|
p := NewDockerPublisher()
|
|
|
|
t.Run("dry run creates no images or containers", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a Dockerfile
|
|
err := os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte("FROM alpine:latest\n"), 0644)
|
|
require.NoError(t, err)
|
|
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v1.2.3",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "docker",
|
|
Extended: map[string]any{
|
|
"registry": "ghcr.io",
|
|
"image": "test-org/test-app",
|
|
"platforms": []any{"linux/amd64", "linux/arm64"},
|
|
"tags": []any{"latest", "{{.Version}}", "stable"},
|
|
"build_args": map[string]any{
|
|
"APP_VERSION": "{{.Version}}",
|
|
"GO_VERSION": "1.21",
|
|
},
|
|
},
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "test-org/test-app"}
|
|
|
|
err = p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
// Verify dry run output
|
|
assert.Contains(t, output, "DRY RUN: Docker Build & Push")
|
|
assert.Contains(t, output, "Version: v1.2.3")
|
|
assert.Contains(t, output, "Registry: ghcr.io")
|
|
assert.Contains(t, output, "Image: test-org/test-app")
|
|
assert.Contains(t, output, "Platforms: linux/amd64, linux/arm64")
|
|
|
|
// Verify resolved tags
|
|
assert.Contains(t, output, "ghcr.io/test-org/test-app:latest")
|
|
assert.Contains(t, output, "ghcr.io/test-org/test-app:v1.2.3")
|
|
assert.Contains(t, output, "ghcr.io/test-org/test-app:stable")
|
|
|
|
// Verify build args shown
|
|
assert.Contains(t, output, "Build arguments:")
|
|
assert.Contains(t, output, "GO_VERSION=1.21")
|
|
|
|
// Verify command
|
|
assert.Contains(t, output, "docker buildx build")
|
|
assert.Contains(t, output, "END DRY RUN")
|
|
})
|
|
|
|
t.Run("dry run produces correct buildx command for multi-platform", func(t *testing.T) {
|
|
cfg := DockerConfig{
|
|
Registry: "ghcr.io",
|
|
Image: "org/app",
|
|
Dockerfile: "/project/Dockerfile",
|
|
Platforms: []string{"linux/amd64", "linux/arm64", "linux/arm/v7"},
|
|
Tags: []string{"latest", "{{.Version}}"},
|
|
BuildArgs: map[string]string{
|
|
"CUSTOM_ARG": "custom_value",
|
|
},
|
|
}
|
|
tags := p.resolveTags(cfg.Tags, "v3.0.0")
|
|
args := p.buildBuildxArgs(cfg, tags, "v3.0.0")
|
|
|
|
// Verify multi-platform string
|
|
foundPlatform := false
|
|
for i, arg := range args {
|
|
if arg == "--platform" && i+1 < len(args) {
|
|
foundPlatform = true
|
|
assert.Equal(t, "linux/amd64,linux/arm64,linux/arm/v7", args[i+1])
|
|
}
|
|
}
|
|
assert.True(t, foundPlatform, "should have --platform flag")
|
|
|
|
// Verify tags
|
|
assert.Contains(t, args, "ghcr.io/org/app:latest")
|
|
assert.Contains(t, args, "ghcr.io/org/app:v3.0.0")
|
|
|
|
// Verify build args
|
|
foundCustom := false
|
|
foundVersion := false
|
|
for i, arg := range args {
|
|
if arg == "--build-arg" && i+1 < len(args) {
|
|
if args[i+1] == "CUSTOM_ARG=custom_value" {
|
|
foundCustom = true
|
|
}
|
|
if args[i+1] == "VERSION=v3.0.0" {
|
|
foundVersion = true
|
|
}
|
|
}
|
|
}
|
|
assert.True(t, foundCustom, "CUSTOM_ARG build arg not found")
|
|
assert.True(t, foundVersion, "auto-added VERSION build arg not found")
|
|
|
|
// Verify push flag
|
|
assert.Contains(t, args, "--push")
|
|
})
|
|
}
|
|
|
|
func TestDockerPublisher_Integration_ConfigParsing_Good(t *testing.T) {
|
|
p := NewDockerPublisher()
|
|
|
|
t.Run("full config round-trip from PublisherConfig to DockerConfig", func(t *testing.T) {
|
|
pubCfg := PublisherConfig{
|
|
Type: "docker",
|
|
Extended: map[string]any{
|
|
"registry": "registry.example.com",
|
|
"image": "myteam/myservice",
|
|
"dockerfile": "deploy/Dockerfile.prod",
|
|
"platforms": []any{"linux/amd64"},
|
|
"tags": []any{"{{.Version}}", "latest", "release-{{.Version}}"},
|
|
"build_args": map[string]any{
|
|
"BUILD_ENV": "production",
|
|
"VERSION": "{{.Version}}",
|
|
},
|
|
},
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "fallback/repo"}
|
|
|
|
cfg := p.parseConfig(pubCfg, relCfg, "/myproject")
|
|
|
|
assert.Equal(t, "registry.example.com", cfg.Registry)
|
|
assert.Equal(t, "myteam/myservice", cfg.Image)
|
|
assert.Equal(t, "/myproject/deploy/Dockerfile.prod", cfg.Dockerfile)
|
|
assert.Equal(t, []string{"linux/amd64"}, cfg.Platforms)
|
|
assert.Equal(t, []string{"{{.Version}}", "latest", "release-{{.Version}}"}, cfg.Tags)
|
|
assert.Equal(t, "production", cfg.BuildArgs["BUILD_ENV"])
|
|
assert.Equal(t, "{{.Version}}", cfg.BuildArgs["VERSION"])
|
|
|
|
// Verify tag resolution
|
|
resolved := p.resolveTags(cfg.Tags, "v2.5.0")
|
|
assert.Equal(t, []string{"v2.5.0", "latest", "release-v2.5.0"}, resolved)
|
|
})
|
|
}
|
|
|
|
// --- Homebrew Publisher Integration Tests ---
|
|
|
|
func TestHomebrewPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|
p := NewHomebrewPublisher()
|
|
|
|
t.Run("dry run generates formula without writing files", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v2.1.0",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/dist/myapp-darwin-amd64.tar.gz", Checksum: "sha256_darwin_amd64"},
|
|
{Path: "/dist/myapp-darwin-arm64.tar.gz", Checksum: "sha256_darwin_arm64"},
|
|
{Path: "/dist/myapp-linux-amd64.tar.gz", Checksum: "sha256_linux_amd64"},
|
|
{Path: "/dist/myapp-linux-arm64.tar.gz", Checksum: "sha256_linux_arm64"},
|
|
},
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "homebrew",
|
|
Extended: map[string]any{
|
|
"tap": "test-org/homebrew-tap",
|
|
"formula": "my-cli",
|
|
},
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "test-org/my-cli", projectName: "my-cli"}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
// Verify dry run output
|
|
assert.Contains(t, output, "DRY RUN: Homebrew Publish")
|
|
assert.Contains(t, output, "Formula: MyCli")
|
|
assert.Contains(t, output, "Version: 2.1.0")
|
|
assert.Contains(t, output, "Tap: test-org/homebrew-tap")
|
|
assert.Contains(t, output, "Repository: test-org/my-cli")
|
|
|
|
// Verify generated formula content
|
|
assert.Contains(t, output, "class MyCli < Formula")
|
|
assert.Contains(t, output, `version "2.1.0"`)
|
|
assert.Contains(t, output, "sha256_darwin_amd64")
|
|
assert.Contains(t, output, "sha256_darwin_arm64")
|
|
assert.Contains(t, output, "sha256_linux_amd64")
|
|
assert.Contains(t, output, "sha256_linux_arm64")
|
|
|
|
assert.Contains(t, output, "Would commit to tap: test-org/homebrew-tap")
|
|
|
|
// Verify no files created
|
|
entries, err := os.ReadDir(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, entries, "dry run should not create any files")
|
|
})
|
|
|
|
t.Run("dry run with official config shows output path", func(t *testing.T) {
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
ProjectDir: "/project",
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{},
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "homebrew",
|
|
Extended: map[string]any{
|
|
"official": map[string]any{
|
|
"enabled": true,
|
|
"output": "dist/homebrew-official",
|
|
},
|
|
},
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "owner/repo", projectName: "repo"}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
assert.Contains(t, output, "Would write files for official PR to: dist/homebrew-official")
|
|
})
|
|
}
|
|
|
|
func TestHomebrewPublisher_Integration_FormulaGeneration_Good(t *testing.T) {
|
|
p := NewHomebrewPublisher()
|
|
|
|
t.Run("generated formula contains correct Ruby class structure", func(t *testing.T) {
|
|
data := homebrewTemplateData{
|
|
FormulaClass: "CoreCli",
|
|
Description: "Core CLI tool",
|
|
Repository: "host-uk/core-cli",
|
|
Version: "3.0.0",
|
|
License: "MIT",
|
|
BinaryName: "core",
|
|
Checksums: ChecksumMap{
|
|
DarwinAmd64: "a1b2c3d4e5f6",
|
|
DarwinArm64: "f6e5d4c3b2a1",
|
|
LinuxAmd64: "112233445566",
|
|
LinuxArm64: "665544332211",
|
|
},
|
|
}
|
|
|
|
formula, err := p.renderTemplate(io.Local, "templates/homebrew/formula.rb.tmpl", data)
|
|
require.NoError(t, err)
|
|
|
|
// Verify class definition
|
|
assert.Contains(t, formula, "class CoreCli < Formula")
|
|
|
|
// Verify metadata
|
|
assert.Contains(t, formula, `desc "Core CLI tool"`)
|
|
assert.Contains(t, formula, `version "3.0.0"`)
|
|
assert.Contains(t, formula, `license "MIT"`)
|
|
|
|
// Verify checksums for all platforms
|
|
assert.Contains(t, formula, "a1b2c3d4e5f6")
|
|
assert.Contains(t, formula, "f6e5d4c3b2a1")
|
|
assert.Contains(t, formula, "112233445566")
|
|
assert.Contains(t, formula, "665544332211")
|
|
|
|
// Verify binary install
|
|
assert.Contains(t, formula, `bin.install "core"`)
|
|
})
|
|
|
|
t.Run("toFormulaClass handles various naming patterns", func(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"my-app", "MyApp"},
|
|
{"core", "Core"},
|
|
{"go-devops", "GoDevops"},
|
|
{"a-b-c-d", "ABCD"},
|
|
{"single", "Single"},
|
|
{"UPPER", "UPPER"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.input, func(t *testing.T) {
|
|
result := toFormulaClass(tc.input)
|
|
assert.Equal(t, tc.expected, result)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- Scoop Publisher Integration Tests ---
|
|
|
|
func TestScoopPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|
p := NewScoopPublisher()
|
|
|
|
t.Run("dry run generates manifest without writing files", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v1.5.0",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/dist/myapp-windows-amd64.zip", Checksum: "win64hash"},
|
|
{Path: "/dist/myapp-windows-arm64.zip", Checksum: "winarm64hash"},
|
|
},
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "scoop",
|
|
Extended: map[string]any{
|
|
"bucket": "test-org/scoop-bucket",
|
|
},
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "test-org/myapp", projectName: "myapp"}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
assert.Contains(t, output, "DRY RUN: Scoop Publish")
|
|
assert.Contains(t, output, "Package: myapp")
|
|
assert.Contains(t, output, "Version: 1.5.0")
|
|
assert.Contains(t, output, "Bucket: test-org/scoop-bucket")
|
|
assert.Contains(t, output, "Generated manifest.json:")
|
|
assert.Contains(t, output, `"version": "1.5.0"`)
|
|
assert.Contains(t, output, "Would commit to bucket: test-org/scoop-bucket")
|
|
|
|
// Verify no files created
|
|
entries, err := os.ReadDir(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, entries)
|
|
})
|
|
}
|
|
|
|
// --- AUR Publisher Integration Tests ---
|
|
|
|
func TestAURPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|
p := NewAURPublisher()
|
|
|
|
t.Run("dry run generates PKGBUILD and SRCINFO without writing files", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v2.0.0",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/dist/myapp-linux-amd64.tar.gz", Checksum: "amd64hash"},
|
|
{Path: "/dist/myapp-linux-arm64.tar.gz", Checksum: "arm64hash"},
|
|
},
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "aur",
|
|
Extended: map[string]any{
|
|
"maintainer": "Test User <test@example.com>",
|
|
},
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "test-org/myapp", projectName: "myapp"}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
assert.Contains(t, output, "DRY RUN: AUR Publish")
|
|
assert.Contains(t, output, "Package: myapp-bin")
|
|
assert.Contains(t, output, "Version: 2.0.0")
|
|
assert.Contains(t, output, "Maintainer: Test User <test@example.com>")
|
|
assert.Contains(t, output, "Generated PKGBUILD:")
|
|
assert.Contains(t, output, "pkgname=myapp-bin")
|
|
assert.Contains(t, output, "pkgver=2.0.0")
|
|
assert.Contains(t, output, "Generated .SRCINFO:")
|
|
assert.Contains(t, output, "pkgbase = myapp-bin")
|
|
assert.Contains(t, output, "Would push to AUR:")
|
|
|
|
// Verify no files created
|
|
entries, err := os.ReadDir(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, entries)
|
|
})
|
|
}
|
|
|
|
// --- Chocolatey Publisher Integration Tests ---
|
|
|
|
func TestChocolateyPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|
p := NewChocolateyPublisher()
|
|
|
|
t.Run("dry run generates nuspec and install script without side effects", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/dist/myapp-windows-amd64.zip", Checksum: "choco_hash"},
|
|
},
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "chocolatey",
|
|
Extended: map[string]any{
|
|
"package": "my-cli-tool",
|
|
"push": false,
|
|
},
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "owner/my-cli-tool", projectName: "my-cli-tool"}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
assert.Contains(t, output, "DRY RUN: Chocolatey Publish")
|
|
assert.Contains(t, output, "Package: my-cli-tool")
|
|
assert.Contains(t, output, "Version: 1.0.0")
|
|
assert.Contains(t, output, "Push: false")
|
|
assert.Contains(t, output, "Generated package.nuspec:")
|
|
assert.Contains(t, output, "<id>my-cli-tool</id>")
|
|
assert.Contains(t, output, "Generated chocolateyinstall.ps1:")
|
|
assert.Contains(t, output, "Would generate package files only")
|
|
|
|
// Verify no files created
|
|
entries, err := os.ReadDir(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, entries)
|
|
})
|
|
}
|
|
|
|
// --- npm Publisher Integration Tests ---
|
|
|
|
func TestNpmPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|
p := NewNpmPublisher()
|
|
|
|
t.Run("dry run generates package.json without writing files or publishing", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v3.0.0",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "npm",
|
|
Extended: map[string]any{
|
|
"package": "@test-org/my-cli",
|
|
"access": "public",
|
|
},
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "test-org/my-cli", projectName: "my-cli"}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
assert.Contains(t, output, "DRY RUN: npm Publish")
|
|
assert.Contains(t, output, "Package: @test-org/my-cli")
|
|
assert.Contains(t, output, "Version: 3.0.0")
|
|
assert.Contains(t, output, "Access: public")
|
|
assert.Contains(t, output, "Generated package.json:")
|
|
assert.Contains(t, output, `"name": "@test-org/my-cli"`)
|
|
assert.Contains(t, output, `"version": "3.0.0"`)
|
|
assert.Contains(t, output, "Would run: npm publish --access public")
|
|
|
|
// Verify no files created
|
|
entries, err := os.ReadDir(tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, entries)
|
|
})
|
|
}
|
|
|
|
// --- LinuxKit Publisher Integration Tests ---
|
|
|
|
func TestLinuxKitPublisher_Integration_DryRunNoSideEffects_Good(t *testing.T) {
|
|
if err := validateLinuxKitCli(); err != nil {
|
|
t.Skip("skipping: linuxkit CLI not available")
|
|
}
|
|
|
|
p := NewLinuxKitPublisher()
|
|
|
|
t.Run("dry run with multiple formats and platforms", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create config file
|
|
configDir := filepath.Join(tmpDir, ".core", "linuxkit")
|
|
require.NoError(t, os.MkdirAll(configDir, 0755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(configDir, "server.yml"), []byte("kernel:\n image: test\n"), 0644))
|
|
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "linuxkit",
|
|
Extended: map[string]any{
|
|
"formats": []any{"iso", "qcow2", "docker"},
|
|
"platforms": []any{"linux/amd64", "linux/arm64"},
|
|
},
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "test-org/my-os"}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
output := buf.String()
|
|
|
|
assert.Contains(t, output, "DRY RUN: LinuxKit Build & Publish")
|
|
assert.Contains(t, output, "Formats: iso, qcow2, docker")
|
|
assert.Contains(t, output, "Platforms: linux/amd64, linux/arm64")
|
|
|
|
// Verify all combinations listed
|
|
assert.Contains(t, output, "linuxkit-1.0.0-amd64.iso")
|
|
assert.Contains(t, output, "linuxkit-1.0.0-amd64.qcow2")
|
|
assert.Contains(t, output, "linuxkit-1.0.0-amd64.docker.tar")
|
|
assert.Contains(t, output, "linuxkit-1.0.0-arm64.iso")
|
|
assert.Contains(t, output, "linuxkit-1.0.0-arm64.qcow2")
|
|
assert.Contains(t, output, "linuxkit-1.0.0-arm64.docker.tar")
|
|
|
|
// Verify docker usage hint
|
|
assert.Contains(t, output, "docker load")
|
|
|
|
// Verify no files created in dist
|
|
distDir := filepath.Join(tmpDir, "dist")
|
|
_, err = os.Stat(distDir)
|
|
assert.True(t, os.IsNotExist(err), "dry run should not create dist directory")
|
|
})
|
|
}
|
|
|
|
// --- Cross-Publisher Integration Tests ---
|
|
|
|
func TestAllPublishers_Integration_NameUniqueness_Good(t *testing.T) {
|
|
t.Run("all publishers have unique names", func(t *testing.T) {
|
|
publishers := []Publisher{
|
|
NewGitHubPublisher(),
|
|
NewDockerPublisher(),
|
|
NewHomebrewPublisher(),
|
|
NewNpmPublisher(),
|
|
NewScoopPublisher(),
|
|
NewAURPublisher(),
|
|
NewChocolateyPublisher(),
|
|
NewLinuxKitPublisher(),
|
|
}
|
|
|
|
names := make(map[string]bool)
|
|
for _, pub := range publishers {
|
|
name := pub.Name()
|
|
assert.False(t, names[name], "duplicate publisher name: %s", name)
|
|
names[name] = true
|
|
assert.NotEmpty(t, name, "publisher name should not be empty")
|
|
}
|
|
|
|
assert.Len(t, names, 8, "should have 8 unique publishers")
|
|
})
|
|
}
|
|
|
|
func TestAllPublishers_Integration_NilRelCfg_Good(t *testing.T) {
|
|
t.Run("github handles nil relCfg with git repo", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
cmd := exec.Command("git", "init")
|
|
cmd.Dir = tmpDir
|
|
require.NoError(t, cmd.Run())
|
|
|
|
cmd = exec.Command("git", "remote", "add", "origin", "git@github.com:niltest/repo.git")
|
|
cmd.Dir = tmpDir
|
|
require.NoError(t, cmd.Run())
|
|
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
|
|
err := NewGitHubPublisher().Publish(context.Background(), release, pubCfg, nil, true)
|
|
|
|
_ = w.Close()
|
|
var buf bytes.Buffer
|
|
_, _ = buf.ReadFrom(r)
|
|
os.Stdout = oldStdout
|
|
|
|
require.NoError(t, err)
|
|
assert.Contains(t, buf.String(), "niltest/repo")
|
|
})
|
|
}
|
|
|
|
func TestBuildChecksumMap_Integration_Good(t *testing.T) {
|
|
t.Run("maps all platforms correctly from realistic artifacts", func(t *testing.T) {
|
|
artifacts := []build.Artifact{
|
|
{Path: "/dist/core-v1.0.0-darwin-amd64.tar.gz", Checksum: "da64"},
|
|
{Path: "/dist/core-v1.0.0-darwin-arm64.tar.gz", Checksum: "da65"},
|
|
{Path: "/dist/core-v1.0.0-linux-amd64.tar.gz", Checksum: "la64"},
|
|
{Path: "/dist/core-v1.0.0-linux-arm64.tar.gz", Checksum: "la65"},
|
|
{Path: "/dist/core-v1.0.0-windows-amd64.zip", Checksum: "wa64"},
|
|
{Path: "/dist/core-v1.0.0-windows-arm64.zip", Checksum: "wa65"},
|
|
{Path: "/dist/CHECKSUMS.txt"}, // No checksum for checksum file
|
|
}
|
|
|
|
checksums := buildChecksumMap(artifacts)
|
|
|
|
assert.Equal(t, "da64", checksums.DarwinAmd64)
|
|
assert.Equal(t, "da65", checksums.DarwinArm64)
|
|
assert.Equal(t, "la64", checksums.LinuxAmd64)
|
|
assert.Equal(t, "la65", checksums.LinuxArm64)
|
|
assert.Equal(t, "wa64", checksums.WindowsAmd64)
|
|
assert.Equal(t, "wa65", checksums.WindowsArm64)
|
|
})
|
|
}
|
|
|
|
// indexOf returns the index of an element in a string slice, or -1 if not found.
|
|
func indexOf(slice []string, item string) int {
|
|
for i, s := range slice {
|
|
if s == item {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// Compile-time check: all publishers implement Publisher interface
|
|
var _ Publisher = (*GitHubPublisher)(nil)
|
|
var _ Publisher = (*DockerPublisher)(nil)
|
|
var _ Publisher = (*HomebrewPublisher)(nil)
|
|
var _ Publisher = (*NpmPublisher)(nil)
|
|
var _ Publisher = (*ScoopPublisher)(nil)
|
|
var _ Publisher = (*AURPublisher)(nil)
|
|
var _ Publisher = (*ChocolateyPublisher)(nil)
|
|
var _ Publisher = (*LinuxKitPublisher)(nil)
|