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:
Snider 2026-01-29 13:19:08 +00:00
parent a7ee58d29e
commit 50f6839c51
8 changed files with 2386 additions and 32 deletions

View 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")
}

View file

@ -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() func TestLinuxKitManager_Logs_Good_Follow(t *testing.T) {
manager, _, _ := newTestManager(t)
// Create a unique container ID
uniqueID, _ := GenerateID()
container := &Container{ID: uniqueID}
manager.State().Add(container)
// Create a log file at the expected location
logPath, err := LogPath(uniqueID)
require.NoError(t, err)
os.MkdirAll(filepath.Dir(logPath), 0755)
// Write initial content
err = os.WriteFile(logPath, []byte("initial log content\n"), 0644)
require.NoError(t, err)
// 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 TestFollowReader_Read_Good_WithData(t *testing.T) {
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 = reader.Close()
assert.NoError(t, err)
// Reading after close should fail or return EOF
buf := make([]byte, 1024)
_, readErr := reader.Read(buf)
assert.Error(t, readErr)
}
func TestNewFollowReader_Bad_FileNotFound(t *testing.T) {
ctx := context.Background()
_, err := newFollowReader(ctx, "/nonexistent/path/to/file.log")
_, err := q.BuildCommand(ctx, "/path/to/image.txt", opts)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown image format")
} }
func TestHyperkitHypervisor_Name_Good(t *testing.T) { func TestLinuxKitManager_Run_Bad_BuildCommandError(t *testing.T) {
h := NewHyperkitHypervisor() manager, mock, tmpDir := newTestManager(t)
assert.Equal(t, "hyperkit", h.Name())
}
func TestHyperkitHypervisor_BuildCommand_Good(t *testing.T) { // Create a test image file
h := NewHyperkitHypervisor() 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() ctx := context.Background()
opts := &HypervisorOptions{ opts := RunOptions{Detach: true}
Memory: 1024,
CPUs: 2, _, err = manager.Run(ctx, imagePath, opts)
SSHPort: 2222, 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,
} }
cmd, err := h.BuildCommand(ctx, "/path/to/image.iso", opts) container, err := manager.Run(ctx, imagePath, opts)
require.NoError(t, err) require.NoError(t, err)
assert.NotNil(t, cmd)
args := cmd.Args assert.NotEmpty(t, container.ID)
assert.Contains(t, args, "-m") assert.Equal(t, "test-foreground", container.Name)
assert.Contains(t, args, "1024M") // Foreground process should have completed
assert.Contains(t, args, "-c") assert.Equal(t, StatusStopped, container.Status)
assert.Contains(t, args, "2")
} }
func TestHyperkitHypervisor_BuildCommand_Bad_UnknownFormat(t *testing.T) { func TestLinuxKitManager_Stop_Good_ContextCancelled(t *testing.T) {
h := NewHyperkitHypervisor() 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() ctx := context.Background()
opts := &HypervisorOptions{Memory: 1024, CPUs: 1} 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"},
}
_, err := h.BuildCommand(ctx, "/path/to/image.unknown", opts) container, err := manager.Run(ctx, imagePath, opts)
assert.Error(t, err) require.NoError(t, err)
assert.Contains(t, err.Error(), "unknown image format")
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 TestGetHypervisor_Bad_Unknown(t *testing.T) { func TestFollowReader_Read_Good_ReaderError(t *testing.T) {
_, err := GetHypervisor("unknown-hypervisor") tmpDir := t.TempDir()
assert.Error(t, err) logPath := filepath.Join(tmpDir, "test.log")
assert.Contains(t, err.Error(), "unknown hypervisor")
// 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)
} }

View file

@ -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)
}

View 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)
})
}

View 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)
})
}

View 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)
})
}

View 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)
})
}

View 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)
})
}