548 lines
15 KiB
Go
548 lines
15 KiB
Go
package publishers
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"dappco.re/go/core"
|
|
"dappco.re/go/core/build/internal/ax"
|
|
"dappco.re/go/core/build/pkg/build"
|
|
"dappco.re/go/core/io"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGitHub_ParseGitHubRepo_Good(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "SSH URL",
|
|
input: "git@github.com:owner/repo.git",
|
|
expected: "owner/repo",
|
|
},
|
|
{
|
|
name: "HTTPS URL with .git",
|
|
input: "https://github.com/owner/repo.git",
|
|
expected: "owner/repo",
|
|
},
|
|
{
|
|
name: "HTTPS URL without .git",
|
|
input: "https://github.com/owner/repo",
|
|
expected: "owner/repo",
|
|
},
|
|
{
|
|
name: "SSH URL without .git",
|
|
input: "git@github.com:owner/repo",
|
|
expected: "owner/repo",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result, err := parseGitHubRepo(tc.input)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tc.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitHub_ParseGitHubRepo_Bad(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
}{
|
|
{
|
|
name: "GitLab URL",
|
|
input: "https://gitlab.com/owner/repo.git",
|
|
},
|
|
{
|
|
name: "Bitbucket URL",
|
|
input: "git@bitbucket.org:owner/repo.git",
|
|
},
|
|
{
|
|
name: "Random URL",
|
|
input: "https://example.com/something",
|
|
},
|
|
{
|
|
name: "Not a URL",
|
|
input: "owner/repo",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, err := parseGitHubRepo(tc.input)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitHub_GitHubPublisherName_Good(t *testing.T) {
|
|
t.Run("returns github", func(t *testing.T) {
|
|
p := NewGitHubPublisher()
|
|
assert.Equal(t, "github", p.Name())
|
|
})
|
|
}
|
|
|
|
func TestGitHub_NewRelease_Good(t *testing.T) {
|
|
t.Run("creates release struct", func(t *testing.T) {
|
|
r := NewRelease("v1.0.0", nil, "changelog", "/project", io.Local)
|
|
assert.Equal(t, "v1.0.0", r.Version)
|
|
assert.Equal(t, "changelog", r.Changelog)
|
|
assert.Equal(t, "/project", r.ProjectDir)
|
|
assert.Nil(t, r.Artifacts)
|
|
})
|
|
}
|
|
|
|
func TestGitHub_NewPublisherConfig_Good(t *testing.T) {
|
|
t.Run("creates config struct", func(t *testing.T) {
|
|
cfg := NewPublisherConfig("github", true, false, nil)
|
|
assert.Equal(t, "github", cfg.Type)
|
|
assert.True(t, cfg.Prerelease)
|
|
assert.False(t, cfg.Draft)
|
|
assert.Nil(t, cfg.Extended)
|
|
})
|
|
|
|
t.Run("creates config with extended", func(t *testing.T) {
|
|
ext := map[string]any{"key": "value"}
|
|
cfg := NewPublisherConfig("docker", false, false, ext)
|
|
assert.Equal(t, "docker", cfg.Type)
|
|
assert.Equal(t, ext, cfg.Extended)
|
|
})
|
|
}
|
|
|
|
func TestGitHub_BuildCreateArgs_Good(t *testing.T) {
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("basic args", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "## v1.0.0\n\nChanges",
|
|
FS: io.Local,
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "release")
|
|
assert.Contains(t, args, "create")
|
|
assert.Contains(t, args, "v1.0.0")
|
|
assert.Contains(t, args, "--repo")
|
|
assert.Contains(t, args, "owner/repo")
|
|
assert.Contains(t, args, "--title")
|
|
assert.Contains(t, args, "--notes")
|
|
})
|
|
|
|
t.Run("with draft flag", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
FS: io.Local,
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: true,
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "--draft")
|
|
})
|
|
|
|
t.Run("with prerelease flag", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
FS: io.Local,
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Prerelease: true,
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "--prerelease")
|
|
})
|
|
|
|
t.Run("generates notes when no changelog", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "",
|
|
FS: io.Local,
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "--generate-notes")
|
|
})
|
|
|
|
t.Run("with draft and prerelease flags", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0-alpha",
|
|
FS: io.Local,
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: true,
|
|
Prerelease: true,
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "owner/repo")
|
|
|
|
assert.Contains(t, args, "--draft")
|
|
assert.Contains(t, args, "--prerelease")
|
|
})
|
|
|
|
t.Run("without repo includes version", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v2.0.0",
|
|
Changelog: "Some changes",
|
|
FS: io.Local,
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
}
|
|
|
|
args := p.buildCreateArgs(release, cfg, "")
|
|
|
|
assert.Contains(t, args, "release")
|
|
assert.Contains(t, args, "create")
|
|
assert.Contains(t, args, "v2.0.0")
|
|
assert.NotContains(t, args, "--repo")
|
|
})
|
|
}
|
|
|
|
func TestGitHub_GitHubPublisherDryRunPublish_Good(t *testing.T) {
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("outputs expected dry run information", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "## Changes\n\n- Feature A\n- Bug fix B",
|
|
ProjectDir: "/project",
|
|
FS: io.Local,
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: false,
|
|
Prerelease: false,
|
|
}
|
|
|
|
var err error
|
|
output := capturePublisherOutput(t, func() {
|
|
err = p.dryRunPublish(release, cfg, "owner/repo")
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, output, "DRY RUN: GitHub Release")
|
|
assert.Contains(t, output, "Repository: owner/repo")
|
|
assert.Contains(t, output, "Version: v1.0.0")
|
|
assert.Contains(t, output, "Draft: false")
|
|
assert.Contains(t, output, "Prerelease: false")
|
|
assert.Contains(t, output, "Would create release with command:")
|
|
assert.Contains(t, output, "gh release create")
|
|
assert.Contains(t, output, "Changelog:")
|
|
assert.Contains(t, output, "## Changes")
|
|
assert.Contains(t, output, "END DRY RUN")
|
|
})
|
|
|
|
t.Run("shows artifacts when present", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: "/project",
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/dist/myapp-darwin-amd64.tar.gz"},
|
|
{Path: "/dist/myapp-linux-amd64.tar.gz"},
|
|
},
|
|
}
|
|
cfg := PublisherConfig{Type: "github"}
|
|
|
|
var err error
|
|
output := capturePublisherOutput(t, func() {
|
|
err = p.dryRunPublish(release, cfg, "owner/repo")
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, output, "Would upload artifacts:")
|
|
assert.Contains(t, output, "myapp-darwin-amd64.tar.gz")
|
|
assert.Contains(t, output, "myapp-linux-amd64.tar.gz")
|
|
})
|
|
|
|
t.Run("shows draft and prerelease flags", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0-beta",
|
|
Changelog: "Beta release",
|
|
ProjectDir: "/project",
|
|
FS: io.Local,
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: true,
|
|
Prerelease: true,
|
|
}
|
|
|
|
var err error
|
|
output := capturePublisherOutput(t, func() {
|
|
err = p.dryRunPublish(release, cfg, "owner/repo")
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, output, "Draft: true")
|
|
assert.Contains(t, output, "Prerelease: true")
|
|
assert.Contains(t, output, "--draft")
|
|
assert.Contains(t, output, "--prerelease")
|
|
})
|
|
}
|
|
|
|
func TestGitHub_GitHubPublisherPublish_Good(t *testing.T) {
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("dry run uses repository from config", func(t *testing.T) {
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: "/tmp",
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
relCfg := &mockReleaseConfig{repository: "custom/repo"}
|
|
|
|
// Dry run should succeed without needing gh CLI
|
|
var err error
|
|
output := capturePublisherOutput(t, func() {
|
|
err = p.Publish(context.TODO(), release, pubCfg, relCfg, true)
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Contains(t, output, "Repository: custom/repo")
|
|
})
|
|
}
|
|
|
|
func TestGitHub_GitHubPublisherPublish_Bad(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("fails when gh CLI not available and not dry run", func(t *testing.T) {
|
|
// This test will fail if gh is installed but not authenticated
|
|
// or succeed if gh is not installed
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: "/nonexistent",
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, false)
|
|
|
|
// Should fail due to either gh not found or not authenticated
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("fails when repository cannot be detected", func(t *testing.T) {
|
|
// Create a temp directory that is NOT a git repo
|
|
tmpDir := t.TempDir()
|
|
|
|
release := &Release{
|
|
Version: "v1.0.0",
|
|
Changelog: "Changes",
|
|
ProjectDir: tmpDir,
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{Type: "github"}
|
|
relCfg := &mockReleaseConfig{repository: ""} // Empty repository
|
|
|
|
err := p.Publish(context.Background(), release, pubCfg, relCfg, true)
|
|
|
|
// Should fail because detectRepository will fail on non-git dir
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "could not determine repository")
|
|
})
|
|
}
|
|
|
|
func TestGitHub_DetectRepository_Good(t *testing.T) {
|
|
t.Run("detects repository from git remote", func(t *testing.T) {
|
|
// Create a temp git repo
|
|
tmpDir := t.TempDir()
|
|
|
|
// Initialize git repo and set remote
|
|
runPublisherCommand(t, tmpDir, "git", "init")
|
|
runPublisherCommand(t, tmpDir, "git", "remote", "add", "origin", "git@github.com:test-owner/test-repo.git")
|
|
|
|
repo, err := detectRepository(context.Background(), tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test-owner/test-repo", repo)
|
|
})
|
|
|
|
t.Run("detects repository from HTTPS remote", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
runPublisherCommand(t, tmpDir, "git", "init")
|
|
runPublisherCommand(t, tmpDir, "git", "remote", "add", "origin", "https://github.com/another-owner/another-repo.git")
|
|
|
|
repo, err := detectRepository(context.Background(), tmpDir)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "another-owner/another-repo", repo)
|
|
})
|
|
}
|
|
|
|
func TestGitHub_DetectRepository_Bad(t *testing.T) {
|
|
t.Run("fails when not a git repository", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
_, err := detectRepository(context.Background(), tmpDir)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to get git remote")
|
|
})
|
|
|
|
t.Run("fails when directory does not exist", func(t *testing.T) {
|
|
_, err := detectRepository(context.Background(), "/nonexistent/directory/that/does/not/exist")
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("fails when remote is not GitHub", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
runPublisherCommand(t, tmpDir, "git", "init")
|
|
runPublisherCommand(t, tmpDir, "git", "remote", "add", "origin", "git@gitlab.com:owner/repo.git")
|
|
|
|
_, err := detectRepository(context.Background(), tmpDir)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not a GitHub URL")
|
|
})
|
|
|
|
t.Run("respects cancelled context", func(t *testing.T) {
|
|
commandDir := t.TempDir()
|
|
commandPath := ax.Join(commandDir, "git")
|
|
require.NoError(t, ax.WriteFile(commandPath, []byte("#!/bin/sh\nexit 0\n"), 0o755))
|
|
t.Setenv("PATH", commandDir)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := detectRepository(ctx, t.TempDir())
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "context canceled")
|
|
})
|
|
}
|
|
|
|
func TestGitHub_ValidateGhCli_Bad(t *testing.T) {
|
|
// This test verifies the error messages from validateGhCli
|
|
// We can't easily mock exec.Command, but we can at least
|
|
// verify the function exists and returns expected error types
|
|
t.Run("returns error when gh not installed", func(t *testing.T) {
|
|
// We can't force gh to not be installed, but we can verify
|
|
// the function signature works correctly
|
|
err := validateGhCli(context.Background())
|
|
if err != nil {
|
|
// Either gh is not installed or not authenticated
|
|
assert.True(t,
|
|
core.Contains(err.Error(), "gh CLI not found") ||
|
|
core.Contains(err.Error(), "not authenticated"),
|
|
"unexpected error: %s", err.Error())
|
|
}
|
|
// If err is nil, gh is installed and authenticated - that's OK too
|
|
})
|
|
|
|
t.Run("respects cancelled context during auth check", func(t *testing.T) {
|
|
commandDir := t.TempDir()
|
|
commandPath := ax.Join(commandDir, "gh")
|
|
require.NoError(t, ax.WriteFile(commandPath, []byte("#!/bin/sh\necho 'Logged in'\n"), 0o755))
|
|
t.Setenv("PATH", commandDir)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
err := validateGhCli(ctx)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "context canceled")
|
|
})
|
|
}
|
|
|
|
func TestGitHub_ResolveGhCli_Good(t *testing.T) {
|
|
fallbackDir := t.TempDir()
|
|
fallbackPath := ax.Join(fallbackDir, "gh")
|
|
require.NoError(t, ax.WriteFile(fallbackPath, []byte("#!/bin/sh\nexit 0\n"), 0o755))
|
|
t.Setenv("PATH", "")
|
|
|
|
command, err := resolveGhCli(fallbackPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, fallbackPath, command)
|
|
}
|
|
|
|
func TestGitHub_ResolveGhCli_Bad(t *testing.T) {
|
|
t.Setenv("PATH", "")
|
|
_, err := resolveGhCli(ax.Join(t.TempDir(), "missing-gh"))
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "gh CLI not found")
|
|
}
|
|
|
|
func TestGitHub_GitHubPublisherExecutePublish_Good(t *testing.T) {
|
|
// These tests run only when gh CLI is available and authenticated
|
|
if err := validateGhCli(context.Background()); err != nil {
|
|
t.Skip("skipping test: gh CLI not available or not authenticated")
|
|
}
|
|
|
|
p := NewGitHubPublisher()
|
|
|
|
t.Run("executePublish builds command with artifacts", func(t *testing.T) {
|
|
// We test the command building by checking that it fails appropriately
|
|
// with a non-existent release (rather than testing actual release creation)
|
|
release := &Release{
|
|
Version: "v999.999.999-test-nonexistent",
|
|
Changelog: "Test changelog",
|
|
ProjectDir: "/tmp",
|
|
FS: io.Local,
|
|
Artifacts: []build.Artifact{
|
|
{Path: "/tmp/nonexistent-artifact.tar.gz"},
|
|
},
|
|
}
|
|
cfg := PublisherConfig{
|
|
Type: "github",
|
|
Draft: true,
|
|
Prerelease: true,
|
|
}
|
|
|
|
// This will fail because the artifact doesn't exist, but it proves
|
|
// the code path runs
|
|
command, err := resolveGhCli()
|
|
require.NoError(t, err)
|
|
|
|
err = p.executePublish(context.Background(), release, cfg, "test-owner/test-repo-nonexistent", command)
|
|
assert.Error(t, err) // Expected to fail
|
|
})
|
|
}
|
|
|
|
func TestGitHub_ReleaseExists_Good(t *testing.T) {
|
|
// These tests run only when gh CLI is available
|
|
if err := validateGhCli(context.Background()); err != nil {
|
|
t.Skip("skipping test: gh CLI not available or not authenticated")
|
|
}
|
|
|
|
t.Run("returns false for non-existent release", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
// Use a non-existent repo and version
|
|
exists := ReleaseExists(ctx, "nonexistent-owner-12345/nonexistent-repo-67890", "v999.999.999")
|
|
assert.False(t, exists)
|
|
})
|
|
|
|
t.Run("checks release existence", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
// Test against a known public repository with releases
|
|
// This tests the true path if the release exists
|
|
exists := ReleaseExists(ctx, "cli/cli", "v2.0.0")
|
|
// We don't assert the result since it depends on network access
|
|
// and the release may or may not exist
|
|
_ = exists // Just verify function runs without panic
|
|
})
|
|
}
|