Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions internal/handlers/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,6 @@ func createTestDescriptors() DescriptorMap {
}
}




func TestNewAssetHandler_WithFlags(t *testing.T) {
runner := &mockAssetRunner{supportsReader: true}
desc := createTestDescriptors()
Expand Down Expand Up @@ -258,7 +255,6 @@ func TestAssetHandler_Get_WithSupport(t *testing.T) {
assert.Equal(t, "resources", cmd.Use)
}


func TestAssetHandler_Describe_ExactArgsSet(t *testing.T) {
runner := &mockAssetRunner{
supportsReader: true,
Expand Down Expand Up @@ -335,7 +331,6 @@ func TestAssetHandler_Clear_WithSupport(t *testing.T) {
assert.NotNil(t, cmd)
}


func TestAssetHandler_Edit_WithSupport(t *testing.T) {
runner := &mockAssetRunner{
supportsEditor: true,
Expand Down Expand Up @@ -480,7 +475,6 @@ func TestAssetHandler_Load_WithSupport(t *testing.T) {
assert.NotNil(t, cmd)
}


func TestAssetHandler_WithCustomFlags(t *testing.T) {
runner := &mockAssetRunner{
supportsReader: true,
Expand Down
14 changes: 7 additions & 7 deletions internal/handlers/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ func TestNewRegistry_EmptyHandlers(t *testing.T) {

func TestNewRegistry_SingleInterface(t *testing.T) {
tests := []struct {
name string
handler any
checkFn func(*testing.T, *Registry)
name string
handler any
checkFn func(*testing.T, *Registry)
}{
{
name: "Reader interface",
Expand Down Expand Up @@ -305,10 +305,10 @@ func TestNewRegistry_NonHandlerTypes(t *testing.T) {
// Test that non-handler types are safely ignored
handlers := []any{
&mockReader{name: "valid"},
"string", // Not a handler
42, // Not a handler
nil, // Nil value
struct{}{}, // Empty struct
"string", // Not a handler
42, // Not a handler
nil, // Nil value
struct{}{}, // Empty struct
}

registry := NewRegistry(handlers)
Expand Down
24 changes: 12 additions & 12 deletions internal/handlers/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import (
// mockClient implements client.Client for testing
type mockClient struct{}

func (m *mockClient) Get(*client.Request) (*client.Response, error) { return nil, nil }
func (m *mockClient) Post(*client.Request) (*client.Response, error) { return nil, nil }
func (m *mockClient) Put(*client.Request) (*client.Response, error) { return nil, nil }
func (m *mockClient) Get(*client.Request) (*client.Response, error) { return nil, nil }
func (m *mockClient) Post(*client.Request) (*client.Response, error) { return nil, nil }
func (m *mockClient) Put(*client.Request) (*client.Response, error) { return nil, nil }
func (m *mockClient) Delete(*client.Request) (*client.Response, error) { return nil, nil }
func (m *mockClient) Patch(*client.Request) (*client.Response, error) { return nil, nil }
func (m *mockClient) Trace(*client.Request) (*client.Response, error) { return nil, nil }
func (m *mockClient) Patch(*client.Request) (*client.Response, error) { return nil, nil }
func (m *mockClient) Trace(*client.Request) (*client.Response, error) { return nil, nil }

// mockConfig implements config.Provider for testing
type mockConfig struct {
Expand All @@ -48,13 +48,13 @@ func (m *mockConfig) GetRepository(name string) (*repository.Repository, error)
return &repository.Repository{Url: name}, nil
}

func (m *mockConfig) GetWorkingDir() string { return m.workingDir }
func (m *mockConfig) GetDefaultProfile() string { return m.defaultProfile }
func (m *mockConfig) GetDefaultRepository() string { return m.defaultRepository }
func (m *mockConfig) IsDatasetsEnabled() bool { return m.datasetsEnabled }
func (m *mockConfig) GetGitName() string { return m.gitName }
func (m *mockConfig) GetGitEmail() string { return m.gitEmail }
func (m *mockConfig) GetGitUser() string { return m.gitUser }
func (m *mockConfig) GetWorkingDir() string { return m.workingDir }
func (m *mockConfig) GetDefaultProfile() string { return m.defaultProfile }
func (m *mockConfig) GetDefaultRepository() string { return m.defaultRepository }
func (m *mockConfig) IsDatasetsEnabled() bool { return m.datasetsEnabled }
func (m *mockConfig) GetGitName() string { return m.gitName }
func (m *mockConfig) GetGitEmail() string { return m.gitEmail }
func (m *mockConfig) GetGitUser() string { return m.gitUser }

func TestNewRuntime(t *testing.T) {
tests := []struct {
Expand Down
2 changes: 1 addition & 1 deletion internal/runners/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ func (r *GroupRunner) importGroup(in services.Group, replace bool) error {
existing, err := r.resource.GetByName(in.Name)

if err != nil {
if err.Error() != "group does not exist" {
if !errors.Is(err, resources.ErrNotFound) {
return err
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/runners/jsonforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func (r *JsonFormRunner) Create(in Request) (*Response, error) {
return nil, err
}
} else if err != nil {
if err.Error() != "jsonform not found" {
if !errors.Is(err, resources.ErrNotFound) {
return nil, err
}
}
Expand Down
2 changes: 1 addition & 1 deletion internal/runners/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (r *ModelRunner) Create(in Request) (*Response, error) {
return nil, err
}
} else if err != nil {
if err.Error() != "model not found" {
if !errors.Is(err, resources.ErrNotFound) {
return nil, err
}
}
Expand Down
4 changes: 2 additions & 2 deletions internal/runners/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (r *TemplateRunner) Create(in Request) (*Response, error) {
return nil, err
}
} else if err != nil {
if err.Error() != "template not found" {
if !errors.Is(err, resources.ErrNotFound) {
return nil, err
}
}
Expand Down Expand Up @@ -413,7 +413,7 @@ func (r TemplateRunner) importTemplate(in services.Template, replace bool) error

p, err := r.resource.GetByName(in.Name)
if err != nil {
if err.Error() != "template not found" {
if !errors.Is(err, resources.ErrNotFound) {
return err
}
}
Expand Down
68 changes: 68 additions & 0 deletions internal/runners/templates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2024 Itential Inc. All Rights Reserved
// Unauthorized copying of this file, via any medium is strictly prohibited
// Proprietary and confidential

package runners

import (
"testing"

"github.com/itential/ipctl/internal/flags"
"github.com/itential/ipctl/internal/testlib"
"github.com/stretchr/testify/assert"
)

var (
templatesGetAllEmpty = testlib.Fixture("testdata/templates/getall_empty.json")
templatesImportResponse = testlib.Fixture("testdata/templates/import_response.json")
)

// TestImportTemplateReplaceNotExists covers the bug where importing with --replace
// failed with "item with name '...' not found" when the template did not yet exist
// on the server. The fix uses errors.Is(err, ErrNotFound) so a missing template is
// treated as a no-op and the import proceeds.
func TestImportTemplateReplaceNotExists(t *testing.T) {
runner := NewTemplateRunner(
testlib.Setup(),
testlib.DefaultConfig(),
)
defer testlib.Teardown()

testlib.AddGetResponseToMux("/automation-studio/templates", templatesGetAllEmpty, 0)
testlib.AddPostResponseToMux("/automation-studio/templates/import", templatesImportResponse, 200)

res, err := runner.Import(Request{
Args: []string{"testdata/templates/template.json"},
Common: &flags.AssetImportCommon{
Replace: true,
},
})

assert.NoError(t, err)
assert.NotNil(t, res)
assert.Contains(t, res.Text, "test-template")
}

// TestImportTemplateNoReplace verifies a plain import (no --replace) succeeds
// when the template does not exist on the server.
func TestImportTemplateNoReplace(t *testing.T) {
runner := NewTemplateRunner(
testlib.Setup(),
testlib.DefaultConfig(),
)
defer testlib.Teardown()

testlib.AddGetResponseToMux("/automation-studio/templates", templatesGetAllEmpty, 0)
testlib.AddPostResponseToMux("/automation-studio/templates/import", templatesImportResponse, 200)

res, err := runner.Import(Request{
Args: []string{"testdata/templates/template.json"},
Common: &flags.AssetImportCommon{
Replace: false,
},
})

assert.NoError(t, err)
assert.NotNil(t, res)
assert.Contains(t, res.Text, "test-template")
}
7 changes: 7 additions & 0 deletions internal/runners/testdata/templates/getall_empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"items": [],
"total": 0,
"count": 0,
"limit": 100,
"skip": 0
}
16 changes: 16 additions & 0 deletions internal/runners/testdata/templates/import_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"imported": [
{
"success": true,
"message": "Template imported successfully",
"original": {
"_id": "abc123",
"name": "test-template",
"description": "A test template",
"type": "jinja2",
"group": "Imported",
"template": "Hello {{ name }}"
}
}
]
}
8 changes: 8 additions & 0 deletions internal/runners/testdata/templates/template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"_id": "abc123",
"name": "test-template",
"description": "A test template",
"type": "jinja2",
"group": "Imported",
"template": "Hello {{ name }}"
}
2 changes: 1 addition & 1 deletion pkg/resources/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (r *AccountResource) GetByName(name string) (*services.Account, error) {
return nil, err
}

return FindByName(accounts, name, func(a services.Account) string {
return FindByName(accounts, "account", name, func(a services.Account) string {
return a.Username
})
}
23 changes: 21 additions & 2 deletions pkg/resources/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ import (
"fmt"
)

// ErrNotFound is the sentinel error returned when a resource cannot be found by name.
// Callers should use errors.Is(err, resources.ErrNotFound) to check for this condition.
var ErrNotFound = errors.New("not found")

// NotFoundError is returned by FindByName when no item matches the given name.
type NotFoundError struct {
Kind string
Name string
}

func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s '%s' not found", e.Kind, e.Name)
}

func (e *NotFoundError) Is(target error) bool {
return target == ErrNotFound
}

// BaseResource provides common functionality for resource operations.
type BaseResource struct{}

Expand All @@ -19,13 +37,14 @@ func NewBaseResource() BaseResource {

// FindByName is a generic helper that searches through a slice of items
// and returns the first item where the getName function returns a matching name.
func FindByName[T any](items []T, name string, getName func(T) string) (*T, error) {
// Returns a *NotFoundError (which satisfies errors.Is(err, ErrNotFound)) when not found.
func FindByName[T any](items []T, kind, name string, getName func(T) string) (*T, error) {
for i := range items {
if getName(items[i]) == name {
return &items[i], nil
}
}
return nil, fmt.Errorf("item with name '%s' not found", name)
return nil, &NotFoundError{Kind: kind, Name: name}
}

// DeleteAll is a generic helper that deletes all items using the provided delete function.
Expand Down
44 changes: 44 additions & 0 deletions pkg/resources/base_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2024 Itential Inc. All Rights Reserved
// Unauthorized copying of this file, via any medium is strictly prohibited
// Proprietary and confidential

package resources

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
)

func TestFindByNameFound(t *testing.T) {
items := []string{"alpha", "beta", "gamma"}
result, err := FindByName(items, "item", "beta", func(s string) string { return s })
assert.NoError(t, err)
assert.Equal(t, "beta", *result)
}

func TestFindByNameNotFound(t *testing.T) {
items := []string{"alpha", "beta"}
result, err := FindByName(items, "item", "missing", func(s string) string { return s })
assert.Nil(t, result)
assert.Error(t, err)
}

func TestFindByNameNotFoundErrorIs(t *testing.T) {
items := []string{"alpha"}
_, err := FindByName(items, "template", "missing", func(s string) string { return s })
assert.True(t, errors.Is(err, ErrNotFound), "expected errors.Is(err, ErrNotFound) to be true")
}

func TestFindByNameNotFoundErrorMessage(t *testing.T) {
items := []string{"alpha"}
_, err := FindByName(items, "template", "my-template", func(s string) string { return s })
assert.EqualError(t, err, "template 'my-template' not found")
}

func TestFindByNameEmpty(t *testing.T) {
var items []string
_, err := FindByName(items, "template", "any", func(s string) string { return s })
assert.True(t, errors.Is(err, ErrNotFound))
}
2 changes: 1 addition & 1 deletion pkg/resources/device_groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (r *DeviceGroupResource) GetByName(name string) (*services.DeviceGroup, err
return nil, err
}

return FindByName(groups, name, func(g services.DeviceGroup) string {
return FindByName(groups, "device group", name, func(g services.DeviceGroup) string {
return g.Name
})
}
2 changes: 1 addition & 1 deletion pkg/resources/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func (r *GroupResource) GetByName(name string) (*services.Group, error) {
return nil, err
}

return FindByName(groups, name, func(g services.Group) string {
return FindByName(groups, "group", name, func(g services.Group) string {
return g.Name
})
}
2 changes: 1 addition & 1 deletion pkg/resources/jsonforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (r *JsonFormResource) GetByName(name string) (*services.JsonForm, error) {
return nil, err
}

return FindByName(forms, name, func(f services.JsonForm) string {
return FindByName(forms, "json form", name, func(f services.JsonForm) string {
return f.Name
})
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/resources/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (r *ModelResource) GetByName(name string) (*services.Model, error) {
return nil, err
}

return FindByName(models, name, func(m services.Model) string {
return FindByName(models, "model", name, func(m services.Model) string {
return m.Name
})
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/resources/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (r *ProjectResource) GetByName(name string) (*services.Project, error) {
return nil, err
}

return FindByName(projects, name, func(p services.Project) string {
return FindByName(projects, "project", name, func(p services.Project) string {
return p.Name
})
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/resources/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (r *TemplateResource) GetByName(name string) (*services.Template, error) {
return nil, err
}

return FindByName(templates, name, func(t services.Template) string {
return FindByName(templates, "template", name, func(t services.Template) string {
return t.Name
})
}
Expand Down
Loading