* chore(io): migrate pkg/release to io.Medium abstraction Migrated `pkg/release` and its subpackages to use the `io.Medium` abstraction for filesystem operations. This enables better testability and support for alternative storage backends. Changes: - Added `FS io.Medium` field to `release.Release` and `publishers.Release` structs. - Updated `LoadConfig`, `ConfigExists`, and `WriteConfig` in `pkg/release/config.go` to accept `io.Medium`. - Updated `Publish`, `Run`, `findArtifacts`, and `buildArtifacts` in `pkg/release/release.go` to use `io.Medium`. - Migrated all publishers (`aur`, `chocolatey`, `docker`, `github`, `homebrew`, `linuxkit`, `npm`, `scoop`) to use `io.Medium` for file operations. - Implemented custom template overrides in publishers by checking for templates in `.core/templates/<publisher>/` via `io.Medium`. - Updated all relevant tests to provide `io.Medium`. * chore(io): fix missing callers in pkg/release migration Updated callers of `release` package functions that had their signatures changed during the `io.Medium` migration. Fixed files: - `internal/cmd/ci/cmd_init.go` - `internal/cmd/ci/cmd_publish.go` - `pkg/build/buildcmd/cmd_release.go` These changes ensure the project compiles successfully by providing `io.Local` to `LoadConfig`, `WriteConfig`, and `ConfigExists`. * chore(io): fix build errors in pkg/release migration Fixed compilation errors by updating all callers of `release.LoadConfig`, `release.ConfigExists`, and `release.WriteConfig` to provide the required `io.Medium` argument. Files updated: - `internal/cmd/ci/cmd_init.go` - `internal/cmd/ci/cmd_publish.go` - `pkg/build/buildcmd/cmd_release.go` These entry points now correctly pass `io.Local` to the `release` package functions.
303 lines
7.9 KiB
Go
303 lines
7.9 KiB
Go
package publishers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/host-uk/core/pkg/io"
|
|
|
|
"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(io.Local, "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(io.Local, "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(io.Local, "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(io.Local, 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(io.Local, 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",
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{Type: "npm"}
|
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
|
|
|
err := p.Publish(context.TODO(), 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",
|
|
FS: io.Local,
|
|
}
|
|
pubCfg := PublisherConfig{
|
|
Type: "npm",
|
|
Extended: map[string]any{
|
|
"package": "@test/package",
|
|
},
|
|
}
|
|
relCfg := &mockReleaseConfig{repository: "owner/repo"}
|
|
|
|
err := p.Publish(context.TODO(), 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)
|
|
})
|
|
}
|