test: increase coverage across packages
- pkg/container: 65.6% → 85.7% (hypervisor, linuxkit, templates tests) - pkg/release/publishers: 13.3% → 41.7% (homebrew, aur, npm, scoop, chocolatey tests) - Fix flaky test cleanup in TestLinuxKitManager_Stop_Good_ContextCancelled Overall coverage: 29.2% → 40.6% Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a7ee58d29e
commit
50f6839c51
8 changed files with 2386 additions and 32 deletions
358
pkg/container/hypervisor_test.go
Normal file
358
pkg/container/hypervisor_test.go
Normal file
|
|
@ -0,0 +1,358 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQemuHypervisor_Available_Good(t *testing.T) {
|
||||||
|
q := NewQemuHypervisor()
|
||||||
|
|
||||||
|
// Check if qemu is available on this system
|
||||||
|
available := q.Available()
|
||||||
|
|
||||||
|
// We just verify it returns a boolean without error
|
||||||
|
// The actual availability depends on the system
|
||||||
|
assert.IsType(t, true, available)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQemuHypervisor_Available_Bad_InvalidBinary(t *testing.T) {
|
||||||
|
q := &QemuHypervisor{
|
||||||
|
Binary: "nonexistent-qemu-binary-that-does-not-exist",
|
||||||
|
}
|
||||||
|
|
||||||
|
available := q.Available()
|
||||||
|
|
||||||
|
assert.False(t, available)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyperkitHypervisor_Available_Good(t *testing.T) {
|
||||||
|
h := NewHyperkitHypervisor()
|
||||||
|
|
||||||
|
available := h.Available()
|
||||||
|
|
||||||
|
// On non-darwin systems, should always be false
|
||||||
|
if runtime.GOOS != "darwin" {
|
||||||
|
assert.False(t, available)
|
||||||
|
} else {
|
||||||
|
// On darwin, just verify it returns a boolean
|
||||||
|
assert.IsType(t, true, available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyperkitHypervisor_Available_Bad_NotDarwin(t *testing.T) {
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
t.Skip("This test only runs on non-darwin systems")
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewHyperkitHypervisor()
|
||||||
|
|
||||||
|
available := h.Available()
|
||||||
|
|
||||||
|
assert.False(t, available, "Hyperkit should not be available on non-darwin systems")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyperkitHypervisor_Available_Bad_InvalidBinary(t *testing.T) {
|
||||||
|
h := &HyperkitHypervisor{
|
||||||
|
Binary: "nonexistent-hyperkit-binary-that-does-not-exist",
|
||||||
|
}
|
||||||
|
|
||||||
|
available := h.Available()
|
||||||
|
|
||||||
|
assert.False(t, available)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsKVMAvailable_Good(t *testing.T) {
|
||||||
|
// This test verifies the function runs without error
|
||||||
|
// The actual result depends on the system
|
||||||
|
result := isKVMAvailable()
|
||||||
|
|
||||||
|
// On non-linux systems, should be false
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
assert.False(t, result, "KVM should not be available on non-linux systems")
|
||||||
|
} else {
|
||||||
|
// On linux, just verify it returns a boolean
|
||||||
|
assert.IsType(t, true, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectHypervisor_Good(t *testing.T) {
|
||||||
|
// DetectHypervisor tries to find an available hypervisor
|
||||||
|
hv, err := DetectHypervisor()
|
||||||
|
|
||||||
|
// This test may pass or fail depending on system configuration
|
||||||
|
// If no hypervisor is available, it should return an error
|
||||||
|
if err != nil {
|
||||||
|
assert.Nil(t, hv)
|
||||||
|
assert.Contains(t, err.Error(), "no hypervisor available")
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, hv)
|
||||||
|
assert.NotEmpty(t, hv.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHypervisor_Good_Qemu(t *testing.T) {
|
||||||
|
hv, err := GetHypervisor("qemu")
|
||||||
|
|
||||||
|
// Depends on whether qemu is installed
|
||||||
|
if err != nil {
|
||||||
|
assert.Contains(t, err.Error(), "not available")
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, hv)
|
||||||
|
assert.Equal(t, "qemu", hv.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHypervisor_Good_QemuUppercase(t *testing.T) {
|
||||||
|
hv, err := GetHypervisor("QEMU")
|
||||||
|
|
||||||
|
// Depends on whether qemu is installed
|
||||||
|
if err != nil {
|
||||||
|
assert.Contains(t, err.Error(), "not available")
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, hv)
|
||||||
|
assert.Equal(t, "qemu", hv.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHypervisor_Good_Hyperkit(t *testing.T) {
|
||||||
|
hv, err := GetHypervisor("hyperkit")
|
||||||
|
|
||||||
|
// On non-darwin systems, should always fail
|
||||||
|
if runtime.GOOS != "darwin" {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not available")
|
||||||
|
} else {
|
||||||
|
// On darwin, depends on whether hyperkit is installed
|
||||||
|
if err != nil {
|
||||||
|
assert.Contains(t, err.Error(), "not available")
|
||||||
|
} else {
|
||||||
|
assert.NotNil(t, hv)
|
||||||
|
assert.Equal(t, "hyperkit", hv.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetHypervisor_Bad_Unknown(t *testing.T) {
|
||||||
|
_, err := GetHypervisor("unknown-hypervisor")
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unknown hypervisor")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQemuHypervisor_BuildCommand_Good_WithPortsAndVolumes(t *testing.T) {
|
||||||
|
q := NewQemuHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{
|
||||||
|
Memory: 2048,
|
||||||
|
CPUs: 4,
|
||||||
|
SSHPort: 2222,
|
||||||
|
Ports: map[int]int{8080: 80, 443: 443},
|
||||||
|
Volumes: map[string]string{
|
||||||
|
"/host/data": "/container/data",
|
||||||
|
"/host/logs": "/container/logs",
|
||||||
|
},
|
||||||
|
Detach: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, err := q.BuildCommand(ctx, "/path/to/image.iso", opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, cmd)
|
||||||
|
|
||||||
|
// Verify command includes all expected args
|
||||||
|
args := cmd.Args
|
||||||
|
assert.Contains(t, args, "-m")
|
||||||
|
assert.Contains(t, args, "2048")
|
||||||
|
assert.Contains(t, args, "-smp")
|
||||||
|
assert.Contains(t, args, "4")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQemuHypervisor_BuildCommand_Good_QCow2Format(t *testing.T) {
|
||||||
|
q := NewQemuHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{Memory: 1024, CPUs: 1}
|
||||||
|
|
||||||
|
cmd, err := q.BuildCommand(ctx, "/path/to/image.qcow2", opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the drive format is qcow2
|
||||||
|
found := false
|
||||||
|
for _, arg := range cmd.Args {
|
||||||
|
if arg == "file=/path/to/image.qcow2,format=qcow2" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "Should have qcow2 drive argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQemuHypervisor_BuildCommand_Good_VMDKFormat(t *testing.T) {
|
||||||
|
q := NewQemuHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{Memory: 1024, CPUs: 1}
|
||||||
|
|
||||||
|
cmd, err := q.BuildCommand(ctx, "/path/to/image.vmdk", opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the drive format is vmdk
|
||||||
|
found := false
|
||||||
|
for _, arg := range cmd.Args {
|
||||||
|
if arg == "file=/path/to/image.vmdk,format=vmdk" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "Should have vmdk drive argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQemuHypervisor_BuildCommand_Good_RawFormat(t *testing.T) {
|
||||||
|
q := NewQemuHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{Memory: 1024, CPUs: 1}
|
||||||
|
|
||||||
|
cmd, err := q.BuildCommand(ctx, "/path/to/image.raw", opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that the drive format is raw
|
||||||
|
found := false
|
||||||
|
for _, arg := range cmd.Args {
|
||||||
|
if arg == "file=/path/to/image.raw,format=raw" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "Should have raw drive argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyperkitHypervisor_BuildCommand_Good_WithPorts(t *testing.T) {
|
||||||
|
h := NewHyperkitHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{
|
||||||
|
Memory: 1024,
|
||||||
|
CPUs: 2,
|
||||||
|
SSHPort: 2222,
|
||||||
|
Ports: map[int]int{8080: 80},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, err := h.BuildCommand(ctx, "/path/to/image.iso", opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, cmd)
|
||||||
|
|
||||||
|
// Verify it creates a command with memory and CPU args
|
||||||
|
args := cmd.Args
|
||||||
|
assert.Contains(t, args, "-m")
|
||||||
|
assert.Contains(t, args, "1024M")
|
||||||
|
assert.Contains(t, args, "-c")
|
||||||
|
assert.Contains(t, args, "2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyperkitHypervisor_BuildCommand_Good_QCow2Format(t *testing.T) {
|
||||||
|
h := NewHyperkitHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{Memory: 1024, CPUs: 1}
|
||||||
|
|
||||||
|
cmd, err := h.BuildCommand(ctx, "/path/to/image.qcow2", opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyperkitHypervisor_BuildCommand_Good_RawFormat(t *testing.T) {
|
||||||
|
h := NewHyperkitHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{Memory: 1024, CPUs: 1}
|
||||||
|
|
||||||
|
cmd, err := h.BuildCommand(ctx, "/path/to/image.raw", opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyperkitHypervisor_BuildCommand_Good_NoPorts(t *testing.T) {
|
||||||
|
h := NewHyperkitHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{
|
||||||
|
Memory: 512,
|
||||||
|
CPUs: 1,
|
||||||
|
SSHPort: 0, // No SSH port
|
||||||
|
Ports: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, err := h.BuildCommand(ctx, "/path/to/image.iso", opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQemuHypervisor_BuildCommand_Good_NoSSHPort(t *testing.T) {
|
||||||
|
q := NewQemuHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{
|
||||||
|
Memory: 512,
|
||||||
|
CPUs: 1,
|
||||||
|
SSHPort: 0, // No SSH port
|
||||||
|
Ports: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, err := q.BuildCommand(ctx, "/path/to/image.iso", opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQemuHypervisor_BuildCommand_Bad_UnknownFormat(t *testing.T) {
|
||||||
|
q := NewQemuHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{Memory: 1024, CPUs: 1}
|
||||||
|
|
||||||
|
_, err := q.BuildCommand(ctx, "/path/to/image.txt", opts)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unknown image format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyperkitHypervisor_BuildCommand_Bad_UnknownFormat(t *testing.T) {
|
||||||
|
h := NewHyperkitHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{Memory: 1024, CPUs: 1}
|
||||||
|
|
||||||
|
_, err := h.BuildCommand(ctx, "/path/to/image.unknown", opts)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "unknown image format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyperkitHypervisor_Name_Good(t *testing.T) {
|
||||||
|
h := NewHyperkitHypervisor()
|
||||||
|
assert.Equal(t, "hyperkit", h.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHyperkitHypervisor_BuildCommand_Good_ISOFormat(t *testing.T) {
|
||||||
|
h := NewHyperkitHypervisor()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := &HypervisorOptions{
|
||||||
|
Memory: 1024,
|
||||||
|
CPUs: 2,
|
||||||
|
SSHPort: 2222,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, err := h.BuildCommand(ctx, "/path/to/image.iso", opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, cmd)
|
||||||
|
|
||||||
|
args := cmd.Args
|
||||||
|
assert.Contains(t, args, "-m")
|
||||||
|
assert.Contains(t, args, "1024M")
|
||||||
|
assert.Contains(t, args, "-c")
|
||||||
|
assert.Contains(t, args, "2")
|
||||||
|
}
|
||||||
|
|
@ -410,56 +410,362 @@ func TestQemuHypervisor_BuildCommand_Good(t *testing.T) {
|
||||||
assert.Contains(t, args, "-nographic")
|
assert.Contains(t, args, "-nographic")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQemuHypervisor_BuildCommand_Bad_UnknownFormat(t *testing.T) {
|
|
||||||
q := NewQemuHypervisor()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
func TestLinuxKitManager_Logs_Good_Follow(t *testing.T) {
|
||||||
opts := &HypervisorOptions{Memory: 1024, CPUs: 1}
|
manager, _, _ := newTestManager(t)
|
||||||
|
|
||||||
_, err := q.BuildCommand(ctx, "/path/to/image.txt", opts)
|
// Create a unique container ID
|
||||||
assert.Error(t, err)
|
uniqueID, _ := GenerateID()
|
||||||
assert.Contains(t, err.Error(), "unknown image format")
|
container := &Container{ID: uniqueID}
|
||||||
}
|
manager.State().Add(container)
|
||||||
|
|
||||||
func TestHyperkitHypervisor_Name_Good(t *testing.T) {
|
// Create a log file at the expected location
|
||||||
h := NewHyperkitHypervisor()
|
logPath, err := LogPath(uniqueID)
|
||||||
assert.Equal(t, "hyperkit", h.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHyperkitHypervisor_BuildCommand_Good(t *testing.T) {
|
|
||||||
h := NewHyperkitHypervisor()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
opts := &HypervisorOptions{
|
|
||||||
Memory: 1024,
|
|
||||||
CPUs: 2,
|
|
||||||
SSHPort: 2222,
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd, err := h.BuildCommand(ctx, "/path/to/image.iso", opts)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, cmd)
|
os.MkdirAll(filepath.Dir(logPath), 0755)
|
||||||
|
|
||||||
args := cmd.Args
|
// Write initial content
|
||||||
assert.Contains(t, args, "-m")
|
err = os.WriteFile(logPath, []byte("initial log content\n"), 0644)
|
||||||
assert.Contains(t, args, "1024M")
|
require.NoError(t, err)
|
||||||
assert.Contains(t, args, "-c")
|
|
||||||
assert.Contains(t, args, "2")
|
// Create a cancellable context
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// Get the follow reader
|
||||||
|
reader, err := manager.Logs(ctx, uniqueID, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Cancel the context to stop the follow
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Read should return EOF after context cancellation
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
_, readErr := reader.Read(buf)
|
||||||
|
// After context cancel, Read should return EOF
|
||||||
|
assert.Equal(t, "EOF", readErr.Error())
|
||||||
|
|
||||||
|
// Close the reader
|
||||||
|
err = reader.Close()
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHyperkitHypervisor_BuildCommand_Bad_UnknownFormat(t *testing.T) {
|
func TestFollowReader_Read_Good_WithData(t *testing.T) {
|
||||||
h := NewHyperkitHypervisor()
|
tmpDir := t.TempDir()
|
||||||
|
logPath := filepath.Join(tmpDir, "test.log")
|
||||||
|
|
||||||
|
// Create log file with content
|
||||||
|
content := "test log line 1\ntest log line 2\n"
|
||||||
|
err := os.WriteFile(logPath, []byte(content), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
reader, err := newFollowReader(ctx, logPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
// The followReader seeks to end, so we need to append more content
|
||||||
|
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = f.WriteString("new line\n")
|
||||||
|
require.NoError(t, err)
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
// Give the reader time to poll
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
n, err := reader.Read(buf)
|
||||||
|
if err == nil {
|
||||||
|
assert.Greater(t, n, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFollowReader_Read_Good_ContextCancel(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
logPath := filepath.Join(tmpDir, "test.log")
|
||||||
|
|
||||||
|
// Create log file
|
||||||
|
err := os.WriteFile(logPath, []byte("initial content\n"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
reader, err := newFollowReader(ctx, logPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Cancel the context
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Read should return EOF
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
_, readErr := reader.Read(buf)
|
||||||
|
assert.Equal(t, "EOF", readErr.Error())
|
||||||
|
|
||||||
|
reader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFollowReader_Close_Good(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
logPath := filepath.Join(tmpDir, "test.log")
|
||||||
|
|
||||||
|
err := os.WriteFile(logPath, []byte("content\n"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
opts := &HypervisorOptions{Memory: 1024, CPUs: 1}
|
reader, err := newFollowReader(ctx, logPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
_, err := h.BuildCommand(ctx, "/path/to/image.unknown", opts)
|
err = reader.Close()
|
||||||
assert.Error(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, err.Error(), "unknown image format")
|
|
||||||
|
// Reading after close should fail or return EOF
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
_, readErr := reader.Read(buf)
|
||||||
|
assert.Error(t, readErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetHypervisor_Bad_Unknown(t *testing.T) {
|
func TestNewFollowReader_Bad_FileNotFound(t *testing.T) {
|
||||||
_, err := GetHypervisor("unknown-hypervisor")
|
ctx := context.Background()
|
||||||
|
_, err := newFollowReader(ctx, "/nonexistent/path/to/file.log")
|
||||||
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "unknown hypervisor")
|
}
|
||||||
|
|
||||||
|
func TestLinuxKitManager_Run_Bad_BuildCommandError(t *testing.T) {
|
||||||
|
manager, mock, tmpDir := newTestManager(t)
|
||||||
|
|
||||||
|
// Create a test image file
|
||||||
|
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||||
|
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Configure mock to return an error
|
||||||
|
mock.buildErr = assert.AnError
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := RunOptions{Detach: true}
|
||||||
|
|
||||||
|
_, err = manager.Run(ctx, imagePath, opts)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to build hypervisor command")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinuxKitManager_Run_Good_Foreground(t *testing.T) {
|
||||||
|
manager, mock, tmpDir := newTestManager(t)
|
||||||
|
|
||||||
|
// Create a test image file
|
||||||
|
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||||
|
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Use echo which exits quickly
|
||||||
|
mock.commandToRun = "echo"
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := RunOptions{
|
||||||
|
Name: "test-foreground",
|
||||||
|
Detach: false, // Run in foreground
|
||||||
|
Memory: 512,
|
||||||
|
CPUs: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
container, err := manager.Run(ctx, imagePath, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, container.ID)
|
||||||
|
assert.Equal(t, "test-foreground", container.Name)
|
||||||
|
// Foreground process should have completed
|
||||||
|
assert.Equal(t, StatusStopped, container.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinuxKitManager_Stop_Good_ContextCancelled(t *testing.T) {
|
||||||
|
manager, mock, tmpDir := newTestManager(t)
|
||||||
|
|
||||||
|
// Create a test image file
|
||||||
|
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||||
|
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Use a command that takes a long time
|
||||||
|
mock.commandToRun = "sleep"
|
||||||
|
|
||||||
|
// Start a container
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := RunOptions{
|
||||||
|
Name: "test-cancel",
|
||||||
|
Detach: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
container, err := manager.Run(ctx, imagePath, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Ensure cleanup happens regardless of test outcome
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = manager.Stop(context.Background(), container.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a context that's already cancelled
|
||||||
|
cancelCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Stop with cancelled context
|
||||||
|
err = manager.Stop(cancelCtx, container.ID)
|
||||||
|
// Should return context error
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, context.Canceled, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsProcessRunning_Good_ExistingProcess(t *testing.T) {
|
||||||
|
// Use our own PID which definitely exists
|
||||||
|
running := isProcessRunning(os.Getpid())
|
||||||
|
assert.True(t, running)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsProcessRunning_Bad_NonexistentProcess(t *testing.T) {
|
||||||
|
// Use a PID that almost certainly doesn't exist
|
||||||
|
running := isProcessRunning(999999)
|
||||||
|
assert.False(t, running)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinuxKitManager_Run_Good_WithPortsAndVolumes(t *testing.T) {
|
||||||
|
manager, mock, tmpDir := newTestManager(t)
|
||||||
|
|
||||||
|
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||||
|
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := RunOptions{
|
||||||
|
Name: "test-ports",
|
||||||
|
Detach: true,
|
||||||
|
Memory: 512,
|
||||||
|
CPUs: 1,
|
||||||
|
SSHPort: 2223,
|
||||||
|
Ports: map[int]int{8080: 80, 443: 443},
|
||||||
|
Volumes: map[string]string{"/host/data": "/container/data"},
|
||||||
|
}
|
||||||
|
|
||||||
|
container, err := manager.Run(ctx, imagePath, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEmpty(t, container.ID)
|
||||||
|
assert.Equal(t, map[int]int{8080: 80, 443: 443}, container.Ports)
|
||||||
|
assert.Equal(t, 2223, mock.lastOpts.SSHPort)
|
||||||
|
assert.Equal(t, map[string]string{"/host/data": "/container/data"}, mock.lastOpts.Volumes)
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFollowReader_Read_Good_ReaderError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
logPath := filepath.Join(tmpDir, "test.log")
|
||||||
|
|
||||||
|
// Create log file
|
||||||
|
err := os.WriteFile(logPath, []byte("content\n"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
reader, err := newFollowReader(ctx, logPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Close the underlying file to cause read errors
|
||||||
|
reader.file.Close()
|
||||||
|
|
||||||
|
// Read should return an error
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
_, readErr := reader.Read(buf)
|
||||||
|
assert.Error(t, readErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinuxKitManager_Run_Bad_StartError(t *testing.T) {
|
||||||
|
manager, mock, tmpDir := newTestManager(t)
|
||||||
|
|
||||||
|
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||||
|
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Use a command that doesn't exist to cause Start() to fail
|
||||||
|
mock.commandToRun = "/nonexistent/command/that/does/not/exist"
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := RunOptions{
|
||||||
|
Name: "test-start-error",
|
||||||
|
Detach: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = manager.Run(ctx, imagePath, opts)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to start VM")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinuxKitManager_Run_Bad_ForegroundStartError(t *testing.T) {
|
||||||
|
manager, mock, tmpDir := newTestManager(t)
|
||||||
|
|
||||||
|
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||||
|
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Use a command that doesn't exist to cause Start() to fail
|
||||||
|
mock.commandToRun = "/nonexistent/command/that/does/not/exist"
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := RunOptions{
|
||||||
|
Name: "test-foreground-error",
|
||||||
|
Detach: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = manager.Run(ctx, imagePath, opts)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to start VM")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinuxKitManager_Run_Good_ForegroundWithError(t *testing.T) {
|
||||||
|
manager, mock, tmpDir := newTestManager(t)
|
||||||
|
|
||||||
|
imagePath := filepath.Join(tmpDir, "test.iso")
|
||||||
|
err := os.WriteFile(imagePath, []byte("fake image"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Use a command that exits with error
|
||||||
|
mock.commandToRun = "false" // false command exits with code 1
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
opts := RunOptions{
|
||||||
|
Name: "test-foreground-exit-error",
|
||||||
|
Detach: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
container, err := manager.Run(ctx, imagePath, opts)
|
||||||
|
require.NoError(t, err) // Run itself should succeed
|
||||||
|
|
||||||
|
// Container should be in error state since process exited with error
|
||||||
|
assert.Equal(t, StatusError, container.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinuxKitManager_Stop_Good_ProcessExitedWhileRunning(t *testing.T) {
|
||||||
|
manager, _, _ := newTestManager(t)
|
||||||
|
|
||||||
|
// Add a "running" container with a process that has already exited
|
||||||
|
// This simulates the race condition where process exits between status check
|
||||||
|
// and signal send
|
||||||
|
container := &Container{
|
||||||
|
ID: "test1234",
|
||||||
|
Status: StatusRunning,
|
||||||
|
PID: 999999, // Non-existent PID
|
||||||
|
StartedAt: time.Now(),
|
||||||
|
}
|
||||||
|
manager.State().Add(container)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
err := manager.Stop(ctx, "test1234")
|
||||||
|
|
||||||
|
// Stop should succeed gracefully
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Container should be stopped
|
||||||
|
c, ok := manager.State().Get("test1234")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, StatusStopped, c.Status)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -383,3 +383,201 @@ func TestVariablePatternEdgeCases_Good(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListTemplates_Good_WithUserTemplates(t *testing.T) {
|
||||||
|
// Create a workspace directory with user templates
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
coreDir := filepath.Join(tmpDir, ".core", "linuxkit")
|
||||||
|
err := os.MkdirAll(coreDir, 0755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a user template
|
||||||
|
templateContent := `# Custom user template
|
||||||
|
kernel:
|
||||||
|
image: linuxkit/kernel:6.6
|
||||||
|
`
|
||||||
|
err = os.WriteFile(filepath.Join(coreDir, "user-custom.yml"), []byte(templateContent), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Change to the temp directory
|
||||||
|
oldWd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.Chdir(tmpDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.Chdir(oldWd)
|
||||||
|
|
||||||
|
templates := ListTemplates()
|
||||||
|
|
||||||
|
// Should have at least the builtin templates plus the user template
|
||||||
|
assert.GreaterOrEqual(t, len(templates), 3)
|
||||||
|
|
||||||
|
// Check that user template is included
|
||||||
|
found := false
|
||||||
|
for _, tmpl := range templates {
|
||||||
|
if tmpl.Name == "user-custom" {
|
||||||
|
found = true
|
||||||
|
assert.Equal(t, "Custom user template", tmpl.Description)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "user-custom template should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTemplate_Good_UserTemplate(t *testing.T) {
|
||||||
|
// Create a workspace directory with user templates
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
coreDir := filepath.Join(tmpDir, ".core", "linuxkit")
|
||||||
|
err := os.MkdirAll(coreDir, 0755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a user template
|
||||||
|
templateContent := `# My user template
|
||||||
|
kernel:
|
||||||
|
image: linuxkit/kernel:6.6
|
||||||
|
services:
|
||||||
|
- name: test
|
||||||
|
`
|
||||||
|
err = os.WriteFile(filepath.Join(coreDir, "my-user-template.yml"), []byte(templateContent), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Change to the temp directory
|
||||||
|
oldWd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.Chdir(tmpDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.Chdir(oldWd)
|
||||||
|
|
||||||
|
content, err := GetTemplate("my-user-template")
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, content, "kernel:")
|
||||||
|
assert.Contains(t, content, "My user template")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanUserTemplates_Good_SkipsBuiltinNames(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a template with a builtin name (should be skipped)
|
||||||
|
err := os.WriteFile(filepath.Join(tmpDir, "core-dev.yml"), []byte("# Duplicate\nkernel:"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a unique template
|
||||||
|
err = os.WriteFile(filepath.Join(tmpDir, "unique.yml"), []byte("# Unique\nkernel:"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
templates := scanUserTemplates(tmpDir)
|
||||||
|
|
||||||
|
// Should only have the unique template, not the builtin name
|
||||||
|
assert.Len(t, templates, 1)
|
||||||
|
assert.Equal(t, "unique", templates[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanUserTemplates_Good_SkipsDirectories(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a subdirectory (should be skipped)
|
||||||
|
err := os.MkdirAll(filepath.Join(tmpDir, "subdir"), 0755)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a valid template
|
||||||
|
err = os.WriteFile(filepath.Join(tmpDir, "valid.yml"), []byte("# Valid\nkernel:"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
templates := scanUserTemplates(tmpDir)
|
||||||
|
|
||||||
|
assert.Len(t, templates, 1)
|
||||||
|
assert.Equal(t, "valid", templates[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanUserTemplates_Good_YamlExtension(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create templates with both extensions
|
||||||
|
err := os.WriteFile(filepath.Join(tmpDir, "template1.yml"), []byte("# Template 1\nkernel:"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.WriteFile(filepath.Join(tmpDir, "template2.yaml"), []byte("# Template 2\nkernel:"), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
templates := scanUserTemplates(tmpDir)
|
||||||
|
|
||||||
|
assert.Len(t, templates, 2)
|
||||||
|
|
||||||
|
names := make(map[string]bool)
|
||||||
|
for _, tmpl := range templates {
|
||||||
|
names[tmpl.Name] = true
|
||||||
|
}
|
||||||
|
assert.True(t, names["template1"])
|
||||||
|
assert.True(t, names["template2"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTemplateDescription_Good_EmptyComment(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
path := filepath.Join(tmpDir, "test.yml")
|
||||||
|
|
||||||
|
// First comment is empty, second has content
|
||||||
|
content := `#
|
||||||
|
# Actual description here
|
||||||
|
kernel:
|
||||||
|
image: test
|
||||||
|
`
|
||||||
|
err := os.WriteFile(path, []byte(content), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
desc := extractTemplateDescription(path)
|
||||||
|
|
||||||
|
assert.Equal(t, "Actual description here", desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractTemplateDescription_Good_MultipleEmptyComments(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
path := filepath.Join(tmpDir, "test.yml")
|
||||||
|
|
||||||
|
// Multiple empty comments before actual content
|
||||||
|
content := `#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Real description
|
||||||
|
kernel:
|
||||||
|
image: test
|
||||||
|
`
|
||||||
|
err := os.WriteFile(path, []byte(content), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
desc := extractTemplateDescription(path)
|
||||||
|
|
||||||
|
assert.Equal(t, "Real description", desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserTemplatesDir_Good_NoDirectory(t *testing.T) {
|
||||||
|
// Save current working directory
|
||||||
|
oldWd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create a temp directory without .core/linuxkit
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
err = os.Chdir(tmpDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.Chdir(oldWd)
|
||||||
|
|
||||||
|
dir := getUserTemplatesDir()
|
||||||
|
|
||||||
|
// Should return empty string since no templates dir exists
|
||||||
|
// (unless home dir has one)
|
||||||
|
assert.True(t, dir == "" || strings.Contains(dir, "linuxkit"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanUserTemplates_Good_DefaultDescription(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
// Create a template without comments
|
||||||
|
content := `kernel:
|
||||||
|
image: test
|
||||||
|
`
|
||||||
|
err := os.WriteFile(filepath.Join(tmpDir, "nocomment.yml"), []byte(content), 0644)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
templates := scanUserTemplates(tmpDir)
|
||||||
|
|
||||||
|
assert.Len(t, templates, 1)
|
||||||
|
assert.Equal(t, "User-defined template", templates[0].Description)
|
||||||
|
}
|
||||||
|
|
|
||||||
223
pkg/release/publishers/aur_test.go
Normal file
223
pkg/release/publishers/aur_test.go
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
package publishers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAURPublisher_Name_Good(t *testing.T) {
|
||||||
|
t.Run("returns aur", func(t *testing.T) {
|
||||||
|
p := NewAURPublisher()
|
||||||
|
assert.Equal(t, "aur", p.Name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAURPublisher_ParseConfig_Good(t *testing.T) {
|
||||||
|
p := NewAURPublisher()
|
||||||
|
|
||||||
|
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{Type: "aur"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Package)
|
||||||
|
assert.Empty(t, cfg.Maintainer)
|
||||||
|
assert.Nil(t, cfg.Official)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses package and maintainer from extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "aur",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"package": "mypackage",
|
||||||
|
"maintainer": "John Doe <john@example.com>",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Equal(t, "mypackage", cfg.Package)
|
||||||
|
assert.Equal(t, "John Doe <john@example.com>", cfg.Maintainer)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses official config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "aur",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"official": map[string]any{
|
||||||
|
"enabled": true,
|
||||||
|
"output": "dist/aur-files",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
require.NotNil(t, cfg.Official)
|
||||||
|
assert.True(t, cfg.Official.Enabled)
|
||||||
|
assert.Equal(t, "dist/aur-files", cfg.Official.Output)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles missing official fields", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "aur",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"official": map[string]any{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
require.NotNil(t, cfg.Official)
|
||||||
|
assert.False(t, cfg.Official.Enabled)
|
||||||
|
assert.Empty(t, cfg.Official.Output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAURPublisher_RenderTemplate_Good(t *testing.T) {
|
||||||
|
p := NewAURPublisher()
|
||||||
|
|
||||||
|
t.Run("renders PKGBUILD template with data", func(t *testing.T) {
|
||||||
|
data := aurTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Description: "My awesome CLI",
|
||||||
|
Repository: "owner/myapp",
|
||||||
|
Version: "1.2.3",
|
||||||
|
License: "MIT",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Maintainer: "John Doe <john@example.com>",
|
||||||
|
Checksums: ChecksumMap{
|
||||||
|
LinuxAmd64: "abc123",
|
||||||
|
LinuxArm64: "def456",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.renderTemplate("templates/aur/PKGBUILD.tmpl", data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, result, "# Maintainer: John Doe <john@example.com>")
|
||||||
|
assert.Contains(t, result, "pkgname=myapp-bin")
|
||||||
|
assert.Contains(t, result, "pkgver=1.2.3")
|
||||||
|
assert.Contains(t, result, `pkgdesc="My awesome CLI"`)
|
||||||
|
assert.Contains(t, result, "url=\"https://github.com/owner/myapp\"")
|
||||||
|
assert.Contains(t, result, "license=('MIT')")
|
||||||
|
assert.Contains(t, result, "sha256sums_x86_64=('abc123')")
|
||||||
|
assert.Contains(t, result, "sha256sums_aarch64=('def456')")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("renders .SRCINFO template with data", func(t *testing.T) {
|
||||||
|
data := aurTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Description: "My CLI",
|
||||||
|
Repository: "owner/myapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
License: "MIT",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Maintainer: "Test <test@test.com>",
|
||||||
|
Checksums: ChecksumMap{
|
||||||
|
LinuxAmd64: "checksum1",
|
||||||
|
LinuxArm64: "checksum2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.renderTemplate("templates/aur/.SRCINFO.tmpl", data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, result, "pkgbase = myapp-bin")
|
||||||
|
assert.Contains(t, result, "pkgdesc = My CLI")
|
||||||
|
assert.Contains(t, result, "pkgver = 1.0.0")
|
||||||
|
assert.Contains(t, result, "arch = x86_64")
|
||||||
|
assert.Contains(t, result, "arch = aarch64")
|
||||||
|
assert.Contains(t, result, "sha256sums_x86_64 = checksum1")
|
||||||
|
assert.Contains(t, result, "sha256sums_aarch64 = checksum2")
|
||||||
|
assert.Contains(t, result, "pkgname = myapp-bin")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAURPublisher_RenderTemplate_Bad(t *testing.T) {
|
||||||
|
p := NewAURPublisher()
|
||||||
|
|
||||||
|
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||||
|
data := aurTemplateData{}
|
||||||
|
_, err := p.renderTemplate("templates/aur/nonexistent.tmpl", data)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to read template")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAURPublisher_DryRunPublish_Good(t *testing.T) {
|
||||||
|
p := NewAURPublisher()
|
||||||
|
|
||||||
|
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := aurTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Maintainer: "John Doe <john@example.com>",
|
||||||
|
Repository: "owner/repo",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
cfg := AURConfig{
|
||||||
|
Maintainer: "John Doe <john@example.com>",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
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: 1.0.0")
|
||||||
|
assert.Contains(t, output, "Maintainer: John Doe <john@example.com>")
|
||||||
|
assert.Contains(t, output, "Repository: owner/repo")
|
||||||
|
assert.Contains(t, output, "Generated PKGBUILD:")
|
||||||
|
assert.Contains(t, output, "Generated .SRCINFO:")
|
||||||
|
assert.Contains(t, output, "Would push to AUR: ssh://aur@aur.archlinux.org/myapp-bin.git")
|
||||||
|
assert.Contains(t, output, "END DRY RUN")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAURPublisher_Publish_Bad(t *testing.T) {
|
||||||
|
p := NewAURPublisher()
|
||||||
|
|
||||||
|
t.Run("fails when maintainer not configured", func(t *testing.T) {
|
||||||
|
release := &Release{
|
||||||
|
Version: "v1.0.0",
|
||||||
|
ProjectDir: "/project",
|
||||||
|
}
|
||||||
|
pubCfg := PublisherConfig{Type: "aur"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
|
||||||
|
err := p.Publish(nil, release, pubCfg, relCfg, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "maintainer is required")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAURConfig_Defaults_Good(t *testing.T) {
|
||||||
|
t.Run("has sensible defaults", func(t *testing.T) {
|
||||||
|
p := NewAURPublisher()
|
||||||
|
pubCfg := PublisherConfig{Type: "aur"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Package)
|
||||||
|
assert.Empty(t, cfg.Maintainer)
|
||||||
|
assert.Nil(t, cfg.Official)
|
||||||
|
})
|
||||||
|
}
|
||||||
320
pkg/release/publishers/chocolatey_test.go
Normal file
320
pkg/release/publishers/chocolatey_test.go
Normal file
|
|
@ -0,0 +1,320 @@
|
||||||
|
package publishers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChocolateyPublisher_Name_Good(t *testing.T) {
|
||||||
|
t.Run("returns chocolatey", func(t *testing.T) {
|
||||||
|
p := NewChocolateyPublisher()
|
||||||
|
assert.Equal(t, "chocolatey", p.Name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChocolateyPublisher_ParseConfig_Good(t *testing.T) {
|
||||||
|
p := NewChocolateyPublisher()
|
||||||
|
|
||||||
|
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{Type: "chocolatey"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Package)
|
||||||
|
assert.False(t, cfg.Push)
|
||||||
|
assert.Nil(t, cfg.Official)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses package and push from extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "chocolatey",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"package": "mypackage",
|
||||||
|
"push": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Equal(t, "mypackage", cfg.Package)
|
||||||
|
assert.True(t, cfg.Push)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses official config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "chocolatey",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"official": map[string]any{
|
||||||
|
"enabled": true,
|
||||||
|
"output": "dist/choco",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
require.NotNil(t, cfg.Official)
|
||||||
|
assert.True(t, cfg.Official.Enabled)
|
||||||
|
assert.Equal(t, "dist/choco", cfg.Official.Output)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles missing official fields", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "chocolatey",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"official": map[string]any{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
require.NotNil(t, cfg.Official)
|
||||||
|
assert.False(t, cfg.Official.Enabled)
|
||||||
|
assert.Empty(t, cfg.Official.Output)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles nil extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "chocolatey",
|
||||||
|
Extended: nil,
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Package)
|
||||||
|
assert.False(t, cfg.Push)
|
||||||
|
assert.Nil(t, cfg.Official)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("defaults push to false when not specified", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "chocolatey",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"package": "mypackage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.False(t, cfg.Push)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChocolateyPublisher_RenderTemplate_Good(t *testing.T) {
|
||||||
|
p := NewChocolateyPublisher()
|
||||||
|
|
||||||
|
t.Run("renders nuspec template with data", func(t *testing.T) {
|
||||||
|
data := chocolateyTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Title: "MyApp CLI",
|
||||||
|
Description: "My awesome CLI",
|
||||||
|
Repository: "owner/myapp",
|
||||||
|
Version: "1.2.3",
|
||||||
|
License: "MIT",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Authors: "owner",
|
||||||
|
Tags: "cli myapp",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.renderTemplate("templates/chocolatey/package.nuspec.tmpl", data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, result, `<id>myapp</id>`)
|
||||||
|
assert.Contains(t, result, `<version>1.2.3</version>`)
|
||||||
|
assert.Contains(t, result, `<title>MyApp CLI</title>`)
|
||||||
|
assert.Contains(t, result, `<authors>owner</authors>`)
|
||||||
|
assert.Contains(t, result, `<description>My awesome CLI</description>`)
|
||||||
|
assert.Contains(t, result, `<tags>cli myapp</tags>`)
|
||||||
|
assert.Contains(t, result, "projectUrl>https://github.com/owner/myapp")
|
||||||
|
assert.Contains(t, result, "releaseNotes>https://github.com/owner/myapp/releases/tag/v1.2.3")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("renders install script template with data", func(t *testing.T) {
|
||||||
|
data := chocolateyTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Repository: "owner/myapp",
|
||||||
|
Version: "1.2.3",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{
|
||||||
|
WindowsAmd64: "abc123def456",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.renderTemplate("templates/chocolatey/tools/chocolateyinstall.ps1.tmpl", data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, result, "$ErrorActionPreference = 'Stop'")
|
||||||
|
assert.Contains(t, result, "https://github.com/owner/myapp/releases/download/v1.2.3/myapp-windows-amd64.zip")
|
||||||
|
assert.Contains(t, result, "packageName = 'myapp'")
|
||||||
|
assert.Contains(t, result, "checksum64 = 'abc123def456'")
|
||||||
|
assert.Contains(t, result, "checksumType64 = 'sha256'")
|
||||||
|
assert.Contains(t, result, "Install-ChocolateyZipPackage")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChocolateyPublisher_RenderTemplate_Bad(t *testing.T) {
|
||||||
|
p := NewChocolateyPublisher()
|
||||||
|
|
||||||
|
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||||
|
data := chocolateyTemplateData{}
|
||||||
|
_, err := p.renderTemplate("templates/chocolatey/nonexistent.tmpl", data)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to read template")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChocolateyPublisher_DryRunPublish_Good(t *testing.T) {
|
||||||
|
p := NewChocolateyPublisher()
|
||||||
|
|
||||||
|
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := chocolateyTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Repository: "owner/repo",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Authors: "owner",
|
||||||
|
Tags: "cli myapp",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
cfg := ChocolateyConfig{
|
||||||
|
Push: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
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: myapp")
|
||||||
|
assert.Contains(t, output, "Version: 1.0.0")
|
||||||
|
assert.Contains(t, output, "Push: false")
|
||||||
|
assert.Contains(t, output, "Repository: owner/repo")
|
||||||
|
assert.Contains(t, output, "Generated package.nuspec:")
|
||||||
|
assert.Contains(t, output, "Generated chocolateyinstall.ps1:")
|
||||||
|
assert.Contains(t, output, "Would generate package files only (push=false)")
|
||||||
|
assert.Contains(t, output, "END DRY RUN")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("shows push message when push is enabled", func(t *testing.T) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := chocolateyTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Authors: "owner",
|
||||||
|
Tags: "cli",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
cfg := ChocolateyConfig{
|
||||||
|
Push: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_, _ = buf.ReadFrom(r)
|
||||||
|
os.Stdout = oldStdout
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
output := buf.String()
|
||||||
|
assert.Contains(t, output, "Push: true")
|
||||||
|
assert.Contains(t, output, "Would push to Chocolatey community repo")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChocolateyPublisher_ExecutePublish_Bad(t *testing.T) {
|
||||||
|
p := NewChocolateyPublisher()
|
||||||
|
|
||||||
|
t.Run("fails when CHOCOLATEY_API_KEY not set for push", func(t *testing.T) {
|
||||||
|
// Ensure CHOCOLATEY_API_KEY is not set
|
||||||
|
oldKey := os.Getenv("CHOCOLATEY_API_KEY")
|
||||||
|
os.Unsetenv("CHOCOLATEY_API_KEY")
|
||||||
|
defer func() {
|
||||||
|
if oldKey != "" {
|
||||||
|
os.Setenv("CHOCOLATEY_API_KEY", oldKey)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a temp directory for the test
|
||||||
|
tmpDir, err := os.MkdirTemp("", "choco-test-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
|
data := chocolateyTemplateData{
|
||||||
|
PackageName: "testpkg",
|
||||||
|
Version: "1.0.0",
|
||||||
|
BinaryName: "testpkg",
|
||||||
|
Repository: "owner/repo",
|
||||||
|
Authors: "owner",
|
||||||
|
Tags: "cli",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.pushToChocolatey(nil, tmpDir, data)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "CHOCOLATEY_API_KEY environment variable is required")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChocolateyConfig_Defaults_Good(t *testing.T) {
|
||||||
|
t.Run("has sensible defaults", func(t *testing.T) {
|
||||||
|
p := NewChocolateyPublisher()
|
||||||
|
pubCfg := PublisherConfig{Type: "chocolatey"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Package)
|
||||||
|
assert.False(t, cfg.Push)
|
||||||
|
assert.Nil(t, cfg.Official)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChocolateyTemplateData_Good(t *testing.T) {
|
||||||
|
t.Run("struct has all expected fields", func(t *testing.T) {
|
||||||
|
data := chocolateyTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Title: "MyApp CLI",
|
||||||
|
Description: "description",
|
||||||
|
Repository: "org/repo",
|
||||||
|
Version: "1.0.0",
|
||||||
|
License: "MIT",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Authors: "org",
|
||||||
|
Tags: "cli tool",
|
||||||
|
Checksums: ChecksumMap{
|
||||||
|
WindowsAmd64: "hash1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "myapp", data.PackageName)
|
||||||
|
assert.Equal(t, "MyApp CLI", data.Title)
|
||||||
|
assert.Equal(t, "description", data.Description)
|
||||||
|
assert.Equal(t, "org/repo", data.Repository)
|
||||||
|
assert.Equal(t, "1.0.0", data.Version)
|
||||||
|
assert.Equal(t, "MIT", data.License)
|
||||||
|
assert.Equal(t, "myapp", data.BinaryName)
|
||||||
|
assert.Equal(t, "org", data.Authors)
|
||||||
|
assert.Equal(t, "cli tool", data.Tags)
|
||||||
|
assert.Equal(t, "hash1", data.Checksums.WindowsAmd64)
|
||||||
|
})
|
||||||
|
}
|
||||||
344
pkg/release/publishers/homebrew_test.go
Normal file
344
pkg/release/publishers/homebrew_test.go
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
package publishers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/host-uk/core/pkg/build"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHomebrewPublisher_Name_Good(t *testing.T) {
|
||||||
|
t.Run("returns homebrew", func(t *testing.T) {
|
||||||
|
p := NewHomebrewPublisher()
|
||||||
|
assert.Equal(t, "homebrew", p.Name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomebrewPublisher_ParseConfig_Good(t *testing.T) {
|
||||||
|
p := NewHomebrewPublisher()
|
||||||
|
|
||||||
|
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{Type: "homebrew"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Tap)
|
||||||
|
assert.Empty(t, cfg.Formula)
|
||||||
|
assert.Nil(t, cfg.Official)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses tap and formula from extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "homebrew",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"tap": "host-uk/homebrew-tap",
|
||||||
|
"formula": "myformula",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Equal(t, "host-uk/homebrew-tap", cfg.Tap)
|
||||||
|
assert.Equal(t, "myformula", cfg.Formula)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses official config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "homebrew",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"official": map[string]any{
|
||||||
|
"enabled": true,
|
||||||
|
"output": "dist/brew",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
require.NotNil(t, cfg.Official)
|
||||||
|
assert.True(t, cfg.Official.Enabled)
|
||||||
|
assert.Equal(t, "dist/brew", cfg.Official.Output)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles missing official fields", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "homebrew",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"official": map[string]any{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
require.NotNil(t, cfg.Official)
|
||||||
|
assert.False(t, cfg.Official.Enabled)
|
||||||
|
assert.Empty(t, cfg.Official.Output)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomebrewPublisher_ToFormulaClass_Good(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple name",
|
||||||
|
input: "core",
|
||||||
|
expected: "Core",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "kebab case",
|
||||||
|
input: "my-cli-tool",
|
||||||
|
expected: "MyCliTool",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "already capitalised",
|
||||||
|
input: "CLI",
|
||||||
|
expected: "CLI",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single letter",
|
||||||
|
input: "x",
|
||||||
|
expected: "X",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple dashes",
|
||||||
|
input: "my-super-cool-app",
|
||||||
|
expected: "MySuperCoolApp",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := toFormulaClass(tc.input)
|
||||||
|
assert.Equal(t, tc.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomebrewPublisher_BuildChecksumMap_Good(t *testing.T) {
|
||||||
|
t.Run("maps artifacts to checksums by platform", func(t *testing.T) {
|
||||||
|
artifacts := []build.Artifact{
|
||||||
|
{Path: "/dist/myapp-darwin-amd64.tar.gz", OS: "darwin", Arch: "amd64", Checksum: "abc123"},
|
||||||
|
{Path: "/dist/myapp-darwin-arm64.tar.gz", OS: "darwin", Arch: "arm64", Checksum: "def456"},
|
||||||
|
{Path: "/dist/myapp-linux-amd64.tar.gz", OS: "linux", Arch: "amd64", Checksum: "ghi789"},
|
||||||
|
{Path: "/dist/myapp-linux-arm64.tar.gz", OS: "linux", Arch: "arm64", Checksum: "jkl012"},
|
||||||
|
{Path: "/dist/myapp-windows-amd64.zip", OS: "windows", Arch: "amd64", Checksum: "mno345"},
|
||||||
|
{Path: "/dist/myapp-windows-arm64.zip", OS: "windows", Arch: "arm64", Checksum: "pqr678"},
|
||||||
|
}
|
||||||
|
|
||||||
|
checksums := buildChecksumMap(artifacts)
|
||||||
|
|
||||||
|
assert.Equal(t, "abc123", checksums.DarwinAmd64)
|
||||||
|
assert.Equal(t, "def456", checksums.DarwinArm64)
|
||||||
|
assert.Equal(t, "ghi789", checksums.LinuxAmd64)
|
||||||
|
assert.Equal(t, "jkl012", checksums.LinuxArm64)
|
||||||
|
assert.Equal(t, "mno345", checksums.WindowsAmd64)
|
||||||
|
assert.Equal(t, "pqr678", checksums.WindowsArm64)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles empty artifacts", func(t *testing.T) {
|
||||||
|
checksums := buildChecksumMap([]build.Artifact{})
|
||||||
|
|
||||||
|
assert.Empty(t, checksums.DarwinAmd64)
|
||||||
|
assert.Empty(t, checksums.DarwinArm64)
|
||||||
|
assert.Empty(t, checksums.LinuxAmd64)
|
||||||
|
assert.Empty(t, checksums.LinuxArm64)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles partial platform coverage", func(t *testing.T) {
|
||||||
|
artifacts := []build.Artifact{
|
||||||
|
{Path: "/dist/myapp-darwin-arm64.tar.gz", Checksum: "def456"},
|
||||||
|
{Path: "/dist/myapp-linux-amd64.tar.gz", Checksum: "ghi789"},
|
||||||
|
}
|
||||||
|
|
||||||
|
checksums := buildChecksumMap(artifacts)
|
||||||
|
|
||||||
|
assert.Empty(t, checksums.DarwinAmd64)
|
||||||
|
assert.Equal(t, "def456", checksums.DarwinArm64)
|
||||||
|
assert.Equal(t, "ghi789", checksums.LinuxAmd64)
|
||||||
|
assert.Empty(t, checksums.LinuxArm64)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomebrewPublisher_RenderTemplate_Good(t *testing.T) {
|
||||||
|
p := NewHomebrewPublisher()
|
||||||
|
|
||||||
|
t.Run("renders formula template with data", func(t *testing.T) {
|
||||||
|
data := homebrewTemplateData{
|
||||||
|
FormulaClass: "MyApp",
|
||||||
|
Description: "My awesome CLI",
|
||||||
|
Repository: "owner/myapp",
|
||||||
|
Version: "1.2.3",
|
||||||
|
License: "MIT",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{
|
||||||
|
DarwinAmd64: "abc123",
|
||||||
|
DarwinArm64: "def456",
|
||||||
|
LinuxAmd64: "ghi789",
|
||||||
|
LinuxArm64: "jkl012",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.renderTemplate("templates/homebrew/formula.rb.tmpl", data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, result, "class MyApp < Formula")
|
||||||
|
assert.Contains(t, result, `desc "My awesome CLI"`)
|
||||||
|
assert.Contains(t, result, `version "1.2.3"`)
|
||||||
|
assert.Contains(t, result, `license "MIT"`)
|
||||||
|
assert.Contains(t, result, "owner/myapp")
|
||||||
|
assert.Contains(t, result, "abc123")
|
||||||
|
assert.Contains(t, result, "def456")
|
||||||
|
assert.Contains(t, result, "ghi789")
|
||||||
|
assert.Contains(t, result, "jkl012")
|
||||||
|
assert.Contains(t, result, `bin.install "myapp"`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomebrewPublisher_RenderTemplate_Bad(t *testing.T) {
|
||||||
|
p := NewHomebrewPublisher()
|
||||||
|
|
||||||
|
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||||
|
data := homebrewTemplateData{}
|
||||||
|
_, err := p.renderTemplate("templates/homebrew/nonexistent.tmpl", data)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to read template")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomebrewPublisher_DryRunPublish_Good(t *testing.T) {
|
||||||
|
p := NewHomebrewPublisher()
|
||||||
|
|
||||||
|
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||||
|
// Capture stdout
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := homebrewTemplateData{
|
||||||
|
FormulaClass: "MyApp",
|
||||||
|
Description: "My CLI",
|
||||||
|
Repository: "owner/repo",
|
||||||
|
Version: "1.0.0",
|
||||||
|
License: "MIT",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
cfg := HomebrewConfig{
|
||||||
|
Tap: "owner/homebrew-tap",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
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: Homebrew Publish")
|
||||||
|
assert.Contains(t, output, "Formula: MyApp")
|
||||||
|
assert.Contains(t, output, "Version: 1.0.0")
|
||||||
|
assert.Contains(t, output, "Tap: owner/homebrew-tap")
|
||||||
|
assert.Contains(t, output, "Repository: owner/repo")
|
||||||
|
assert.Contains(t, output, "Would commit to tap: owner/homebrew-tap")
|
||||||
|
assert.Contains(t, output, "END DRY RUN")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("shows official output path when enabled", func(t *testing.T) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := homebrewTemplateData{
|
||||||
|
FormulaClass: "MyApp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
cfg := HomebrewConfig{
|
||||||
|
Official: &OfficialConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Output: "custom/path",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
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: custom/path")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("uses default official output path when not specified", func(t *testing.T) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := homebrewTemplateData{
|
||||||
|
FormulaClass: "MyApp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
cfg := HomebrewConfig{
|
||||||
|
Official: &OfficialConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomebrewPublisher_Publish_Bad(t *testing.T) {
|
||||||
|
p := NewHomebrewPublisher()
|
||||||
|
|
||||||
|
t.Run("fails when tap not configured and not official mode", func(t *testing.T) {
|
||||||
|
release := &Release{
|
||||||
|
Version: "v1.0.0",
|
||||||
|
ProjectDir: "/project",
|
||||||
|
}
|
||||||
|
pubCfg := PublisherConfig{Type: "homebrew"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
|
||||||
|
err := p.Publish(nil, release, pubCfg, relCfg, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "tap is required")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomebrewConfig_Defaults_Good(t *testing.T) {
|
||||||
|
t.Run("has sensible defaults", func(t *testing.T) {
|
||||||
|
p := NewHomebrewPublisher()
|
||||||
|
pubCfg := PublisherConfig{Type: "homebrew"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Tap)
|
||||||
|
assert.Empty(t, cfg.Formula)
|
||||||
|
assert.Nil(t, cfg.Official)
|
||||||
|
})
|
||||||
|
}
|
||||||
298
pkg/release/publishers/npm_test.go
Normal file
298
pkg/release/publishers/npm_test.go
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
package publishers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNpmPublisher_Name_Good(t *testing.T) {
|
||||||
|
t.Run("returns npm", func(t *testing.T) {
|
||||||
|
p := NewNpmPublisher()
|
||||||
|
assert.Equal(t, "npm", p.Name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNpmPublisher_ParseConfig_Good(t *testing.T) {
|
||||||
|
p := NewNpmPublisher()
|
||||||
|
|
||||||
|
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{Type: "npm"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Package)
|
||||||
|
assert.Equal(t, "public", cfg.Access)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses package and access from extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "npm",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"package": "@myorg/mypackage",
|
||||||
|
"access": "restricted",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Equal(t, "@myorg/mypackage", cfg.Package)
|
||||||
|
assert.Equal(t, "restricted", cfg.Access)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeps default access when not specified", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "npm",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"package": "@myorg/mypackage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Equal(t, "@myorg/mypackage", cfg.Package)
|
||||||
|
assert.Equal(t, "public", cfg.Access)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles nil extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "npm",
|
||||||
|
Extended: nil,
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Package)
|
||||||
|
assert.Equal(t, "public", cfg.Access)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles empty strings in config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "npm",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"package": "",
|
||||||
|
"access": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Package)
|
||||||
|
assert.Equal(t, "public", cfg.Access)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNpmPublisher_RenderTemplate_Good(t *testing.T) {
|
||||||
|
p := NewNpmPublisher()
|
||||||
|
|
||||||
|
t.Run("renders package.json template with data", func(t *testing.T) {
|
||||||
|
data := npmTemplateData{
|
||||||
|
Package: "@myorg/mycli",
|
||||||
|
Version: "1.2.3",
|
||||||
|
Description: "My awesome CLI",
|
||||||
|
License: "MIT",
|
||||||
|
Repository: "owner/myapp",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
ProjectName: "myapp",
|
||||||
|
Access: "public",
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.renderTemplate("templates/npm/package.json.tmpl", data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, result, `"name": "@myorg/mycli"`)
|
||||||
|
assert.Contains(t, result, `"version": "1.2.3"`)
|
||||||
|
assert.Contains(t, result, `"description": "My awesome CLI"`)
|
||||||
|
assert.Contains(t, result, `"license": "MIT"`)
|
||||||
|
assert.Contains(t, result, "owner/myapp")
|
||||||
|
assert.Contains(t, result, `"myapp": "./bin/run.js"`)
|
||||||
|
assert.Contains(t, result, `"access": "public"`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("renders restricted access correctly", func(t *testing.T) {
|
||||||
|
data := npmTemplateData{
|
||||||
|
Package: "@private/cli",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Description: "Private CLI",
|
||||||
|
License: "MIT",
|
||||||
|
Repository: "org/repo",
|
||||||
|
BinaryName: "cli",
|
||||||
|
ProjectName: "cli",
|
||||||
|
Access: "restricted",
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.renderTemplate("templates/npm/package.json.tmpl", data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, result, `"access": "restricted"`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNpmPublisher_RenderTemplate_Bad(t *testing.T) {
|
||||||
|
p := NewNpmPublisher()
|
||||||
|
|
||||||
|
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||||
|
data := npmTemplateData{}
|
||||||
|
_, err := p.renderTemplate("templates/npm/nonexistent.tmpl", data)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to read template")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNpmPublisher_DryRunPublish_Good(t *testing.T) {
|
||||||
|
p := NewNpmPublisher()
|
||||||
|
|
||||||
|
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := npmTemplateData{
|
||||||
|
Package: "@myorg/mycli",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Access: "public",
|
||||||
|
Repository: "owner/repo",
|
||||||
|
BinaryName: "mycli",
|
||||||
|
Description: "My CLI",
|
||||||
|
}
|
||||||
|
cfg := &NpmConfig{
|
||||||
|
Package: "@myorg/mycli",
|
||||||
|
Access: "public",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
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: @myorg/mycli")
|
||||||
|
assert.Contains(t, output, "Version: 1.0.0")
|
||||||
|
assert.Contains(t, output, "Access: public")
|
||||||
|
assert.Contains(t, output, "Repository: owner/repo")
|
||||||
|
assert.Contains(t, output, "Binary: mycli")
|
||||||
|
assert.Contains(t, output, "Generated package.json:")
|
||||||
|
assert.Contains(t, output, "Would run: npm publish --access public")
|
||||||
|
assert.Contains(t, output, "END DRY RUN")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("shows restricted access correctly", func(t *testing.T) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := npmTemplateData{
|
||||||
|
Package: "@private/cli",
|
||||||
|
Version: "2.0.0",
|
||||||
|
Access: "restricted",
|
||||||
|
Repository: "org/repo",
|
||||||
|
BinaryName: "cli",
|
||||||
|
}
|
||||||
|
cfg := &NpmConfig{
|
||||||
|
Package: "@private/cli",
|
||||||
|
Access: "restricted",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
w.Close()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_, _ = buf.ReadFrom(r)
|
||||||
|
os.Stdout = oldStdout
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
output := buf.String()
|
||||||
|
|
||||||
|
assert.Contains(t, output, "Access: restricted")
|
||||||
|
assert.Contains(t, output, "Would run: npm publish --access restricted")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNpmPublisher_Publish_Bad(t *testing.T) {
|
||||||
|
p := NewNpmPublisher()
|
||||||
|
|
||||||
|
t.Run("fails when package name not configured", func(t *testing.T) {
|
||||||
|
release := &Release{
|
||||||
|
Version: "v1.0.0",
|
||||||
|
ProjectDir: "/project",
|
||||||
|
}
|
||||||
|
pubCfg := PublisherConfig{Type: "npm"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
|
||||||
|
err := p.Publish(nil, release, pubCfg, relCfg, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "package name is required")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fails when NPM_TOKEN not set in non-dry-run", func(t *testing.T) {
|
||||||
|
// Ensure NPM_TOKEN is not set
|
||||||
|
oldToken := os.Getenv("NPM_TOKEN")
|
||||||
|
os.Unsetenv("NPM_TOKEN")
|
||||||
|
defer func() {
|
||||||
|
if oldToken != "" {
|
||||||
|
os.Setenv("NPM_TOKEN", oldToken)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
release := &Release{
|
||||||
|
Version: "v1.0.0",
|
||||||
|
ProjectDir: "/project",
|
||||||
|
}
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "npm",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"package": "@test/package",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
|
||||||
|
err := p.Publish(nil, release, pubCfg, relCfg, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "NPM_TOKEN environment variable is required")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNpmConfig_Defaults_Good(t *testing.T) {
|
||||||
|
t.Run("has sensible defaults", func(t *testing.T) {
|
||||||
|
p := NewNpmPublisher()
|
||||||
|
pubCfg := PublisherConfig{Type: "npm"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Package)
|
||||||
|
assert.Equal(t, "public", cfg.Access)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNpmTemplateData_Good(t *testing.T) {
|
||||||
|
t.Run("struct has all expected fields", func(t *testing.T) {
|
||||||
|
data := npmTemplateData{
|
||||||
|
Package: "@myorg/package",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Description: "description",
|
||||||
|
License: "MIT",
|
||||||
|
Repository: "org/repo",
|
||||||
|
BinaryName: "cli",
|
||||||
|
ProjectName: "cli",
|
||||||
|
Access: "public",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "@myorg/package", data.Package)
|
||||||
|
assert.Equal(t, "1.0.0", data.Version)
|
||||||
|
assert.Equal(t, "description", data.Description)
|
||||||
|
assert.Equal(t, "MIT", data.License)
|
||||||
|
assert.Equal(t, "org/repo", data.Repository)
|
||||||
|
assert.Equal(t, "cli", data.BinaryName)
|
||||||
|
assert.Equal(t, "cli", data.ProjectName)
|
||||||
|
assert.Equal(t, "public", data.Access)
|
||||||
|
})
|
||||||
|
}
|
||||||
307
pkg/release/publishers/scoop_test.go
Normal file
307
pkg/release/publishers/scoop_test.go
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
package publishers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestScoopPublisher_Name_Good(t *testing.T) {
|
||||||
|
t.Run("returns scoop", func(t *testing.T) {
|
||||||
|
p := NewScoopPublisher()
|
||||||
|
assert.Equal(t, "scoop", p.Name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScoopPublisher_ParseConfig_Good(t *testing.T) {
|
||||||
|
p := NewScoopPublisher()
|
||||||
|
|
||||||
|
t.Run("uses defaults when no extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{Type: "scoop"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Bucket)
|
||||||
|
assert.Nil(t, cfg.Official)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses bucket from extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "scoop",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"bucket": "host-uk/scoop-bucket",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Equal(t, "host-uk/scoop-bucket", cfg.Bucket)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("parses official config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "scoop",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"official": map[string]any{
|
||||||
|
"enabled": true,
|
||||||
|
"output": "dist/scoop-manifest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
require.NotNil(t, cfg.Official)
|
||||||
|
assert.True(t, cfg.Official.Enabled)
|
||||||
|
assert.Equal(t, "dist/scoop-manifest", cfg.Official.Output)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles missing official fields", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "scoop",
|
||||||
|
Extended: map[string]any{
|
||||||
|
"official": map[string]any{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
require.NotNil(t, cfg.Official)
|
||||||
|
assert.False(t, cfg.Official.Enabled)
|
||||||
|
assert.Empty(t, cfg.Official.Output)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("handles nil extended config", func(t *testing.T) {
|
||||||
|
pubCfg := PublisherConfig{
|
||||||
|
Type: "scoop",
|
||||||
|
Extended: nil,
|
||||||
|
}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Bucket)
|
||||||
|
assert.Nil(t, cfg.Official)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScoopPublisher_RenderTemplate_Good(t *testing.T) {
|
||||||
|
p := NewScoopPublisher()
|
||||||
|
|
||||||
|
t.Run("renders manifest template with data", func(t *testing.T) {
|
||||||
|
data := scoopTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Description: "My awesome CLI",
|
||||||
|
Repository: "owner/myapp",
|
||||||
|
Version: "1.2.3",
|
||||||
|
License: "MIT",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{
|
||||||
|
WindowsAmd64: "abc123",
|
||||||
|
WindowsArm64: "def456",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.renderTemplate("templates/scoop/manifest.json.tmpl", data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, result, `"version": "1.2.3"`)
|
||||||
|
assert.Contains(t, result, `"description": "My awesome CLI"`)
|
||||||
|
assert.Contains(t, result, `"homepage": "https://github.com/owner/myapp"`)
|
||||||
|
assert.Contains(t, result, `"license": "MIT"`)
|
||||||
|
assert.Contains(t, result, `"64bit"`)
|
||||||
|
assert.Contains(t, result, `"arm64"`)
|
||||||
|
assert.Contains(t, result, "myapp-windows-amd64.zip")
|
||||||
|
assert.Contains(t, result, "myapp-windows-arm64.zip")
|
||||||
|
assert.Contains(t, result, `"hash": "abc123"`)
|
||||||
|
assert.Contains(t, result, `"hash": "def456"`)
|
||||||
|
assert.Contains(t, result, `"bin": "myapp.exe"`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("includes autoupdate configuration", func(t *testing.T) {
|
||||||
|
data := scoopTemplateData{
|
||||||
|
PackageName: "tool",
|
||||||
|
Description: "A tool",
|
||||||
|
Repository: "org/tool",
|
||||||
|
Version: "2.0.0",
|
||||||
|
License: "Apache-2.0",
|
||||||
|
BinaryName: "tool",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := p.renderTemplate("templates/scoop/manifest.json.tmpl", data)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, result, `"checkver"`)
|
||||||
|
assert.Contains(t, result, `"github": "https://github.com/org/tool"`)
|
||||||
|
assert.Contains(t, result, `"autoupdate"`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScoopPublisher_RenderTemplate_Bad(t *testing.T) {
|
||||||
|
p := NewScoopPublisher()
|
||||||
|
|
||||||
|
t.Run("returns error for non-existent template", func(t *testing.T) {
|
||||||
|
data := scoopTemplateData{}
|
||||||
|
_, err := p.renderTemplate("templates/scoop/nonexistent.tmpl", data)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "failed to read template")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScoopPublisher_DryRunPublish_Good(t *testing.T) {
|
||||||
|
p := NewScoopPublisher()
|
||||||
|
|
||||||
|
t.Run("outputs expected dry run information", func(t *testing.T) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := scoopTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
Repository: "owner/repo",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
cfg := ScoopConfig{
|
||||||
|
Bucket: "owner/scoop-bucket",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
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.0.0")
|
||||||
|
assert.Contains(t, output, "Bucket: owner/scoop-bucket")
|
||||||
|
assert.Contains(t, output, "Repository: owner/repo")
|
||||||
|
assert.Contains(t, output, "Generated manifest.json:")
|
||||||
|
assert.Contains(t, output, "Would commit to bucket: owner/scoop-bucket")
|
||||||
|
assert.Contains(t, output, "END DRY RUN")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("shows official output path when enabled", func(t *testing.T) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := scoopTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
cfg := ScoopConfig{
|
||||||
|
Official: &OfficialConfig{
|
||||||
|
Enabled: true,
|
||||||
|
Output: "custom/scoop/path",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
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: custom/scoop/path")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("uses default official output path when not specified", func(t *testing.T) {
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
data := scoopTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Version: "1.0.0",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{},
|
||||||
|
}
|
||||||
|
cfg := ScoopConfig{
|
||||||
|
Official: &OfficialConfig{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := p.dryRunPublish(data, cfg)
|
||||||
|
|
||||||
|
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/scoop")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScoopPublisher_Publish_Bad(t *testing.T) {
|
||||||
|
p := NewScoopPublisher()
|
||||||
|
|
||||||
|
t.Run("fails when bucket not configured and not official mode", func(t *testing.T) {
|
||||||
|
release := &Release{
|
||||||
|
Version: "v1.0.0",
|
||||||
|
ProjectDir: "/project",
|
||||||
|
}
|
||||||
|
pubCfg := PublisherConfig{Type: "scoop"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
|
||||||
|
err := p.Publish(nil, release, pubCfg, relCfg, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "bucket is required")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScoopConfig_Defaults_Good(t *testing.T) {
|
||||||
|
t.Run("has sensible defaults", func(t *testing.T) {
|
||||||
|
p := NewScoopPublisher()
|
||||||
|
pubCfg := PublisherConfig{Type: "scoop"}
|
||||||
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
||||||
|
|
||||||
|
cfg := p.parseConfig(pubCfg, relCfg)
|
||||||
|
|
||||||
|
assert.Empty(t, cfg.Bucket)
|
||||||
|
assert.Nil(t, cfg.Official)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScoopTemplateData_Good(t *testing.T) {
|
||||||
|
t.Run("struct has all expected fields", func(t *testing.T) {
|
||||||
|
data := scoopTemplateData{
|
||||||
|
PackageName: "myapp",
|
||||||
|
Description: "description",
|
||||||
|
Repository: "org/repo",
|
||||||
|
Version: "1.0.0",
|
||||||
|
License: "MIT",
|
||||||
|
BinaryName: "myapp",
|
||||||
|
Checksums: ChecksumMap{
|
||||||
|
WindowsAmd64: "hash1",
|
||||||
|
WindowsArm64: "hash2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "myapp", data.PackageName)
|
||||||
|
assert.Equal(t, "description", data.Description)
|
||||||
|
assert.Equal(t, "org/repo", data.Repository)
|
||||||
|
assert.Equal(t, "1.0.0", data.Version)
|
||||||
|
assert.Equal(t, "MIT", data.License)
|
||||||
|
assert.Equal(t, "myapp", data.BinaryName)
|
||||||
|
assert.Equal(t, "hash1", data.Checksums.WindowsAmd64)
|
||||||
|
assert.Equal(t, "hash2", data.Checksums.WindowsArm64)
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue