diff --git a/pkg/onepassword/client/connect/connect_test.go b/pkg/onepassword/client/connect/connect_test.go index 557e9ff..ef21b90 100644 --- a/pkg/onepassword/client/connect/connect_test.go +++ b/pkg/onepassword/client/connect/connect_test.go @@ -7,14 +7,15 @@ import ( "github.com/stretchr/testify/require" "github.com/1Password/connect-sdk-go/onepassword" - "github.com/1Password/onepassword-operator/pkg/onepassword/client/mock" + clienttesting "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing" + "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing/mock" "github.com/1Password/onepassword-operator/pkg/onepassword/model" ) const VaultTitleEmployee = "Employee" func TestConnect_GetItemByID(t *testing.T) { - connectItem := createItem() + connectItem := clienttesting.CreateConnectItem() testCases := map[string]struct { mockClient func() *mock.ConnectClientMock @@ -28,7 +29,7 @@ func TestConnect_GetItemByID(t *testing.T) { }, check: func(t *testing.T, item *model.Item, err error) { require.NoError(t, err) - checkItem(t, connectItem, item) + clienttesting.CheckConnectItemMapping(t, connectItem, item) }, }, "should return an error": { @@ -54,8 +55,8 @@ func TestConnect_GetItemByID(t *testing.T) { } func TestConnect_GetItemsByTitle(t *testing.T) { - connectItem1 := createItem() - connectItem2 := createItem() + connectItem1 := clienttesting.CreateConnectItem() + connectItem2 := clienttesting.CreateConnectItem() testCases := map[string]struct { mockClient func() *mock.ConnectClientMock @@ -89,8 +90,8 @@ func TestConnect_GetItemsByTitle(t *testing.T) { check: func(t *testing.T, items []model.Item, err error) { require.NoError(t, err) require.Len(t, items, 2) - checkItem(t, connectItem1, &items[0]) - checkItem(t, connectItem2, &items[1]) + clienttesting.CheckConnectItemMapping(t, connectItem1, &items[0]) + clienttesting.CheckConnectItemMapping(t, connectItem2, &items[1]) }, }, "should return an error": { @@ -228,42 +229,3 @@ func TestConnect_GetVaultsByTitle(t *testing.T) { }) } } - -func createItem() *onepassword.Item { - return &onepassword.Item{ - ID: "test-id", - Vault: onepassword.ItemVault{ID: "test-vault-id"}, - Version: 1, - Tags: []string{"tag1", "tag2"}, - Fields: []*onepassword.ItemField{ - {Label: "label1", Value: "value1"}, - {Label: "label2", Value: "value2"}, - }, - Files: []*onepassword.File{ - {ID: "file-id-1", Name: "file1.txt", Size: 1234}, - {ID: "file-id-2", Name: "file2.txt", Size: 1234}, - }, - } -} - -func checkItem(t *testing.T, expected *onepassword.Item, actual *model.Item) { - t.Helper() - - require.Equal(t, expected.ID, actual.ID) - require.Equal(t, expected.Vault.ID, actual.VaultID) - require.Equal(t, expected.Version, actual.Version) - require.ElementsMatch(t, expected.Tags, actual.Tags) - - for i, field := range expected.Fields { - require.Equal(t, field.Label, actual.Fields[i].Label) - require.Equal(t, field.Value, actual.Fields[i].Value) - } - - for i, file := range expected.Files { - require.Equal(t, file.ID, actual.Files[i].ID) - require.Equal(t, file.Name, actual.Files[i].Name) - require.Equal(t, file.Size, actual.Files[i].Size) - } - - require.Equal(t, expected.CreatedAt, actual.CreatedAt) -} diff --git a/pkg/onepassword/client/sdk/sdk.go b/pkg/onepassword/client/sdk/sdk.go new file mode 100644 index 0000000..eafd687 --- /dev/null +++ b/pkg/onepassword/client/sdk/sdk.go @@ -0,0 +1,91 @@ +package sdk + +import ( + "context" + + "github.com/1Password/onepassword-operator/pkg/onepassword/model" + sdk "github.com/1password/onepassword-sdk-go" +) + +// Config holds the configuration for the 1Password SDK client. +type Config struct { + ServiceAccountToken string + IntegrationName string + IntegrationVersion string +} + +// SDK is a client for interacting with 1Password using the SDK. +type SDK struct { + client *sdk.Client +} + +func NewClient(config Config) (*SDK, error) { + client, err := sdk.NewClient(context.Background(), + sdk.WithServiceAccountToken(config.ServiceAccountToken), + sdk.WithIntegrationInfo(config.IntegrationName, config.IntegrationVersion), + ) + if err != nil { + return nil, err + } + + return &SDK{ + client: client, + }, nil +} + +func (s *SDK) GetItemByID(vaultID, itemID string) (*model.Item, error) { + sdkItem, err := s.client.Items().Get(context.Background(), vaultID, itemID) + if err != nil { + return nil, err + } + + var item model.Item + item.FromSDKItem(&sdkItem) + return &item, nil +} + +func (s *SDK) GetItemsByTitle(vaultID, itemTitle string) ([]model.Item, error) { + // Get all items in the vault + sdkItems, err := s.client.Items().List(context.Background(), vaultID) + if err != nil { + return nil, err + } + + // Filter items by title + var items []model.Item + for _, sdkItem := range sdkItems { + if sdkItem.Title == itemTitle { + var item model.Item + item.FromSDKItemOverview(&sdkItem) + items = append(items, item) + } + } + + return items, nil +} + +func (s *SDK) GetFileContent(vaultID, itemID, fileID string) ([]byte, error) { + return s.client.Items().Files().Read(context.Background(), vaultID, itemID, sdk.FileAttributes{ + ID: fileID, + }) +} + +func (s *SDK) GetVaultsByTitle(title string) ([]model.Vault, error) { + // List all vaults + sdkVaults, err := s.client.Vaults().List(context.Background()) + if err != nil { + return nil, err + } + + // Filter vaults by title + var vaults []model.Vault + for _, sdkVault := range sdkVaults { + if sdkVault.Title == title { + var vault model.Vault + vault.FromSDKVault(&sdkVault) + vaults = append(vaults, vault) + } + } + + return vaults, nil +} diff --git a/pkg/onepassword/client/sdk/sdk_test.go b/pkg/onepassword/client/sdk/sdk_test.go new file mode 100644 index 0000000..5455c5b --- /dev/null +++ b/pkg/onepassword/client/sdk/sdk_test.go @@ -0,0 +1,269 @@ +package sdk + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + clienttesting "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing" + clientmock "github.com/1Password/onepassword-operator/pkg/onepassword/client/testing/mock" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" + sdk "github.com/1password/onepassword-sdk-go" +) + +const VaultTitleEmployee = "Employee" + +func TestSDK_GetItemByID(t *testing.T) { + sdkItem := clienttesting.CreateSDKItem() + + testCases := map[string]struct { + mockItemAPI func() *clientmock.ItemAPIMock + check func(t *testing.T, item *model.Item, err error) + }{ + "should return a single vault": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + m.On("Get", context.Background(), "vault-id", "item-id").Return(*sdkItem, nil) + return m + }, + check: func(t *testing.T, item *model.Item, err error) { + require.NoError(t, err) + clienttesting.CheckSDKItemMapping(t, sdkItem, item) + }, + }, + "should return an error": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + m.On("Get", context.Background(), "vault-id", "item-id").Return(sdk.Item{}, errors.New("error")) + return m + }, + check: func(t *testing.T, item *model.Item, err error) { + require.Error(t, err) + require.Empty(t, item) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &SDK{ + client: &sdk.Client{ + ItemsAPI: tc.mockItemAPI(), + }, + } + item, err := client.GetItemByID("vault-id", "item-id") + tc.check(t, item, err) + }) + } +} + +func TestSDK_GetItemsByTitle(t *testing.T) { + sdkItem1 := clienttesting.CreateSDKItemOverview() + sdkItem2 := clienttesting.CreateSDKItemOverview() + + testCases := map[string]struct { + mockItemAPI func() *clientmock.ItemAPIMock + check func(t *testing.T, items []model.Item, err error) + }{ + "should return a single item": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + + copySDKItem2 := *sdkItem2 + copySDKItem2.Title = "Some other item" + + m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{ + *sdkItem1, + copySDKItem2, + }, nil) + return m + }, + check: func(t *testing.T, items []model.Item, err error) { + require.NoError(t, err) + require.Len(t, items, 1) + clienttesting.CheckSDKItemOverviewMapping(t, sdkItem1, &items[0]) + }, + }, + "should return a two items": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{ + *sdkItem1, + *sdkItem2, + }, nil) + return m + }, + check: func(t *testing.T, items []model.Item, err error) { + require.NoError(t, err) + require.Len(t, items, 2) + clienttesting.CheckSDKItemOverviewMapping(t, sdkItem1, &items[0]) + clienttesting.CheckSDKItemOverviewMapping(t, sdkItem2, &items[1]) + }, + }, + "should return an error": { + mockItemAPI: func() *clientmock.ItemAPIMock { + m := &clientmock.ItemAPIMock{} + m.On("List", context.Background(), "vault-id", mock.Anything).Return([]sdk.ItemOverview{}, errors.New("error")) + return m + }, + check: func(t *testing.T, items []model.Item, err error) { + require.Error(t, err) + require.Empty(t, items) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &SDK{ + client: &sdk.Client{ + ItemsAPI: tc.mockItemAPI(), + }, + } + items, err := client.GetItemsByTitle("vault-id", "item-title") + tc.check(t, items, err) + }) + } +} + +func TestSDK_GetFileContent(t *testing.T) { + testCases := map[string]struct { + mockItemAPI func() *clientmock.ItemAPIMock + check func(t *testing.T, content []byte, err error) + }{ + "should return file content": { + mockItemAPI: func() *clientmock.ItemAPIMock { + fileMock := &clientmock.FileAPIMock{} + fileMock.On("Read", mock.Anything, "vault-id", "item-id", + mock.MatchedBy(func(attr sdk.FileAttributes) bool { + return attr.ID == "file-id" + }), + ).Return([]byte("file content"), nil) + + itemMock := &clientmock.ItemAPIMock{ + FilesAPI: fileMock, + } + itemMock.On("Files").Return(fileMock) + + return itemMock + }, + check: func(t *testing.T, content []byte, err error) { + require.NoError(t, err) + require.Equal(t, []byte("file content"), content) + }, + }, + "should return an error": { + mockItemAPI: func() *clientmock.ItemAPIMock { + fileMock := &clientmock.FileAPIMock{} + fileMock.On("Read", mock.Anything, "vault-id", "item-id", + mock.MatchedBy(func(attr sdk.FileAttributes) bool { + return attr.ID == "file-id" + }), + ).Return(nil, errors.New("error")) + + itemMock := &clientmock.ItemAPIMock{ + FilesAPI: fileMock, + } + itemMock.On("Files").Return(fileMock) + + return itemMock + }, + check: func(t *testing.T, content []byte, err error) { + require.Error(t, err) + require.Nil(t, content) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &SDK{ + client: &sdk.Client{ + ItemsAPI: tc.mockItemAPI(), + }, + } + content, err := client.GetFileContent("vault-id", "item-id", "file-id") + tc.check(t, content, err) + }) + } +} + +// TODO: check CreatedAt as soon as a new SDK version returns it +func TestSDK_GetVaultsByTitle(t *testing.T) { + testCases := map[string]struct { + mockVaultAPI func() *clientmock.VaultAPIMock + check func(t *testing.T, vaults []model.Vault, err error) + }{ + "should return a single vault": { + mockVaultAPI: func() *clientmock.VaultAPIMock { + m := &clientmock.VaultAPIMock{} + m.On("List", context.Background()).Return([]sdk.VaultOverview{ + { + ID: "test-id", + Title: VaultTitleEmployee, + }, + { + ID: "test-id-2", + Title: "Some other vault", + }, + }, nil) + return m + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.NoError(t, err) + require.Len(t, vaults, 1) + require.Equal(t, "test-id", vaults[0].ID) + }, + }, + "should return a two vaults": { + mockVaultAPI: func() *clientmock.VaultAPIMock { + m := &clientmock.VaultAPIMock{} + m.On("List", context.Background()).Return([]sdk.VaultOverview{ + { + ID: "test-id", + Title: VaultTitleEmployee, + }, + { + ID: "test-id-2", + Title: VaultTitleEmployee, + }, + }, nil) + return m + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.NoError(t, err) + require.Len(t, vaults, 2) + // Check the first vault + require.Equal(t, "test-id", vaults[0].ID) + // Check the second vault + require.Equal(t, "test-id-2", vaults[1].ID) + }, + }, + "should return an error": { + mockVaultAPI: func() *clientmock.VaultAPIMock { + m := &clientmock.VaultAPIMock{} + m.On("List", context.Background()).Return([]sdk.VaultOverview{}, errors.New("error")) + return m + }, + check: func(t *testing.T, vaults []model.Vault, err error) { + require.Error(t, err) + require.Empty(t, vaults) + }, + }, + } + + for description, tc := range testCases { + t.Run(description, func(t *testing.T) { + client := &SDK{ + client: &sdk.Client{ + VaultsAPI: tc.mockVaultAPI(), + }, + } + vault, err := client.GetVaultsByTitle(VaultTitleEmployee) + tc.check(t, vault, err) + }) + } +} diff --git a/pkg/onepassword/client/testing/item.go b/pkg/onepassword/client/testing/item.go new file mode 100644 index 0000000..a44dd77 --- /dev/null +++ b/pkg/onepassword/client/testing/item.go @@ -0,0 +1,110 @@ +package testing + +import ( + sdk "github.com/1password/onepassword-sdk-go" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/1Password/connect-sdk-go/onepassword" + "github.com/1Password/onepassword-operator/pkg/onepassword/model" +) + +func CreateConnectItem() *onepassword.Item { + return &onepassword.Item{ + ID: "test-id", + Vault: onepassword.ItemVault{ID: "test-vault-id"}, + Version: 1, + Tags: []string{"tag1", "tag2"}, + Fields: []*onepassword.ItemField{ + {Label: "label1", Value: "value1"}, + {Label: "label2", Value: "value2"}, + }, + Files: []*onepassword.File{ + {ID: "file-id-1", Name: "file1.txt", Size: 1234}, + {ID: "file-id-2", Name: "file2.txt", Size: 1234}, + }, + } +} + +func CreateSDKItem() *sdk.Item { + return &sdk.Item{ + ID: "test-id", + VaultID: "test-vault-id", + Version: 1, + Tags: []string{"tag1", "tag2"}, + Fields: []sdk.ItemField{ + {Title: "label1", Value: "value1"}, + {Title: "label2", Value: "value2"}, + }, + Files: []sdk.ItemFile{ + {Attributes: sdk.FileAttributes{ID: "file-id-1", Name: "file1.txt", Size: 1234}}, + {Attributes: sdk.FileAttributes{ID: "file-id-2", Name: "file2.txt", Size: 1234}}, + }, + CreatedAt: time.Now(), + } +} + +func CreateSDKItemOverview() *sdk.ItemOverview { + return &sdk.ItemOverview{ + ID: "test-id", + Title: "item-title", + VaultID: "test-vault-id", + Tags: []string{"tag1", "tag2"}, + CreatedAt: time.Now(), + } +} + +func CheckConnectItemMapping(t *testing.T, expected *onepassword.Item, actual *model.Item) { + t.Helper() + + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.Vault.ID, actual.VaultID) + require.Equal(t, expected.Version, actual.Version) + require.ElementsMatch(t, expected.Tags, actual.Tags) + + for i, field := range expected.Fields { + require.Equal(t, field.Label, actual.Fields[i].Label) + require.Equal(t, field.Value, actual.Fields[i].Value) + } + + for i, file := range expected.Files { + require.Equal(t, file.ID, actual.Files[i].ID) + require.Equal(t, file.Name, actual.Files[i].Name) + require.Equal(t, file.Size, actual.Files[i].Size) + } + + require.Equal(t, expected.CreatedAt, actual.CreatedAt) +} + +func CheckSDKItemMapping(t *testing.T, expected *sdk.Item, actual *model.Item) { + t.Helper() + + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.VaultID, actual.VaultID) + require.Equal(t, int(expected.Version), actual.Version) + require.ElementsMatch(t, expected.Tags, actual.Tags) + + for i, field := range expected.Fields { + require.Equal(t, field.Title, actual.Fields[i].Label) + require.Equal(t, field.Value, actual.Fields[i].Value) + } + + for i, file := range expected.Files { + require.Equal(t, file.Attributes.ID, actual.Files[i].ID) + require.Equal(t, file.Attributes.Name, actual.Files[i].Name) + require.Equal(t, int(file.Attributes.Size), actual.Files[i].Size) + } + + require.Equal(t, expected.CreatedAt, actual.CreatedAt) +} + +func CheckSDKItemOverviewMapping(t *testing.T, expected *sdk.ItemOverview, actual *model.Item) { + t.Helper() + + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.VaultID, actual.VaultID) + require.ElementsMatch(t, expected.Tags, actual.Tags) + require.Equal(t, expected.CreatedAt, actual.CreatedAt) +} diff --git a/pkg/onepassword/client/mock/connect.go b/pkg/onepassword/client/testing/mock/connect.go similarity index 100% rename from pkg/onepassword/client/mock/connect.go rename to pkg/onepassword/client/testing/mock/connect.go diff --git a/pkg/onepassword/client/testing/mock/sdk.go b/pkg/onepassword/client/testing/mock/sdk.go new file mode 100644 index 0000000..5940dfd --- /dev/null +++ b/pkg/onepassword/client/testing/mock/sdk.go @@ -0,0 +1,89 @@ +package mock + +import ( + "context" + + "github.com/stretchr/testify/mock" + + sdk "github.com/1password/onepassword-sdk-go" +) + +type VaultAPIMock struct { + mock.Mock +} + +func (v *VaultAPIMock) List(ctx context.Context) ([]sdk.VaultOverview, error) { + args := v.Called(ctx) + return args.Get(0).([]sdk.VaultOverview), args.Error(1) +} + +type ItemAPIMock struct { + mock.Mock + FilesAPI sdk.ItemsFilesAPI +} + +func (i *ItemAPIMock) Create(ctx context.Context, params sdk.ItemCreateParams) (sdk.Item, error) { + //TODO implement me + panic("implement me") +} + +func (i *ItemAPIMock) Get(ctx context.Context, vaultID string, itemID string) (sdk.Item, error) { + args := i.Called(ctx, vaultID, itemID) + return args.Get(0).(sdk.Item), args.Error(1) +} + +func (i *ItemAPIMock) Put(ctx context.Context, item sdk.Item) (sdk.Item, error) { + //TODO implement me + panic("implement me") +} + +func (i *ItemAPIMock) Delete(ctx context.Context, vaultID string, itemID string) error { + //TODO implement me + panic("implement me") +} + +func (i *ItemAPIMock) Archive(ctx context.Context, vaultID string, itemID string) error { + //TODO implement me + panic("implement me") +} + +func (i *ItemAPIMock) List(ctx context.Context, vaultID string, filters ...sdk.ItemListFilter) ([]sdk.ItemOverview, error) { + args := i.Called(ctx, vaultID, filters) + return args.Get(0).([]sdk.ItemOverview), args.Error(1) +} + +func (i *ItemAPIMock) Shares() sdk.ItemsSharesAPI { + //TODO implement me + panic("implement me") +} + +func (i *ItemAPIMock) Files() sdk.ItemsFilesAPI { + return i.FilesAPI +} + +type FileAPIMock struct { + mock.Mock +} + +func (f *FileAPIMock) Attach(ctx context.Context, item sdk.Item, fileParams sdk.FileCreateParams) (sdk.Item, error) { + //TODO implement me + panic("implement me") +} + +func (f *FileAPIMock) Delete(ctx context.Context, item sdk.Item, sectionID string, fieldID string) (sdk.Item, error) { + //TODO implement me + panic("implement me") +} + +func (f *FileAPIMock) ReplaceDocument(ctx context.Context, item sdk.Item, docParams sdk.DocumentCreateParams) (sdk.Item, error) { + //TODO implement me + panic("implement me") +} + +func (f *FileAPIMock) Read(ctx context.Context, vaultID, itemID string, attributes sdk.FileAttributes) ([]byte, error) { + args := f.Called(ctx, vaultID, itemID, attributes) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).([]byte), args.Error(1) +}