mirror of
https://github.com/chenasraf/sofmani.git
synced 2026-05-17 17:28:04 +00:00
feat: validations
This commit is contained in:
@@ -21,6 +21,11 @@ const (
|
||||
PackageManagerApt PackageManager = "apt"
|
||||
)
|
||||
|
||||
func (i *AptInstaller) Validate() []ValidationError {
|
||||
errors := i.BaseValidate()
|
||||
return errors
|
||||
}
|
||||
|
||||
// Install implements IInstaller.
|
||||
func (i *AptInstaller) Install() error {
|
||||
name := *i.Info.Name
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package installer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/chenasraf/sofmani/appconfig"
|
||||
"github.com/chenasraf/sofmani/utils"
|
||||
)
|
||||
@@ -15,6 +17,18 @@ type BrewOpts struct {
|
||||
Tap *string
|
||||
}
|
||||
|
||||
func (i *BrewInstaller) Validate() []ValidationError {
|
||||
errors := i.BaseValidate()
|
||||
info := i.GetData()
|
||||
opts := i.GetOpts()
|
||||
if opts.Tap != nil {
|
||||
if !strings.Contains(*opts.Tap, "/") || len(*opts.Tap) < 3 {
|
||||
errors = append(errors, ValidationError{FieldName: "tap", Message: validationInvalidFormat(), InstallerName: *info.Name})
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// Install implements IInstaller.
|
||||
func (i *BrewInstaller) Install() error {
|
||||
name := *i.Info.Name
|
||||
|
||||
@@ -21,6 +21,19 @@ type GitOpts struct {
|
||||
Ref *string
|
||||
}
|
||||
|
||||
func (i *GitInstaller) Validate() []ValidationError {
|
||||
errors := i.BaseValidate()
|
||||
info := i.GetData()
|
||||
opts := i.GetOpts()
|
||||
if opts.Destination == nil || len(*opts.Destination) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "destination", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
if opts.Ref == nil || len(*opts.Ref) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "ref", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// Install implements IInstaller.
|
||||
func (i *GitInstaller) Install() error {
|
||||
args := []string{"clone", i.GetRepositoryUrl(), i.GetInstallDir()}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
type GitHubReleaseInstaller struct {
|
||||
InstallerBase
|
||||
Config *appconfig.AppConfig
|
||||
Data *appconfig.InstallerData
|
||||
Info *appconfig.InstallerData
|
||||
}
|
||||
|
||||
type GitHubReleaseOpts struct {
|
||||
@@ -37,6 +37,31 @@ const (
|
||||
GitHubReleaseInstallStrategyZip GitHubReleaseInstallStrategy = "zip"
|
||||
)
|
||||
|
||||
func (i *GitHubReleaseInstaller) Validate() []ValidationError {
|
||||
errors := i.BaseValidate()
|
||||
info := i.GetData()
|
||||
opts := i.GetOpts()
|
||||
if opts.Repository == nil || len(*opts.Repository) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "repository", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
if opts.Destination == nil || len(*opts.Destination) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "destination", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
if opts.DownloadFilename == nil || len(*opts.DownloadFilename.Resolve()) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "download_filename", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
} else {
|
||||
if (*opts.DownloadFilename).Resolve() == nil || len(*(*opts.DownloadFilename).Resolve()) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: fmt.Sprintf("download_filename.%s", platform.GetPlatform()), Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
}
|
||||
if opts.Strategy != nil {
|
||||
if *opts.Strategy != GitHubReleaseInstallStrategyNone && *opts.Strategy != GitHubReleaseInstallStrategyTar && *opts.Strategy != GitHubReleaseInstallStrategyZip {
|
||||
errors = append(errors, ValidationError{FieldName: "strategy", Message: validationInvalidFormat(), InstallerName: *info.Name})
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// Install implements IInstaller.
|
||||
func (i *GitHubReleaseInstaller) Install() error {
|
||||
opts := i.GetOpts()
|
||||
@@ -60,10 +85,7 @@ func (i *GitHubReleaseInstaller) Install() error {
|
||||
return err
|
||||
}
|
||||
|
||||
version := tag
|
||||
if strings.HasPrefix(tag, "v") {
|
||||
version = strings.TrimPrefix(tag, "v")
|
||||
}
|
||||
version, _ := strings.CutPrefix(tag, "v")
|
||||
filename := i.GetFilename()
|
||||
filename = strings.ReplaceAll(filename, "{tag}", tag)
|
||||
filename = strings.ReplaceAll(filename, "{version}", version)
|
||||
@@ -152,8 +174,8 @@ func (i *GitHubReleaseInstaller) CheckIsInstalled() (bool, error) {
|
||||
if i.HasCustomInstallCheck() {
|
||||
return i.RunCustomInstallCheck()
|
||||
}
|
||||
logger.Debug("Checking if %s is installed on %s", *i.Data.Name, filepath.Join(i.GetInstallDir(), *i.Data.Name))
|
||||
return utils.PathExists(filepath.Join(i.GetInstallDir(), *i.Data.Name))
|
||||
logger.Debug("Checking if %s is installed on %s", *i.Info.Name, filepath.Join(i.GetInstallDir(), *i.Info.Name))
|
||||
return utils.PathExists(filepath.Join(i.GetInstallDir(), *i.Info.Name))
|
||||
}
|
||||
|
||||
// CheckNeedsUpdate implements IInstaller.
|
||||
@@ -179,10 +201,10 @@ func (i *GitHubReleaseInstaller) CheckNeedsUpdate() (bool, error) {
|
||||
}
|
||||
|
||||
func (i *GitHubReleaseInstaller) GetBinName() string {
|
||||
if i.Data.BinName != nil {
|
||||
return *i.Data.BinName
|
||||
if i.Info.BinName != nil {
|
||||
return *i.Info.BinName
|
||||
}
|
||||
return filepath.Base(*i.Data.Name)
|
||||
return filepath.Base(*i.Info.Name)
|
||||
}
|
||||
|
||||
func (i *GitHubReleaseInstaller) CopyExtractedFile(out *os.File, tmpDir string) (bool, error) {
|
||||
@@ -205,12 +227,12 @@ func (i *GitHubReleaseInstaller) CopyExtractedFile(out *os.File, tmpDir string)
|
||||
}
|
||||
|
||||
func (i *GitHubReleaseInstaller) GetCachedTag() (string, error) {
|
||||
logger.Debug("Getting cached tag for %s", *i.Data.Name)
|
||||
logger.Debug("Getting cached tag for %s", *i.Info.Name)
|
||||
cacheDir, err := utils.GetCacheDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cacheFile := fmt.Sprintf("%s/%s", cacheDir, *i.Data.Name)
|
||||
cacheFile := fmt.Sprintf("%s/%s", cacheDir, *i.Info.Name)
|
||||
exists, err := utils.PathExists(cacheFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -223,7 +245,7 @@ func (i *GitHubReleaseInstaller) GetCachedTag() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logger.Debug("Got cached tag %s for %s", strings.TrimSpace(string(contents)), *i.Data.Name)
|
||||
logger.Debug("Got cached tag %s for %s", strings.TrimSpace(string(contents)), *i.Info.Name)
|
||||
return strings.TrimSpace(string(contents)), nil
|
||||
}
|
||||
|
||||
@@ -232,7 +254,7 @@ func (i *GitHubReleaseInstaller) UpdateCache(tag string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cacheFile := fmt.Sprintf("%s/%s", cacheDir, *i.Data.Name)
|
||||
cacheFile := fmt.Sprintf("%s/%s", cacheDir, *i.Info.Name)
|
||||
logger.Debug("Updating cache file %s with %s", cacheFile, tag)
|
||||
err = os.WriteFile(cacheFile, []byte(tag), 0644)
|
||||
if err != nil {
|
||||
@@ -243,12 +265,12 @@ func (i *GitHubReleaseInstaller) UpdateCache(tag string) error {
|
||||
|
||||
// GetData implements IInstaller.
|
||||
func (i *GitHubReleaseInstaller) GetData() *appconfig.InstallerData {
|
||||
return i.Data
|
||||
return i.Info
|
||||
}
|
||||
|
||||
func (i *GitHubReleaseInstaller) GetOpts() *GitHubReleaseOpts {
|
||||
opts := &GitHubReleaseOpts{}
|
||||
info := i.Data
|
||||
info := i.Info
|
||||
if info.Opts != nil {
|
||||
if repository, ok := (*info.Opts)["repository"].(string); ok {
|
||||
repository = utils.GetRealPath(i.GetData().Environ(), repository)
|
||||
@@ -328,7 +350,7 @@ func NewGitHubReleaseInstaller(cfg *appconfig.AppConfig, installer *appconfig.In
|
||||
i := &GitHubReleaseInstaller{
|
||||
InstallerBase: InstallerBase{Data: installer},
|
||||
Config: cfg,
|
||||
Data: installer,
|
||||
Info: installer,
|
||||
}
|
||||
|
||||
return i
|
||||
|
||||
@@ -16,6 +16,15 @@ type GroupOpts struct {
|
||||
//
|
||||
}
|
||||
|
||||
func (i *GroupInstaller) Validate() []ValidationError {
|
||||
errors := i.BaseValidate()
|
||||
info := i.GetData()
|
||||
if info.Steps == nil || len(*info.Steps) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "steps", Message: "Must have at least one step", InstallerName: *info.Name})
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// Install implements IInstaller.
|
||||
func (i *GroupInstaller) Install() error {
|
||||
info := i.GetData()
|
||||
|
||||
@@ -15,6 +15,7 @@ type IInstaller interface {
|
||||
CheckNeedsUpdate() (bool, error)
|
||||
Install() error
|
||||
Update() error
|
||||
Validate() []ValidationError
|
||||
}
|
||||
|
||||
type InstallerBase struct {
|
||||
@@ -52,6 +53,15 @@ func (i *InstallerBase) GetData() *appconfig.InstallerData {
|
||||
return i.Data
|
||||
}
|
||||
|
||||
func (i *InstallerBase) BaseValidate() []ValidationError {
|
||||
errors := []ValidationError{}
|
||||
info := i.GetData()
|
||||
if info.Name == nil || len(*info.Name) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "name", Message: "Name is required"})
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
func (i *InstallerBase) RunCustomUpdateCheck() (bool, error) {
|
||||
envShell := utils.GetOSShell(i.GetData().EnvShell)
|
||||
args := utils.GetOSShellArgs(*i.GetData().CheckHasUpdate)
|
||||
|
||||
@@ -5,17 +5,19 @@ import (
|
||||
|
||||
"github.com/chenasraf/sofmani/appconfig"
|
||||
"github.com/chenasraf/sofmani/logger"
|
||||
"github.com/chenasraf/sofmani/platform"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type MockInstaller struct {
|
||||
data *appconfig.InstallerData
|
||||
isInstalled bool
|
||||
needsUpdate bool
|
||||
installError error
|
||||
updateError error
|
||||
checkInstall error
|
||||
checkUpdate error
|
||||
data *appconfig.InstallerData
|
||||
isInstalled bool
|
||||
needsUpdate bool
|
||||
installError error
|
||||
updateError error
|
||||
checkInstall error
|
||||
checkUpdate error
|
||||
validationErrors []ValidationError
|
||||
}
|
||||
|
||||
func (m *MockInstaller) GetData() *appconfig.InstallerData {
|
||||
@@ -38,6 +40,10 @@ func (m *MockInstaller) Update() error {
|
||||
return m.updateError
|
||||
}
|
||||
|
||||
func (m *MockInstaller) Validate() []ValidationError {
|
||||
return m.validationErrors
|
||||
}
|
||||
|
||||
func TestGetInstaller(t *testing.T) {
|
||||
config := &appconfig.AppConfig{}
|
||||
logger.InitLogger(false)
|
||||
@@ -69,6 +75,492 @@ func TestRunInstaller(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAptValidation(t *testing.T) {
|
||||
logger.InitLogger(false)
|
||||
aptInstaller := &AptInstaller{
|
||||
InstallerBase: InstallerBase{
|
||||
Data: &appconfig.InstallerData{
|
||||
Name: strPtr("test-apt"),
|
||||
Type: appconfig.InstallerTypeApt,
|
||||
},
|
||||
},
|
||||
}
|
||||
errors := aptInstaller.Validate()
|
||||
assert.Empty(t, errors)
|
||||
}
|
||||
|
||||
func newTestBrewInstaller(data *appconfig.InstallerData) *BrewInstaller {
|
||||
return &BrewInstaller{
|
||||
InstallerBase: InstallerBase{
|
||||
Data: data,
|
||||
},
|
||||
Info: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrewValidation(t *testing.T) {
|
||||
logger.InitLogger(false)
|
||||
|
||||
// 🟢 Valid: No tap specified (tap is optional)
|
||||
emptyData := &appconfig.InstallerData{
|
||||
Name: strPtr("test-brew-valid"),
|
||||
Type: appconfig.InstallerTypeBrew,
|
||||
}
|
||||
assert.Empty(t, newTestBrewInstaller(emptyData).Validate())
|
||||
|
||||
// 🟢 Valid: Well-formed tap (contains slash, sufficient length)
|
||||
validData := &appconfig.InstallerData{
|
||||
Name: strPtr("test-brew-valid-tap"),
|
||||
Type: appconfig.InstallerTypeBrew,
|
||||
Opts: &map[string]any{"tap": "valid/tap"},
|
||||
}
|
||||
assert.Empty(t, newTestBrewInstaller(validData).Validate())
|
||||
|
||||
// 🔴 Invalid: Tap is present but malformed (missing slash or too short)
|
||||
invalidData := &appconfig.InstallerData{
|
||||
Name: strPtr("test-brew-invalid-tap"),
|
||||
Type: appconfig.InstallerTypeBrew,
|
||||
Opts: &map[string]any{"tap": "invalid-tap"},
|
||||
}
|
||||
assert.NotEmpty(t, newTestBrewInstaller(invalidData).Validate())
|
||||
}
|
||||
|
||||
func newTestGitInstaller(data *appconfig.InstallerData) *GitInstaller {
|
||||
return &GitInstaller{
|
||||
InstallerBase: InstallerBase{
|
||||
Data: data,
|
||||
},
|
||||
Info: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitValidation(t *testing.T) {
|
||||
logger.InitLogger(false)
|
||||
|
||||
// 🟢 Valid: Both destination and ref are present
|
||||
validData := &appconfig.InstallerData{
|
||||
Name: strPtr("test-git-valid"),
|
||||
Type: appconfig.InstallerTypeGit,
|
||||
Opts: &map[string]any{
|
||||
"destination": "/some/path",
|
||||
"ref": "main",
|
||||
},
|
||||
}
|
||||
errors := newTestGitInstaller(validData).Validate()
|
||||
assert.Empty(t, errors)
|
||||
|
||||
// 🔴 Invalid: Missing ref
|
||||
missingRefData := &appconfig.InstallerData{
|
||||
Name: strPtr("test-git-missing-ref"),
|
||||
Type: appconfig.InstallerTypeGit,
|
||||
Opts: &map[string]any{
|
||||
"destination": "/some/path",
|
||||
},
|
||||
}
|
||||
errors = newTestGitInstaller(missingRefData).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "ref", errors[0].FieldName)
|
||||
|
||||
// 🔴 Invalid: Missing destination
|
||||
missingDestData := &appconfig.InstallerData{
|
||||
Name: strPtr("test-git-missing-destination"),
|
||||
Type: appconfig.InstallerTypeGit,
|
||||
Opts: &map[string]any{
|
||||
"ref": "main",
|
||||
},
|
||||
}
|
||||
errors = newTestGitInstaller(missingDestData).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "destination", errors[0].FieldName)
|
||||
|
||||
// 🔴 Invalid: Missing both destination and ref
|
||||
missingBothData := &appconfig.InstallerData{
|
||||
Name: strPtr("test-git-missing-both"),
|
||||
Type: appconfig.InstallerTypeGit,
|
||||
Opts: &map[string]any{},
|
||||
}
|
||||
errors = newTestGitInstaller(missingBothData).Validate()
|
||||
assert.Len(t, errors, 2)
|
||||
assert.ElementsMatch(t, []string{"destination", "ref"}, []string{errors[0].FieldName, errors[1].FieldName})
|
||||
}
|
||||
|
||||
func newTestGitHubReleaseInstaller(data *appconfig.InstallerData) *GitHubReleaseInstaller {
|
||||
return &GitHubReleaseInstaller{
|
||||
InstallerBase: InstallerBase{
|
||||
Data: data,
|
||||
},
|
||||
Info: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubReleaseValidation(t *testing.T) {
|
||||
logger.InitLogger(false)
|
||||
|
||||
// 🟢 Valid
|
||||
validData := &appconfig.InstallerData{
|
||||
Name: strPtr("ghr-valid"),
|
||||
Type: appconfig.InstallerTypeGitHubRelease,
|
||||
Opts: &map[string]any{
|
||||
"repository": "owner/repo",
|
||||
"destination": "/some/path",
|
||||
"download_filename": "file.tar.gz", // valid string
|
||||
"strategy": "tar",
|
||||
},
|
||||
}
|
||||
assert.Empty(t, newTestGitHubReleaseInstaller(validData).Validate())
|
||||
|
||||
// 🔴 Missing repository
|
||||
missingRepo := &appconfig.InstallerData{
|
||||
Name: strPtr("ghr-missing-repo"),
|
||||
Type: appconfig.InstallerTypeGitHubRelease,
|
||||
Opts: &map[string]any{
|
||||
"destination": "/some/path",
|
||||
"download_filename": "file.tar.gz",
|
||||
},
|
||||
}
|
||||
errors := newTestGitHubReleaseInstaller(missingRepo).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "repository", errors[0].FieldName)
|
||||
|
||||
// 🔴 Missing download_filename
|
||||
missingDownloadFilename := &appconfig.InstallerData{
|
||||
Name: strPtr("ghr-missing-download"),
|
||||
Type: appconfig.InstallerTypeGitHubRelease,
|
||||
Opts: &map[string]any{
|
||||
"repository": "owner/repo",
|
||||
"destination": "/some/path",
|
||||
},
|
||||
}
|
||||
errors = newTestGitHubReleaseInstaller(missingDownloadFilename).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "download_filename", errors[0].FieldName)
|
||||
|
||||
// 🔴 Empty per-platform download_filename
|
||||
emptyPlatformFilename := &appconfig.InstallerData{
|
||||
Name: strPtr("ghr-empty-platform-filename"),
|
||||
Type: appconfig.InstallerTypeGitHubRelease,
|
||||
Opts: &map[string]any{
|
||||
"repository": "owner/repo",
|
||||
"destination": "/some/path",
|
||||
"download_filename": map[string]*string{
|
||||
string(platform.GetPlatform()): strPtr(""),
|
||||
},
|
||||
},
|
||||
}
|
||||
errors = newTestGitHubReleaseInstaller(emptyPlatformFilename).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Contains(t, errors[0].FieldName, "download_filename")
|
||||
|
||||
// 🔴 Invalid strategy
|
||||
invalidStrategy := &appconfig.InstallerData{
|
||||
Name: strPtr("ghr-invalid-strategy"),
|
||||
Type: appconfig.InstallerTypeGitHubRelease,
|
||||
Opts: &map[string]any{
|
||||
"repository": "owner/repo",
|
||||
"destination": "/some/path",
|
||||
"download_filename": "file.tar.gz",
|
||||
"strategy": "exe", // invalid
|
||||
},
|
||||
}
|
||||
errors = newTestGitHubReleaseInstaller(invalidStrategy).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "strategy", errors[0].FieldName)
|
||||
}
|
||||
|
||||
func newTestGroupInstaller(data *appconfig.InstallerData) *GroupInstaller {
|
||||
return &GroupInstaller{
|
||||
InstallerBase: InstallerBase{
|
||||
Data: data,
|
||||
},
|
||||
Config: nil,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupValidation(t *testing.T) {
|
||||
logger.InitLogger(false)
|
||||
|
||||
// 🟢 Valid: one sub-installer
|
||||
validStep := appconfig.InstallerData{
|
||||
Name: strPtr("child-installer"),
|
||||
Type: appconfig.InstallerTypeBrew,
|
||||
}
|
||||
validData := &appconfig.InstallerData{
|
||||
Name: strPtr("group-valid"),
|
||||
Type: appconfig.InstallerTypeGroup,
|
||||
Steps: &[]appconfig.InstallerData{validStep},
|
||||
}
|
||||
assert.Empty(t, newTestGroupInstaller(validData).Validate())
|
||||
|
||||
// 🔴 Invalid: empty steps slice
|
||||
emptySteps := &appconfig.InstallerData{
|
||||
Name: strPtr("group-empty"),
|
||||
Type: appconfig.InstallerTypeGroup,
|
||||
Steps: &[]appconfig.InstallerData{},
|
||||
}
|
||||
errors := newTestGroupInstaller(emptySteps).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "steps", errors[0].FieldName)
|
||||
|
||||
// 🔴 Invalid: nil steps
|
||||
nilSteps := &appconfig.InstallerData{
|
||||
Name: strPtr("group-nil"),
|
||||
Type: appconfig.InstallerTypeGroup,
|
||||
Steps: nil,
|
||||
}
|
||||
errors = newTestGroupInstaller(nilSteps).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "steps", errors[0].FieldName)
|
||||
}
|
||||
|
||||
func newTestManifestInstaller(data *appconfig.InstallerData) *ManifestInstaller {
|
||||
return &ManifestInstaller{
|
||||
InstallerBase: InstallerBase{
|
||||
Data: data,
|
||||
},
|
||||
Config: nil,
|
||||
Info: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestManifestValidation(t *testing.T) {
|
||||
logger.InitLogger(false)
|
||||
|
||||
// 🟢 Valid
|
||||
validData := &appconfig.InstallerData{
|
||||
Name: strPtr("manifest-valid"),
|
||||
Type: appconfig.InstallerTypeManifest,
|
||||
Opts: &map[string]any{
|
||||
"source": "https://example.com/repo.git",
|
||||
"path": "manifests/installer.yml",
|
||||
"ref": "main",
|
||||
},
|
||||
}
|
||||
assert.Empty(t, newTestManifestInstaller(validData).Validate())
|
||||
|
||||
// 🔴 Missing source
|
||||
missingSource := &appconfig.InstallerData{
|
||||
Name: strPtr("manifest-missing-source"),
|
||||
Type: appconfig.InstallerTypeManifest,
|
||||
Opts: &map[string]any{
|
||||
"path": "some/path",
|
||||
},
|
||||
}
|
||||
errors := newTestManifestInstaller(missingSource).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "source", errors[0].FieldName)
|
||||
|
||||
// 🔴 Missing path
|
||||
missingPath := &appconfig.InstallerData{
|
||||
Name: strPtr("manifest-missing-path"),
|
||||
Type: appconfig.InstallerTypeManifest,
|
||||
Opts: &map[string]any{
|
||||
"source": "https://example.com/repo.git",
|
||||
},
|
||||
}
|
||||
errors = newTestManifestInstaller(missingPath).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "path", errors[0].FieldName)
|
||||
|
||||
// 🔴 Empty ref (not nil, just empty)
|
||||
emptyRef := &appconfig.InstallerData{
|
||||
Name: strPtr("manifest-empty-ref"),
|
||||
Type: appconfig.InstallerTypeManifest,
|
||||
Opts: &map[string]any{
|
||||
"source": "https://example.com/repo.git",
|
||||
"path": "install.yml",
|
||||
"ref": "",
|
||||
},
|
||||
}
|
||||
errors = newTestManifestInstaller(emptyRef).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "ref", errors[0].FieldName)
|
||||
}
|
||||
|
||||
func newTestNpmInstaller(data *appconfig.InstallerData) *NpmInstaller {
|
||||
return &NpmInstaller{
|
||||
InstallerBase: InstallerBase{
|
||||
Data: data,
|
||||
},
|
||||
Config: nil,
|
||||
PackageManager: PackageManagerNpm,
|
||||
Info: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestNpmValidation(t *testing.T) {
|
||||
logger.InitLogger(false)
|
||||
|
||||
// 🟢 Valid npm installer
|
||||
validData := &appconfig.InstallerData{
|
||||
Name: strPtr("some-npm-package"),
|
||||
Type: appconfig.InstallerTypeNpm,
|
||||
}
|
||||
assert.Empty(t, newTestNpmInstaller(validData).Validate())
|
||||
|
||||
// 🔴 Edge case: nil name (will panic or fail in BaseValidate if implemented to check it)
|
||||
nilNameData := &appconfig.InstallerData{
|
||||
Name: nil,
|
||||
Type: appconfig.InstallerTypeNpm,
|
||||
}
|
||||
errors := newTestNpmInstaller(nilNameData).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "name", errors[0].FieldName)
|
||||
}
|
||||
|
||||
func newTestPipxInstaller(data *appconfig.InstallerData) *PipxInstaller {
|
||||
return &PipxInstaller{
|
||||
InstallerBase: InstallerBase{
|
||||
Data: data,
|
||||
},
|
||||
Config: nil,
|
||||
Info: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipxValidation(t *testing.T) {
|
||||
logger.InitLogger(false)
|
||||
|
||||
// 🟢 Valid pipx installer
|
||||
validData := &appconfig.InstallerData{
|
||||
Name: strPtr("some-pipx-package"),
|
||||
Type: appconfig.InstallerTypePipx,
|
||||
}
|
||||
assert.Empty(t, newTestPipxInstaller(validData).Validate())
|
||||
|
||||
// 🔴 Optional: test nil name if BaseValidate handles it
|
||||
nilNameData := &appconfig.InstallerData{
|
||||
Name: nil,
|
||||
Type: appconfig.InstallerTypePipx,
|
||||
}
|
||||
errors := newTestPipxInstaller(nilNameData).Validate()
|
||||
// Uncomment if BaseValidate checks for nil
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "name", errors[0].FieldName)
|
||||
}
|
||||
|
||||
func newTestRsyncInstaller(data *appconfig.InstallerData) *RsyncInstaller {
|
||||
return &RsyncInstaller{
|
||||
InstallerBase: InstallerBase{
|
||||
Data: data,
|
||||
},
|
||||
Config: nil,
|
||||
Info: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestRsyncValidation(t *testing.T) {
|
||||
logger.InitLogger(false)
|
||||
|
||||
// 🟢 Valid rsync config
|
||||
validData := &appconfig.InstallerData{
|
||||
Name: strPtr("rsync-valid"),
|
||||
Type: appconfig.InstallerTypeRsync,
|
||||
Opts: &map[string]any{
|
||||
"source": "/path/from",
|
||||
"destination": "/path/to",
|
||||
"flags": "-avz",
|
||||
},
|
||||
}
|
||||
assert.Empty(t, newTestRsyncInstaller(validData).Validate())
|
||||
|
||||
// 🔴 Missing source
|
||||
missingSource := &appconfig.InstallerData{
|
||||
Name: strPtr("rsync-missing-source"),
|
||||
Type: appconfig.InstallerTypeRsync,
|
||||
Opts: &map[string]any{
|
||||
"destination": "/path/to",
|
||||
},
|
||||
}
|
||||
errors := newTestRsyncInstaller(missingSource).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "source", errors[0].FieldName)
|
||||
|
||||
// 🔴 Missing destination
|
||||
missingDest := &appconfig.InstallerData{
|
||||
Name: strPtr("rsync-missing-destination"),
|
||||
Type: appconfig.InstallerTypeRsync,
|
||||
Opts: &map[string]any{
|
||||
"source": "/path/from",
|
||||
},
|
||||
}
|
||||
errors = newTestRsyncInstaller(missingDest).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "destination", errors[0].FieldName)
|
||||
|
||||
// 🔴 Empty flags string
|
||||
emptyFlags := &appconfig.InstallerData{
|
||||
Name: strPtr("rsync-empty-flags"),
|
||||
Type: appconfig.InstallerTypeRsync,
|
||||
Opts: &map[string]any{
|
||||
"source": "/path/from",
|
||||
"destination": "/path/to",
|
||||
"flags": "",
|
||||
},
|
||||
}
|
||||
errors = newTestRsyncInstaller(emptyFlags).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "flags", errors[0].FieldName)
|
||||
}
|
||||
|
||||
func newTestShellInstaller(data *appconfig.InstallerData) *ShellInstaller {
|
||||
return &ShellInstaller{
|
||||
InstallerBase: InstallerBase{Data: data},
|
||||
Config: nil,
|
||||
Info: data,
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellValidation(t *testing.T) {
|
||||
logger.InitLogger(false)
|
||||
|
||||
// 🟢 Valid shell config
|
||||
validData := &appconfig.InstallerData{
|
||||
Name: strPtr("shell-valid"),
|
||||
Type: appconfig.InstallerTypeShell,
|
||||
Opts: &map[string]any{
|
||||
"command": "echo install",
|
||||
"update_command": "echo update",
|
||||
},
|
||||
}
|
||||
assert.Empty(t, newTestShellInstaller(validData).Validate())
|
||||
|
||||
// 🔴 Missing command
|
||||
missingCommand := &appconfig.InstallerData{
|
||||
Name: strPtr("shell-missing-command"),
|
||||
Type: appconfig.InstallerTypeShell,
|
||||
Opts: &map[string]any{
|
||||
"update_command": "echo update",
|
||||
},
|
||||
}
|
||||
errors := newTestShellInstaller(missingCommand).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "command", errors[0].FieldName)
|
||||
|
||||
// 🔴 Missing update_command
|
||||
missingUpdate := &appconfig.InstallerData{
|
||||
Name: strPtr("shell-missing-update"),
|
||||
Type: appconfig.InstallerTypeShell,
|
||||
Opts: &map[string]any{
|
||||
"command": "echo install",
|
||||
},
|
||||
}
|
||||
errors = newTestShellInstaller(missingUpdate).Validate()
|
||||
assert.Len(t, errors, 1)
|
||||
assert.Equal(t, "update_command", errors[0].FieldName)
|
||||
|
||||
// 🔴 Missing both
|
||||
missingBoth := &appconfig.InstallerData{
|
||||
Name: strPtr("shell-missing-both"),
|
||||
Type: appconfig.InstallerTypeShell,
|
||||
Opts: &map[string]any{},
|
||||
}
|
||||
errors = newTestShellInstaller(missingBoth).Validate()
|
||||
assert.Len(t, errors, 2)
|
||||
assert.ElementsMatch(t, []string{"command", "update_command"},
|
||||
[]string{errors[0].FieldName, errors[1].FieldName})
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
@@ -23,6 +23,22 @@ type ManifestOpts struct {
|
||||
Ref *string
|
||||
}
|
||||
|
||||
func (i *ManifestInstaller) Validate() []ValidationError {
|
||||
errors := i.BaseValidate()
|
||||
info := i.GetData()
|
||||
opts := i.GetOpts()
|
||||
if opts.Source == nil || len(*opts.Source) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "source", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
if opts.Path == nil || len(*opts.Path) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "path", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
if opts.Ref != nil && len(*opts.Ref) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "ref", Message: validationIsNotEmpty(), InstallerName: *info.Name})
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// Install implements IInstaller.
|
||||
func (i *ManifestInstaller) Install() error {
|
||||
logger.Debug("Getting manifest info...")
|
||||
|
||||
@@ -24,6 +24,11 @@ const (
|
||||
PackageManagerPnpm PackageManager = "pnpm"
|
||||
)
|
||||
|
||||
func (i *NpmInstaller) Validate() []ValidationError {
|
||||
errors := i.BaseValidate()
|
||||
return errors
|
||||
}
|
||||
|
||||
// Install implements IInstaller.
|
||||
func (i *NpmInstaller) Install() error {
|
||||
return i.RunCmdPassThrough(string(i.PackageManager), "install", "--global", *i.Info.Name)
|
||||
|
||||
@@ -15,6 +15,11 @@ type PipxOpts struct {
|
||||
//
|
||||
}
|
||||
|
||||
func (i *PipxInstaller) Validate() []ValidationError {
|
||||
errors := i.BaseValidate()
|
||||
return errors
|
||||
}
|
||||
|
||||
// Install implements IInstaller.
|
||||
func (i *PipxInstaller) Install() error {
|
||||
name := *i.Info.Name
|
||||
|
||||
@@ -20,6 +20,22 @@ type RsyncOpts struct {
|
||||
Flags *string
|
||||
}
|
||||
|
||||
func (i *RsyncInstaller) Validate() []ValidationError {
|
||||
errors := i.BaseValidate()
|
||||
info := i.GetData()
|
||||
opts := i.GetOpts()
|
||||
if opts.Source == nil || len(*opts.Source) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "source", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
if opts.Destination == nil || len(*opts.Destination) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "destination", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
if opts.Flags != nil && len(*opts.Flags) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "flags", Message: validationIsNotEmpty(), InstallerName: *info.Name})
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// Install implements IInstaller.
|
||||
func (i *RsyncInstaller) Install() error {
|
||||
defaultFlags := "-tr"
|
||||
|
||||
@@ -16,6 +16,19 @@ type ShellOpts struct {
|
||||
UpdateCommand *string
|
||||
}
|
||||
|
||||
func (i *ShellInstaller) Validate() []ValidationError {
|
||||
errors := i.BaseValidate()
|
||||
info := i.GetData()
|
||||
opts := i.GetOpts()
|
||||
if opts.Command == nil || len(*opts.Command) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "command", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
if opts.UpdateCommand == nil || len(*opts.UpdateCommand) == 0 {
|
||||
errors = append(errors, ValidationError{FieldName: "update_command", Message: validationIsRequired(), InstallerName: *info.Name})
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
// Install implements IInstaller.
|
||||
func (i *ShellInstaller) Install() error {
|
||||
return i.RunCmdAsFile(*i.GetOpts().Command)
|
||||
|
||||
25
installer/validation_error.go
Normal file
25
installer/validation_error.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package installer
|
||||
|
||||
import "fmt"
|
||||
|
||||
type ValidationError struct {
|
||||
FieldName string
|
||||
Message string
|
||||
InstallerName string
|
||||
}
|
||||
|
||||
func (v ValidationError) Error() string {
|
||||
return fmt.Sprintf("Validation Error in %s - Field '%s' is invalid: %s.", v.InstallerName, v.FieldName, v.Message)
|
||||
}
|
||||
|
||||
func validationIsRequired() string {
|
||||
return "Must be specified"
|
||||
}
|
||||
|
||||
func validationInvalidFormat() string {
|
||||
return "Invalid format"
|
||||
}
|
||||
|
||||
func validationIsNotEmpty() string {
|
||||
return "Cannot be empty"
|
||||
}
|
||||
26
main.go
26
main.go
@@ -37,6 +37,9 @@ func main() {
|
||||
}
|
||||
|
||||
logger.Info("Checking all installers...")
|
||||
instances := []installer.IInstaller{}
|
||||
hasValidationErrors := false
|
||||
|
||||
for _, i := range cfg.Install {
|
||||
installerInstance, err := installer.GetInstaller(cfg, &i)
|
||||
if err != nil {
|
||||
@@ -46,12 +49,27 @@ func main() {
|
||||
if installerInstance == nil {
|
||||
logger.Warn("Installer type %s is not supported, skipping", i.Type)
|
||||
} else {
|
||||
err = installer.RunInstaller(cfg, installerInstance)
|
||||
if err != nil {
|
||||
logger.Error("%s", err)
|
||||
os.Exit(1)
|
||||
errors := installerInstance.Validate()
|
||||
if len(errors) > 0 {
|
||||
hasValidationErrors = true
|
||||
for _, e := range errors {
|
||||
logger.Error(e.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasValidationErrors {
|
||||
logger.Error("Validation errors found, exiting. Please fix the errors and try again.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
for _, i := range instances {
|
||||
err = installer.RunInstaller(cfg, i)
|
||||
if err != nil {
|
||||
logger.Error("%s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
logger.Info("Complete")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package platform
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"slices"
|
||||
)
|
||||
|
||||
var osValue string = runtime.GOOS
|
||||
@@ -83,14 +84,8 @@ func (o *PlatformMap[T]) ResolveWithFallback(fallback PlatformMap[T]) T {
|
||||
}
|
||||
|
||||
func ContainsPlatform(platforms *[]Platform, platform Platform) bool {
|
||||
for _, p := range *platforms {
|
||||
if p == platform {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.Contains(*platforms, platform)
|
||||
}
|
||||
|
||||
func (p *Platforms) GetShouldRunOnOS(curOS Platform) bool {
|
||||
if p == nil {
|
||||
return true
|
||||
@@ -104,3 +99,21 @@ func (p *Platforms) GetShouldRunOnOS(curOS Platform) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func NewPlatformMap[T any](values map[string]T) *PlatformMap[T] {
|
||||
p := &PlatformMap[T]{}
|
||||
for k, v := range values {
|
||||
val := v // capture value for pointer
|
||||
switch Platform(k) {
|
||||
case PlatformMacos:
|
||||
p.MacOS = &val
|
||||
case PlatformLinux:
|
||||
p.Linux = &val
|
||||
case PlatformWindows:
|
||||
p.Windows = &val
|
||||
default:
|
||||
panic(fmt.Sprintf("Unsupported platform key: %q", k))
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user