diff --git a/internal/cli/commands/param/diff/diff.go b/internal/cli/commands/param/diff/diff.go index ba44895e..f7b36372 100644 --- a/internal/cli/commands/param/diff/diff.go +++ b/internal/cli/commands/param/diff/diff.go @@ -11,8 +11,8 @@ import ( "github.com/mpyw/suve/internal/cli/output" "github.com/mpyw/suve/internal/cli/pager" - "github.com/mpyw/suve/internal/infra" "github.com/mpyw/suve/internal/jsonutil" + awsparam "github.com/mpyw/suve/internal/provider/aws/param" "github.com/mpyw/suve/internal/usecase/param" "github.com/mpyw/suve/internal/version/paramversion" ) @@ -94,7 +94,7 @@ func action(ctx context.Context, cmd *cli.Command) error { return err } - client, err := infra.NewParamClient(ctx) + adapter, err := awsparam.NewAdapter(ctx) if err != nil { return fmt.Errorf("failed to initialize AWS client: %w", err) } @@ -112,7 +112,7 @@ func action(ctx context.Context, cmd *cli.Command) error { return pager.WithPagerWriter(cmd.Root().Writer, noPager, func(w io.Writer) error { r := &Runner{ - UseCase: ¶m.DiffUseCase{Client: client}, + UseCase: ¶m.DiffUseCase{Client: adapter}, Stdout: w, Stderr: cmd.Root().ErrWriter, } diff --git a/internal/cli/commands/param/diff/diff_test.go b/internal/cli/commands/param/diff/diff_test.go index 8fe3aea5..463a6a74 100644 --- a/internal/cli/commands/param/diff/diff_test.go +++ b/internal/cli/commands/param/diff/diff_test.go @@ -11,16 +11,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mpyw/suve/internal/api/paramapi" appcli "github.com/mpyw/suve/internal/cli/commands" paramdiff "github.com/mpyw/suve/internal/cli/commands/param/diff" "github.com/mpyw/suve/internal/cli/diffargs" + "github.com/mpyw/suve/internal/model" "github.com/mpyw/suve/internal/usecase/param" "github.com/mpyw/suve/internal/version/paramversion" ) -const testParamVersion1 = "/app/param:1" - func TestCommand_Validation(t *testing.T) { t.Parallel() @@ -377,31 +375,31 @@ func assertSpec(t *testing.T, label string, got *paramversion.Spec, want *wantSp assert.Equal(t, want.shift, got.Shift, "%s.Shift", label) } -//nolint:lll // mock struct fields match AWS SDK interface signatures type mockClient struct { - getParameterFunc func(ctx context.Context, params *paramapi.GetParameterInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) - getParameterHistoryFunc func(ctx context.Context, params *paramapi.GetParameterHistoryInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) + getParameterFunc func(ctx context.Context, name string, version string) (*model.Parameter, error) + getParameterHistoryFunc func(ctx context.Context, name string) (*model.ParameterHistory, error) } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockClient) GetParameter(ctx context.Context, params *paramapi.GetParameterInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { +func (m *mockClient) GetParameter(ctx context.Context, name string, version string) (*model.Parameter, error) { if m.getParameterFunc != nil { - return m.getParameterFunc(ctx, params, optFns...) + return m.getParameterFunc(ctx, name, version) } return nil, fmt.Errorf("GetParameter not mocked") } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockClient) GetParameterHistory(ctx context.Context, params *paramapi.GetParameterHistoryInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { +func (m *mockClient) GetParameterHistory(ctx context.Context, name string) (*model.ParameterHistory, error) { if m.getParameterHistoryFunc != nil { - return m.getParameterHistoryFunc(ctx, params, optFns...) + return m.getParameterHistoryFunc(ctx, name) } return nil, fmt.Errorf("GetParameterHistory not mocked") } -//nolint:funlen // Table-driven test with many cases +func (m *mockClient) ListParameters(_ context.Context, _ string, _ bool) ([]*model.ParameterListItem, error) { + return nil, fmt.Errorf("ListParameters not mocked") +} + func TestRun(t *testing.T) { t.Parallel() @@ -421,29 +419,21 @@ func TestRun(t *testing.T) { Spec2: ¶mversion.Spec{Name: "/app/param", Absolute: paramversion.AbsoluteSpec{Version: lo.ToPtr(int64(2))}}, }, mock: &mockClient{ - //nolint:lll // inline mock function in test table - getParameterFunc: func(_ context.Context, params *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - name := lo.FromPtr(params.Name) - if name == testParamVersion1 { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr("old-value"), - Version: 1, - Type: paramapi.ParameterTypeString, - LastModifiedDate: lo.ToPtr(now.Add(-time.Hour)), - }, + getParameterFunc: func(_ context.Context, _ string, version string) (*model.Parameter, error) { + if version == "1" { + return &model.Parameter{ + Name: "/app/param", + Value: "old-value", + Version: "1", + UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), }, nil } - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr("new-value"), - Version: 2, - Type: paramapi.ParameterTypeString, - LastModifiedDate: &now, - }, + return &model.Parameter{ + Name: "/app/param", + Value: "new-value", + Version: "2", + UpdatedAt: &now, }, nil }, }, @@ -460,14 +450,11 @@ func TestRun(t *testing.T) { Spec2: ¶mversion.Spec{Name: "/app/param", Absolute: paramversion.AbsoluteSpec{Version: lo.ToPtr(int64(2))}}, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr("same-value"), - Version: 1, - Type: paramapi.ParameterTypeString, - }, + getParameterFunc: func(_ context.Context, _ string, _ string) (*model.Parameter, error) { + return &model.Parameter{ + Name: "/app/param", + Value: "same-value", + Version: "1", }, nil }, }, @@ -484,18 +471,15 @@ func TestRun(t *testing.T) { Spec2: ¶mversion.Spec{Name: "/app/param", Absolute: paramversion.AbsoluteSpec{Version: lo.ToPtr(int64(2))}}, }, mock: &mockClient{ - //nolint:lll // inline mock function in test table - getParameterFunc: func(_ context.Context, params *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - if lo.FromPtr(params.Name) == testParamVersion1 { + getParameterFunc: func(_ context.Context, _ string, version string) (*model.Parameter, error) { + if version == "1" { return nil, fmt.Errorf("version not found") } - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr("value"), - Version: 2, - }, + return &model.Parameter{ + Name: "/app/param", + Value: "value", + Version: "2", }, nil }, }, @@ -508,18 +492,15 @@ func TestRun(t *testing.T) { Spec2: ¶mversion.Spec{Name: "/app/param", Absolute: paramversion.AbsoluteSpec{Version: lo.ToPtr(int64(2))}}, }, mock: &mockClient{ - //nolint:lll // inline mock function in test table - getParameterFunc: func(_ context.Context, params *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - if lo.FromPtr(params.Name) == "/app/param:2" { + getParameterFunc: func(_ context.Context, _ string, version string) (*model.Parameter, error) { + if version == "2" { return nil, fmt.Errorf("version not found") } - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr("value"), - Version: 1, - }, + return &model.Parameter{ + Name: "/app/param", + Value: "value", + Version: "1", }, nil }, }, @@ -533,27 +514,19 @@ func TestRun(t *testing.T) { ParseJSON: true, }, mock: &mockClient{ - //nolint:lll // mock function signature - getParameterFunc: func(_ context.Context, params *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - name := lo.FromPtr(params.Name) - if name == testParamVersion1 { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr(`{"key":"old"}`), - Version: 1, - Type: paramapi.ParameterTypeString, - }, + getParameterFunc: func(_ context.Context, _ string, version string) (*model.Parameter, error) { + if version == "1" { + return &model.Parameter{ + Name: "/app/param", + Value: `{"key":"old"}`, + Version: "1", }, nil } - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr(`{"key":"new"}`), - Version: 2, - Type: paramapi.ParameterTypeString, - }, + return &model.Parameter{ + Name: "/app/param", + Value: `{"key":"new"}`, + Version: "2", }, nil }, }, @@ -571,27 +544,19 @@ func TestRun(t *testing.T) { ParseJSON: true, }, mock: &mockClient{ - //nolint:lll // mock function signature - getParameterFunc: func(_ context.Context, params *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - name := lo.FromPtr(params.Name) - if name == testParamVersion1 { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr("not json"), - Version: 1, - Type: paramapi.ParameterTypeString, - }, + getParameterFunc: func(_ context.Context, _ string, version string) (*model.Parameter, error) { + if version == "1" { + return &model.Parameter{ + Name: "/app/param", + Value: "not json", + Version: "1", }, nil } - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr("also not json"), - Version: 2, - Type: paramapi.ParameterTypeString, - }, + return &model.Parameter{ + Name: "/app/param", + Value: "also not json", + Version: "2", }, nil }, }, @@ -635,14 +600,11 @@ func TestRun_IdenticalWarning(t *testing.T) { t.Parallel() mock := &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr("same-value"), - Version: 1, - Type: paramapi.ParameterTypeString, - }, + getParameterFunc: func(_ context.Context, _ string, _ string) (*model.Parameter, error) { + return &model.Parameter{ + Name: "/app/param", + Value: "same-value", + Version: "1", }, nil }, } diff --git a/internal/cli/commands/param/log/log.go b/internal/cli/commands/param/log/log.go index 9180ef04..6a01e415 100644 --- a/internal/cli/commands/param/log/log.go +++ b/internal/cli/commands/param/log/log.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "io" + "strconv" "time" "github.com/urfave/cli/v3" @@ -17,8 +18,8 @@ import ( "github.com/mpyw/suve/internal/cli/output" "github.com/mpyw/suve/internal/cli/pager" "github.com/mpyw/suve/internal/cli/terminal" - "github.com/mpyw/suve/internal/infra" "github.com/mpyw/suve/internal/jsonutil" + awsparam "github.com/mpyw/suve/internal/provider/aws/param" "github.com/mpyw/suve/internal/timeutil" "github.com/mpyw/suve/internal/usecase/param" ) @@ -198,7 +199,7 @@ func action(ctx context.Context, cmd *cli.Command) error { } } - client, err := infra.NewParamClient(ctx) + adapter, err := awsparam.NewAdapter(ctx) if err != nil { return fmt.Errorf("failed to initialize AWS client: %w", err) } @@ -208,7 +209,7 @@ func action(ctx context.Context, cmd *cli.Command) error { return pager.WithPagerWriter(cmd.Root().Writer, noPager, func(w io.Writer) error { r := &Runner{ - UseCase: ¶m.LogUseCase{Client: client}, + UseCase: ¶m.LogUseCase{Client: adapter}, Stdout: w, Stderr: cmd.Root().ErrWriter, } @@ -239,13 +240,15 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { if opts.Output == output.FormatJSON { items := make([]JSONOutputItem, len(entries)) for i, entry := range entries { + version, _ := strconv.ParseInt(entry.Version, 10, 64) + items[i] = JSONOutputItem{ - Version: entry.Version, - Type: string(entry.Type), + Version: version, + Type: entry.Type, Value: entry.Value, } - if entry.LastModified != nil { - items[i].Modified = timeutil.FormatRFC3339(*entry.LastModified) + if entry.UpdatedAt != nil { + items[i].Modified = timeutil.FormatRFC3339(*entry.UpdatedAt) } } @@ -259,8 +262,8 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { if opts.Oneline && !opts.ShowPatch { // Compact one-line format: VERSION DATE VALUE_PREVIEW dateStr := "" - if entry.LastModified != nil { - dateStr = entry.LastModified.Format("2006-01-02") + if entry.UpdatedAt != nil { + dateStr = entry.UpdatedAt.Format("2006-01-02") } value := entry.Value @@ -287,7 +290,7 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { currentMark = colors.Current(" (current)") } - output.Printf(r.Stdout, "%s%d%s %s %s\n", + output.Printf(r.Stdout, "%s%s%s %s %s\n", colors.Version(""), entry.Version, currentMark, @@ -298,15 +301,15 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { continue } - versionLabel := fmt.Sprintf("Version %d", entry.Version) + versionLabel := fmt.Sprintf("Version %s", entry.Version) if entry.IsCurrent { versionLabel += " " + colors.Current("(current)") } output.Println(r.Stdout, colors.Version(versionLabel)) - if entry.LastModified != nil { - output.Printf(r.Stdout, "%s %s\n", colors.FieldLabel("Date:"), timeutil.FormatRFC3339(*entry.LastModified)) + if entry.UpdatedAt != nil { + output.Printf(r.Stdout, "%s %s\n", colors.FieldLabel("Date:"), timeutil.FormatRFC3339(*entry.UpdatedAt)) } if opts.ShowPatch { @@ -331,8 +334,8 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { oldValue, newValue = jsonutil.TryFormatOrWarn2(oldValue, newValue, r.Stderr, "") } - oldName := fmt.Sprintf("%s#%d", result.Name, oldEntry.Version) - newName := fmt.Sprintf("%s#%d", result.Name, newEntry.Version) + oldName := fmt.Sprintf("%s#%s", result.Name, oldEntry.Version) + newName := fmt.Sprintf("%s#%s", result.Name, newEntry.Version) diff := output.Diff(oldName, newName, oldValue, newValue) if diff != "" { diff --git a/internal/cli/commands/param/log/log_test.go b/internal/cli/commands/param/log/log_test.go index a3bb1662..6e7f8d4c 100644 --- a/internal/cli/commands/param/log/log_test.go +++ b/internal/cli/commands/param/log/log_test.go @@ -12,10 +12,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mpyw/suve/internal/api/paramapi" appcli "github.com/mpyw/suve/internal/cli/commands" "github.com/mpyw/suve/internal/cli/commands/param/log" "github.com/mpyw/suve/internal/cli/output" + "github.com/mpyw/suve/internal/model" "github.com/mpyw/suve/internal/usecase/param" ) @@ -107,18 +107,40 @@ func TestCommand_Validation(t *testing.T) { }) } -//nolint:lll // mock struct fields match AWS SDK interface signatures type mockClient struct { - getParameterHistoryFunc func(ctx context.Context, params *paramapi.GetParameterHistoryInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) + getParameterResult *model.Parameter + getParameterErr error + getHistoryResult *model.ParameterHistory + getHistoryErr error + listParametersErr error } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockClient) GetParameterHistory(ctx context.Context, params *paramapi.GetParameterHistoryInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - if m.getParameterHistoryFunc != nil { - return m.getParameterHistoryFunc(ctx, params, optFns...) +func (m *mockClient) GetParameter(_ context.Context, _ string, _ string) (*model.Parameter, error) { + if m.getParameterErr != nil { + return nil, m.getParameterErr } - return nil, fmt.Errorf("GetParameterHistory not mocked") + return m.getParameterResult, nil +} + +func (m *mockClient) GetParameterHistory(_ context.Context, _ string) (*model.ParameterHistory, error) { + if m.getHistoryErr != nil { + return nil, m.getHistoryErr + } + + if m.getHistoryResult == nil { + return &model.ParameterHistory{}, nil + } + + return m.getHistoryResult, nil +} + +func (m *mockClient) ListParameters(_ context.Context, _ string, _ bool) ([]*model.ParameterListItem, error) { + if m.listParametersErr != nil { + return nil, m.listParametersErr + } + + return nil, nil } //nolint:funlen // Table-driven test with many cases @@ -138,16 +160,12 @@ func TestRun(t *testing.T) { name: "show history", opts: log.Options{Name: "/app/param", MaxResults: 10}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, params *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - assert.Equal(t, "/app/param", lo.FromPtr(params.Name)) - - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "v1", Version: "1", UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v2", Version: "2", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -161,15 +179,14 @@ func TestRun(t *testing.T) { name: "normal mode shows full value without truncation", opts: log.Options{Name: "/app/param", MaxResults: 10}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - longValue := "this is a very long value that should NOT be truncated in normal mode" - - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr(longValue), Version: 1, LastModifiedDate: &now}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + { + Name: "/app/param", Value: "this is a very long value that should NOT be truncated in normal mode", + Version: "1", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}, }, - }, nil + }, }, }, check: func(t *testing.T, output string) { @@ -183,15 +200,14 @@ func TestRun(t *testing.T) { name: "max-value-length truncates in normal mode", opts: log.Options{Name: "/app/param", MaxResults: 10, MaxValueLength: 20}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - longValue := "this is a very long value that should be truncated" - - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr(longValue), Version: 1, LastModifiedDate: &now}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + { + Name: "/app/param", Value: "this is a very long value that should be truncated", + Version: "1", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}, }, - }, nil + }, }, }, check: func(t *testing.T, output string) { @@ -205,14 +221,18 @@ func TestRun(t *testing.T) { name: "show patch between versions", opts: log.Options{Name: "/app/param", MaxResults: 10, ShowPatch: true}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("old-value"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("new-value"), Version: 2, LastModifiedDate: &now}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + { + Name: "/app/param", Value: "old-value", Version: "1", + UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}, }, - }, nil + { + Name: "/app/param", Value: "new-value", Version: "2", + UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}, + }, + }, }, }, check: func(t *testing.T, output string) { @@ -227,13 +247,11 @@ func TestRun(t *testing.T) { name: "patch with single version shows no diff", opts: log.Options{Name: "/app/param", MaxResults: 10, ShowPatch: true}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("only-value"), Version: 1, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "only-value", Version: "1", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -243,28 +261,21 @@ func TestRun(t *testing.T) { }, }, { - name: "error from AWS", - opts: log.Options{Name: "/app/param", MaxResults: 10}, - mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return nil, fmt.Errorf("AWS error") - }, - }, + name: "error from AWS", + opts: log.Options{Name: "/app/param", MaxResults: 10}, + mock: &mockClient{getHistoryErr: fmt.Errorf("AWS error")}, wantErr: true, }, { name: "reverse order shows oldest first", opts: log.Options{Name: "/app/param", MaxResults: 10, Reverse: true}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "v1", Version: "1", UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v2", Version: "2", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -285,14 +296,18 @@ func TestRun(t *testing.T) { name: "reverse with patch shows diff correctly", opts: log.Options{Name: "/app/param", MaxResults: 10, ShowPatch: true, Reverse: true}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("old-value"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("new-value"), Version: 2, LastModifiedDate: &now}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + { + Name: "/app/param", Value: "old-value", Version: "1", + UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}, }, - }, nil + { + Name: "/app/param", Value: "new-value", Version: "2", + UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}, + }, + }, }, }, check: func(t *testing.T, output string) { @@ -307,11 +322,9 @@ func TestRun(t *testing.T) { name: "empty history", opts: log.Options{Name: "/app/param", MaxResults: 10}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{}, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{}, }, }, check: func(t *testing.T, output string) { @@ -323,14 +336,12 @@ func TestRun(t *testing.T) { name: "oneline format", opts: log.Options{Name: "/app/param", MaxResults: 10, Oneline: true}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "v1", Version: "1", UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v2", Version: "2", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -347,15 +358,14 @@ func TestRun(t *testing.T) { name: "oneline truncates long values", opts: log.Options{Name: "/app/param", MaxResults: 10, Oneline: true}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - longValue := "this is a very long value that exceeds forty characters" - - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr(longValue), Version: 1, LastModifiedDate: &now}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + { + Name: "/app/param", Value: "this is a very long value that exceeds forty characters", + Version: "1", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}, }, - }, nil + }, }, }, check: func(t *testing.T, output string) { @@ -369,15 +379,13 @@ func TestRun(t *testing.T) { name: "filter by since date", opts: log.Options{Name: "/app/param", MaxResults: 10, Since: lo.ToPtr(now.Add(-90 * time.Minute))}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-2 * time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v3"), Version: 3, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "v1", Version: "1", UpdatedAt: lo.ToPtr(now.Add(-2 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v2", Version: "2", UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v3", Version: "3", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -391,15 +399,13 @@ func TestRun(t *testing.T) { name: "filter by until date", opts: log.Options{Name: "/app/param", MaxResults: 10, Until: lo.ToPtr(now.Add(-30 * time.Minute))}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-2 * time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v3"), Version: 3, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "v1", Version: "1", UpdatedAt: lo.ToPtr(now.Add(-2 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v2", Version: "2", UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v3", Version: "3", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -413,16 +419,14 @@ func TestRun(t *testing.T) { name: "filter by since and until date range", opts: log.Options{Name: "/app/param", MaxResults: 10, Since: lo.ToPtr(now.Add(-150 * time.Minute)), Until: lo.ToPtr(now.Add(-30 * time.Minute))}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-3 * time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now.Add(-2 * time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v3"), Version: 3, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v4"), Version: 4, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "v1", Version: "1", UpdatedAt: lo.ToPtr(now.Add(-3 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v2", Version: "2", UpdatedAt: lo.ToPtr(now.Add(-2 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v3", Version: "3", UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v4", Version: "4", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -437,13 +441,11 @@ func TestRun(t *testing.T) { name: "filter with no matching dates returns empty", opts: log.Options{Name: "/app/param", MaxResults: 10, Since: lo.ToPtr(now.Add(time.Hour)), Until: lo.ToPtr(now.Add(2 * time.Hour))}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "v1", Version: "1", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -455,14 +457,12 @@ func TestRun(t *testing.T) { name: "filter skips versions without LastModifiedDate", opts: log.Options{Name: "/app/param", MaxResults: 10, Since: lo.ToPtr(now.Add(-30 * time.Minute))}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: nil}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "v1", Version: "1", UpdatedAt: nil, Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "v2", Version: "2", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -475,14 +475,12 @@ func TestRun(t *testing.T) { name: "JSON output format", opts: log.Options{Name: "/app/param", MaxResults: 10, Output: output.FormatJSON}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("value1"), Version: 1, Type: paramapi.ParameterTypeString, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("value2"), Version: 2, Type: paramapi.ParameterTypeSecureString, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "value1", Version: "1", UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/param", Value: "value2", Version: "2", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "SecureString"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -497,13 +495,11 @@ func TestRun(t *testing.T) { name: "JSON output without LastModifiedDate", opts: log.Options{Name: "/app/param", MaxResults: 10, Output: output.FormatJSON}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("value1"), Version: 1, Type: paramapi.ParameterTypeString, LastModifiedDate: nil}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "value1", Version: "1", UpdatedAt: nil, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -516,14 +512,18 @@ func TestRun(t *testing.T) { name: "patch with JSON format", opts: log.Options{Name: "/app/param", MaxResults: 10, ShowPatch: true, ParseJSON: true}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr(`{"key":"old"}`), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr(`{"key":"new"}`), Version: 2, LastModifiedDate: &now}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + { + Name: "/app/param", Value: `{"key":"old"}`, Version: "1", + UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}, }, - }, nil + { + Name: "/app/param", Value: `{"key":"new"}`, Version: "2", + UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}, + }, + }, }, }, check: func(t *testing.T, output string) { @@ -532,16 +532,14 @@ func TestRun(t *testing.T) { }, }, { - name: "version without LastModifiedDate shows correctly", + name: "version without UpdatedAt shows correctly", opts: log.Options{Name: "/app/param", MaxResults: 10}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: nil}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "v1", Version: "1", UpdatedAt: nil, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -554,13 +552,11 @@ func TestRun(t *testing.T) { name: "oneline without LastModifiedDate", opts: log.Options{Name: "/app/param", MaxResults: 10, Oneline: true}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: nil}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Value: "v1", Version: "1", UpdatedAt: nil, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -572,14 +568,18 @@ func TestRun(t *testing.T) { name: "patch with identical values shows no diff", opts: log.Options{Name: "/app/param", MaxResults: 10, ShowPatch: true}, mock: &mockClient{ - //nolint:lll // inline mock - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("same-value"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/app/param"), Value: lo.ToPtr("same-value"), Version: 2, LastModifiedDate: &now}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + { + Name: "/app/param", Value: "same-value", Version: "1", + UpdatedAt: lo.ToPtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}, + }, + { + Name: "/app/param", Value: "same-value", Version: "2", + UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}, }, - }, nil + }, }, }, check: func(t *testing.T, output string) { diff --git a/internal/cli/commands/param/show/show.go b/internal/cli/commands/param/show/show.go index 3de07bd2..1d84d597 100644 --- a/internal/cli/commands/param/show/show.go +++ b/internal/cli/commands/param/show/show.go @@ -11,11 +11,10 @@ import ( "github.com/samber/lo" "github.com/urfave/cli/v3" - "github.com/mpyw/suve/internal/api/paramapi" "github.com/mpyw/suve/internal/cli/output" "github.com/mpyw/suve/internal/cli/pager" - "github.com/mpyw/suve/internal/infra" "github.com/mpyw/suve/internal/jsonutil" + awsparam "github.com/mpyw/suve/internal/provider/aws/param" "github.com/mpyw/suve/internal/timeutil" "github.com/mpyw/suve/internal/usecase/param" "github.com/mpyw/suve/internal/version/paramversion" @@ -112,7 +111,7 @@ func action(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("--raw and --output=json cannot be used together") } - client, err := infra.NewParamClient(ctx) + adapter, err := awsparam.NewAdapter(ctx) if err != nil { return fmt.Errorf("failed to initialize AWS client: %w", err) } @@ -130,7 +129,7 @@ func action(ctx context.Context, cmd *cli.Command) error { return pager.WithPagerWriter(cmd.Root().Writer, noPager, func(w io.Writer) error { r := &Runner{ - UseCase: ¶m.ShowUseCase{Client: client}, + UseCase: ¶m.ShowUseCase{Client: adapter}, Stdout: w, Stderr: cmd.Root().ErrWriter, } @@ -154,7 +153,7 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { // Warn if --parse-json is used in cases where it's not meaningful if opts.ParseJSON { switch result.Type { - case paramapi.ParameterTypeStringList: + case "StringList": output.Warning(r.Stderr, "--parse-json has no effect on StringList type (comma-separated values)") default: formatted := jsonutil.TryFormatOrWarn(value, r.Stderr, "") @@ -174,10 +173,11 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { // JSON output mode if opts.Output == output.FormatJSON { + version, _ := strconv.ParseInt(result.Version, 10, 64) jsonOut := JSONOutput{ Name: result.Name, - Version: result.Version, - Type: string(result.Type), + Version: version, + Type: result.Type, Value: value, } // Show json_parsed only when --parse-json was used and succeeded @@ -185,8 +185,8 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { jsonOut.JSONParsed = lo.ToPtr(true) } - if result.LastModified != nil { - jsonOut.Modified = timeutil.FormatRFC3339(*result.LastModified) + if result.UpdatedAt != nil { + jsonOut.Modified = timeutil.FormatRFC3339(*result.UpdatedAt) } jsonOut.Tags = make(map[string]string) @@ -203,15 +203,15 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { // Normal mode: show metadata + value out := output.New(r.Stdout) out.Field("Name", result.Name) - out.Field("Version", strconv.FormatInt(result.Version, 10)) - out.Field("Type", string(result.Type)) + out.Field("Version", result.Version) + out.Field("Type", result.Type) // Show json_parsed only when --parse-json was used and succeeded if jsonParsed { out.Field("JsonParsed", "true") } - if result.LastModified != nil { - out.Field("Modified", timeutil.FormatRFC3339(*result.LastModified)) + if result.UpdatedAt != nil { + out.Field("Modified", timeutil.FormatRFC3339(*result.UpdatedAt)) } if len(result.Tags) > 0 { diff --git a/internal/cli/commands/param/show/show_test.go b/internal/cli/commands/param/show/show_test.go index 309d5d01..16a09db4 100644 --- a/internal/cli/commands/param/show/show_test.go +++ b/internal/cli/commands/param/show/show_test.go @@ -8,14 +8,13 @@ import ( "testing" "time" - "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mpyw/suve/internal/api/paramapi" appcli "github.com/mpyw/suve/internal/cli/commands" "github.com/mpyw/suve/internal/cli/commands/param/show" "github.com/mpyw/suve/internal/cli/output" + "github.com/mpyw/suve/internal/model" "github.com/mpyw/suve/internal/usecase/param" "github.com/mpyw/suve/internal/version/paramversion" ) @@ -42,30 +41,59 @@ func TestCommand_Validation(t *testing.T) { }) } -//nolint:lll // mock struct fields match AWS SDK interface signatures type mockClient struct { - getParameterFunc func(ctx context.Context, params *paramapi.GetParameterInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) - getParameterHistoryFunc func(ctx context.Context, params *paramapi.GetParameterHistoryInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) - listTagsForResourceFunc func(ctx context.Context, params *paramapi.ListTagsForResourceInput, optFns ...func(*paramapi.Options)) (*paramapi.ListTagsForResourceOutput, error) + getParameterResult *model.Parameter + getParameterErr error + getHistoryResult *model.ParameterHistory + getHistoryErr error + listParametersResult []*model.ParameterListItem + listParametersErr error + getTagsResult map[string]string + getTagsErr error } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockClient) GetParameter(ctx context.Context, params *paramapi.GetParameterInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return m.getParameterFunc(ctx, params, optFns...) +func (m *mockClient) GetParameter(_ context.Context, _ string, _ string) (*model.Parameter, error) { + if m.getParameterErr != nil { + return nil, m.getParameterErr + } + + return m.getParameterResult, nil } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockClient) GetParameterHistory(ctx context.Context, params *paramapi.GetParameterHistoryInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return m.getParameterHistoryFunc(ctx, params, optFns...) +func (m *mockClient) GetParameterHistory(_ context.Context, _ string) (*model.ParameterHistory, error) { + if m.getHistoryErr != nil { + return nil, m.getHistoryErr + } + + if m.getHistoryResult == nil { + return &model.ParameterHistory{}, nil + } + + return m.getHistoryResult, nil } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockClient) ListTagsForResource(ctx context.Context, params *paramapi.ListTagsForResourceInput, optFns ...func(*paramapi.Options)) (*paramapi.ListTagsForResourceOutput, error) { - if m.listTagsForResourceFunc != nil { - return m.listTagsForResourceFunc(ctx, params, optFns...) +func (m *mockClient) ListParameters(_ context.Context, _ string, _ bool) ([]*model.ParameterListItem, error) { + if m.listParametersErr != nil { + return nil, m.listParametersErr } - return ¶mapi.ListTagsForResourceOutput{}, nil + return m.listParametersResult, nil +} + +func (m *mockClient) GetTags(_ context.Context, _ string) (map[string]string, error) { + if m.getTagsErr != nil { + return nil, m.getTagsErr + } + + return m.getTagsResult, nil +} + +func (m *mockClient) AddTags(_ context.Context, _ string, _ map[string]string) error { + return nil +} + +func (m *mockClient) RemoveTags(_ context.Context, _ string, _ []string) error { + return nil } //nolint:funlen // Table-driven test with many cases @@ -87,16 +115,14 @@ func TestRun(t *testing.T) { Spec: ¶mversion.Spec{Name: "/my/param"}, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("test-value"), - Version: 3, - Type: paramapi.ParameterTypeString, - LastModifiedDate: &now, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: "test-value", + Version: "3", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{ + Type: "String", + }, }, }, check: func(t *testing.T, output string) { @@ -111,15 +137,17 @@ func TestRun(t *testing.T) { Spec: ¶mversion.Spec{Name: "/my/param", Shift: 1}, }, mock: &mockClient{ - //nolint:lll // inline mock function in test table - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v3"), Version: 3, LastModifiedDate: &now}, - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-2 * time.Hour))}, + getHistoryResult: &model.ParameterHistory{ + Name: "/my/param", + Parameters: []*model.Parameter{ + {Name: "/my/param", Value: "v3", Version: "3", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/my/param", Value: "v2", Version: "2", UpdatedAt: timePtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + { + Name: "/my/param", Value: "v1", Version: "1", + UpdatedAt: timePtr(now.Add(-2 * time.Hour)), + Metadata: model.AWSParameterMeta{Type: "String"}, }, - }, nil + }, }, }, check: func(t *testing.T, output string) { @@ -134,16 +162,14 @@ func TestRun(t *testing.T) { ParseJSON: true, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr(`{"zebra":"last","apple":"first"}`), - Version: 1, - Type: paramapi.ParameterTypeString, - LastModifiedDate: &now, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: `{"zebra":"last","apple":"first"}`, + Version: "1", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{ + Type: "String", + }, }, }, check: func(t *testing.T, output string) { @@ -159,28 +185,22 @@ func TestRun(t *testing.T) { }, }, { - name: "error from AWS", - opts: show.Options{Spec: ¶mversion.Spec{Name: "/my/param"}}, - mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return nil, fmt.Errorf("AWS error") - }, - }, + name: "error from AWS", + opts: show.Options{Spec: ¶mversion.Spec{Name: "/my/param"}}, + mock: &mockClient{getParameterErr: fmt.Errorf("AWS error")}, wantErr: true, }, { name: "show without LastModifiedDate", opts: show.Options{Spec: ¶mversion.Spec{Name: "/my/param"}}, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("test-value"), - Version: 1, - Type: paramapi.ParameterTypeString, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: "test-value", + Version: "1", + Metadata: model.AWSParameterMeta{ + Type: "String", + }, }, }, check: func(t *testing.T, output string) { @@ -196,15 +216,13 @@ func TestRun(t *testing.T) { ParseJSON: true, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("a,b,c"), - Version: 1, - Type: paramapi.ParameterTypeStringList, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: "a,b,c", + Version: "1", + Metadata: model.AWSParameterMeta{ + Type: "StringList", + }, }, }, check: func(t *testing.T, output string) { @@ -219,15 +237,13 @@ func TestRun(t *testing.T) { ParseJSON: true, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("encrypted-blob"), - Version: 1, - Type: paramapi.ParameterTypeSecureString, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: "encrypted-blob", + Version: "1", + Metadata: model.AWSParameterMeta{ + Type: "SecureString", + }, }, }, check: func(t *testing.T, output string) { @@ -243,15 +259,13 @@ func TestRun(t *testing.T) { ParseJSON: true, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("not json"), - Version: 1, - Type: paramapi.ParameterTypeString, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: "not json", + Version: "1", + Metadata: model.AWSParameterMeta{ + Type: "String", + }, }, }, check: func(t *testing.T, output string) { @@ -268,16 +282,14 @@ func TestRun(t *testing.T) { Raw: true, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("raw-value"), - Version: 1, - Type: paramapi.ParameterTypeString, - LastModifiedDate: &now, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: "raw-value", + Version: "1", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{ + Type: "String", + }, }, }, check: func(t *testing.T, output string) { @@ -293,14 +305,12 @@ func TestRun(t *testing.T) { Raw: true, }, mock: &mockClient{ - //nolint:lll // inline mock function in test table - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: &now}, - }, - }, nil + getHistoryResult: &model.ParameterHistory{ + Name: "/my/param", + Parameters: []*model.Parameter{ + {Name: "/my/param", Value: "v1", Version: "1", UpdatedAt: timePtr(now.Add(-time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/my/param", Value: "v2", Version: "2", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, + }, }, }, check: func(t *testing.T, output string) { @@ -317,16 +327,14 @@ func TestRun(t *testing.T) { Raw: true, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr(`{"zebra":"last","apple":"first"}`), - Version: 1, - Type: paramapi.ParameterTypeString, - LastModifiedDate: &now, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: `{"zebra":"last","apple":"first"}`, + Version: "1", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{ + Type: "String", + }, }, }, check: func(t *testing.T, output string) { @@ -346,25 +354,18 @@ func TestRun(t *testing.T) { Spec: ¶mversion.Spec{Name: "/my/param"}, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("test-value"), - Version: 1, - Type: paramapi.ParameterTypeString, - LastModifiedDate: &now, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: "test-value", + Version: "1", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{ + Type: "String", + }, }, - //nolint:lll // inline mock function in test table - listTagsForResourceFunc: func(_ context.Context, _ *paramapi.ListTagsForResourceInput, _ ...func(*paramapi.Options)) (*paramapi.ListTagsForResourceOutput, error) { - return ¶mapi.ListTagsForResourceOutput{ - TagList: []paramapi.Tag{ - {Key: lo.ToPtr("Environment"), Value: lo.ToPtr("production")}, - {Key: lo.ToPtr("Team"), Value: lo.ToPtr("backend")}, - }, - }, nil + getTagsResult: map[string]string{ + "Environment": "production", + "Team": "backend", }, }, check: func(t *testing.T, output string) { @@ -385,25 +386,18 @@ func TestRun(t *testing.T) { Output: output.FormatJSON, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("test-value"), - Version: 1, - Type: paramapi.ParameterTypeString, - LastModifiedDate: &now, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: "test-value", + Version: "1", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{ + Type: "String", + }, }, - //nolint:lll // inline mock function in test table - listTagsForResourceFunc: func(_ context.Context, _ *paramapi.ListTagsForResourceInput, _ ...func(*paramapi.Options)) (*paramapi.ListTagsForResourceOutput, error) { - return ¶mapi.ListTagsForResourceOutput{ - TagList: []paramapi.Tag{ - {Key: lo.ToPtr("Environment"), Value: lo.ToPtr("production")}, - {Key: lo.ToPtr("Team"), Value: lo.ToPtr("backend")}, - }, - }, nil + getTagsResult: map[string]string{ + "Environment": "production", + "Team": "backend", }, }, check: func(t *testing.T, output string) { @@ -423,15 +417,13 @@ func TestRun(t *testing.T) { Output: output.FormatJSON, }, mock: &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("test-value"), - Version: 1, - Type: paramapi.ParameterTypeString, - }, - }, nil + getParameterResult: &model.Parameter{ + Name: "/my/param", + Value: "test-value", + Version: "1", + Metadata: model.AWSParameterMeta{ + Type: "String", + }, }, }, check: func(t *testing.T, output string) { @@ -469,3 +461,7 @@ func TestRun(t *testing.T) { }) } } + +func timePtr(t time.Time) *time.Time { + return &t +} diff --git a/internal/cli/commands/stage/diff/diff.go b/internal/cli/commands/stage/diff/diff.go index a15252db..ae52984c 100644 --- a/internal/cli/commands/stage/diff/diff.go +++ b/internal/cli/commands/stage/diff/diff.go @@ -18,7 +18,10 @@ import ( "github.com/mpyw/suve/internal/infra" "github.com/mpyw/suve/internal/jsonutil" "github.com/mpyw/suve/internal/maputil" + "github.com/mpyw/suve/internal/model" "github.com/mpyw/suve/internal/parallel" + "github.com/mpyw/suve/internal/provider" + awsparam "github.com/mpyw/suve/internal/provider/aws/param" "github.com/mpyw/suve/internal/staging" "github.com/mpyw/suve/internal/staging/store" "github.com/mpyw/suve/internal/staging/store/agent" @@ -44,6 +47,7 @@ type SecretClient interface { // Runner executes the diff command. type Runner struct { ParamClient ParamClient + ParamReader provider.ParameterReader // for version resolution SecretClient SecretClient Store store.ReadWriteOperator Stdout io.Writer @@ -144,6 +148,7 @@ func action(ctx context.Context, cmd *cli.Command) error { } r.ParamClient = paramClient + r.ParamReader = awsparam.New(paramClient) } if hasSecret { @@ -193,10 +198,10 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { paramResults := parallel.ExecuteMap( ctx, paramEntries, - func(ctx context.Context, name string, _ staging.Entry) (*paramapi.ParameterHistory, error) { + func(ctx context.Context, name string, _ staging.Entry) (*model.Parameter, error) { spec := ¶mversion.Spec{Name: name} - return paramversion.GetParameterWithVersion(ctx, r.ParamClient, spec) + return paramversion.GetParameterWithVersion(ctx, r.ParamReader, spec) }, ) @@ -353,8 +358,8 @@ func (r *Runner) Run(ctx context.Context, opts Options) error { return nil } -func (r *Runner) outputParamDiff(ctx context.Context, opts Options, name string, entry staging.Entry, param *paramapi.ParameterHistory) error { - awsValue := lo.FromPtr(param.Value) +func (r *Runner) outputParamDiff(ctx context.Context, opts Options, name string, entry staging.Entry, param *model.Parameter) error { + awsValue := param.Value stagedValue := lo.FromPtr(entry.Value) // For delete operation, staged value is empty @@ -378,7 +383,7 @@ func (r *Runner) outputParamDiff(ctx context.Context, opts Options, name string, return nil } - label1 := fmt.Sprintf("%s#%d (AWS)", name, param.Version) + label1 := fmt.Sprintf("%s#%s (AWS)", name, param.Version) label2 := fmt.Sprintf(lo.Ternary( entry.Operation == staging.OperationDelete, "%s (staged for deletion)", diff --git a/internal/cli/commands/stage/diff/diff_test.go b/internal/cli/commands/stage/diff/diff_test.go index 5e35525b..803a3fcf 100644 --- a/internal/cli/commands/stage/diff/diff_test.go +++ b/internal/cli/commands/stage/diff/diff_test.go @@ -17,6 +17,7 @@ import ( appcli "github.com/mpyw/suve/internal/cli/commands" stagediff "github.com/mpyw/suve/internal/cli/commands/stage/diff" "github.com/mpyw/suve/internal/maputil" + "github.com/mpyw/suve/internal/model" "github.com/mpyw/suve/internal/staging" "github.com/mpyw/suve/internal/staging/store/testutil" ) @@ -131,6 +132,59 @@ func (m *mockSecretClient) DescribeSecret( return &secretapi.DescribeSecretOutput{}, nil } +// paramReaderMock implements provider.ParameterReader for testing. +type paramReaderMock struct { + getParameterFunc func(ctx context.Context, name string, version string) (*model.Parameter, error) + getParameterHistoryFunc func(ctx context.Context, name string) (*model.ParameterHistory, error) +} + +func (m *paramReaderMock) GetParameter(ctx context.Context, name string, version string) (*model.Parameter, error) { + if m.getParameterFunc != nil { + return m.getParameterFunc(ctx, name, version) + } + + return nil, fmt.Errorf("GetParameter not mocked") +} + +func (m *paramReaderMock) GetParameterHistory(ctx context.Context, name string) (*model.ParameterHistory, error) { + if m.getParameterHistoryFunc != nil { + return m.getParameterHistoryFunc(ctx, name) + } + + return nil, fmt.Errorf("GetParameterHistory not mocked") +} + +func (m *paramReaderMock) ListParameters(_ context.Context, _ string, _ bool) ([]*model.ParameterListItem, error) { + return nil, fmt.Errorf("ListParameters not mocked") +} + +// newDefaultParamReader creates a paramReaderMock that returns a test value based on the name. +func newDefaultParamReader() *paramReaderMock { + return newParamReaderWithValue("aws-value") +} + +// newParamReaderWithValue creates a paramReaderMock that returns the specified value. +func newParamReaderWithValue(value string) *paramReaderMock { + return ¶mReaderMock{ + getParameterFunc: func(_ context.Context, name string, _ string) (*model.Parameter, error) { + return &model.Parameter{ + Name: name, + Value: value, + Version: "1", + }, nil + }, + } +} + +// newParamReaderWithError creates a paramReaderMock that returns an error. +func newParamReaderWithError(err error) *paramReaderMock { + return ¶mReaderMock{ + getParameterFunc: func(_ context.Context, _ string, _ string) (*model.Parameter, error) { + return nil, err + }, + } +} + func TestCommand_Validation(t *testing.T) { t.Parallel() @@ -205,6 +259,7 @@ func TestRun_ParamOnly(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newParamReaderWithValue("old-value"), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -308,6 +363,7 @@ func TestRun_BothServices(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newParamReaderWithValue("param-old"), SecretClient: secretMock, Store: store, Stdout: &stdout, @@ -371,6 +427,7 @@ func TestRun_DeleteOperations(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newParamReaderWithValue("existing-value"), SecretClient: secretMock, Store: store, Stdout: &stdout, @@ -414,6 +471,7 @@ func TestRun_IdenticalValues(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newParamReaderWithValue("same-value"), // same as staged value Store: store, Stdout: &stdout, Stderr: &stderr, @@ -458,6 +516,7 @@ func TestRun_ParseJSON(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newDefaultParamReader(), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -493,6 +552,7 @@ func TestRun_ParamUpdateAutoUnstageWhenDeleted(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newParamReaderWithError(fmt.Errorf("parameter not found")), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -699,6 +759,7 @@ func TestRun_ParamCreateOperation(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newParamReaderWithError(fmt.Errorf("parameter not found")), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -775,16 +836,13 @@ func TestRun_CreateWithParseJSON(t *testing.T) { }) require.NoError(t, err) - paramMock := &mockParamClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return nil, fmt.Errorf("parameter not found") - }, - } + paramMock := &mockParamClient{} var stdout, stderr bytes.Buffer r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newParamReaderWithError(fmt.Errorf("parameter not found")), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -810,16 +868,13 @@ func TestRun_DeleteAutoUnstageWhenAlreadyDeleted(t *testing.T) { }) require.NoError(t, err) - paramMock := &mockParamClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return nil, fmt.Errorf("parameter not found") - }, - } + paramMock := &mockParamClient{} var stdout, stderr bytes.Buffer r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newParamReaderWithError(fmt.Errorf("parameter not found")), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -902,6 +957,7 @@ func TestRun_MetadataWithDescription(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newDefaultParamReader(), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -948,6 +1004,7 @@ func TestRun_MetadataWithTags(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newDefaultParamReader(), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -1133,6 +1190,7 @@ func TestRun_BothEntriesAndTags(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newParamReaderWithValue("old-value"), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -1182,6 +1240,7 @@ func TestRun_ParamTagDiffWithValues(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newDefaultParamReader(), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -1265,6 +1324,7 @@ func TestRun_ParamTagDiffAPIError(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newDefaultParamReader(), Store: store, Stdout: &stdout, Stderr: &stderr, @@ -1348,6 +1408,7 @@ func TestRun_TagDiffWithMissingValue(t *testing.T) { r := &stagediff.Runner{ ParamClient: paramMock, + ParamReader: newDefaultParamReader(), Store: store, Stdout: &stdout, Stderr: &stderr, diff --git a/internal/gui/param.go b/internal/gui/param.go index 2913dcc9..138c4a54 100644 --- a/internal/gui/param.go +++ b/internal/gui/param.go @@ -4,6 +4,7 @@ package gui import ( "errors" + "strconv" "github.com/mpyw/suve/internal/api/paramapi" awsparam "github.com/mpyw/suve/internal/provider/aws/param" @@ -11,6 +12,11 @@ import ( "github.com/mpyw/suve/internal/version/paramversion" ) +// parseInt64 converts a string to int64, returning 0 on error. +func parseInt64(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) +} + // ============================================================================= // Param Types // ============================================================================= @@ -124,28 +130,30 @@ func (a *App) ParamShow(specStr string) (*ParamShowResult, error) { return nil, err } - client, err := a.getParamClient() + adapter, err := awsparam.NewAdapter(a.ctx) if err != nil { return nil, err } - uc := ¶m.ShowUseCase{Client: client} + uc := ¶m.ShowUseCase{Client: adapter} result, err := uc.Execute(a.ctx, param.ShowInput{Spec: spec}) if err != nil { return nil, err } + version, _ := parseInt64(result.Version) + r := &ParamShowResult{ Name: result.Name, Value: result.Value, - Version: result.Version, - Type: string(result.Type), + Version: version, + Type: result.Type, Description: result.Description, Tags: make([]ParamShowTag, 0, len(result.Tags)), } - if result.LastModified != nil { - r.LastModified = result.LastModified.Format("2006-01-02T15:04:05Z07:00") + if result.UpdatedAt != nil { + r.LastModified = result.UpdatedAt.Format("2006-01-02T15:04:05Z07:00") } for _, tag := range result.Tags { @@ -160,12 +168,12 @@ func (a *App) ParamShow(specStr string) (*ParamShowResult, error) { // ParamLog shows parameter version history. func (a *App) ParamLog(name string, maxResults int32) (*ParamLogResult, error) { - client, err := a.getParamClient() + adapter, err := awsparam.NewAdapter(a.ctx) if err != nil { return nil, err } - uc := ¶m.LogUseCase{Client: client} + uc := ¶m.LogUseCase{Client: adapter} result, err := uc.Execute(a.ctx, param.LogInput{ Name: name, @@ -177,14 +185,16 @@ func (a *App) ParamLog(name string, maxResults int32) (*ParamLogResult, error) { entries := make([]ParamLogEntry, len(result.Entries)) for i, e := range result.Entries { + version, _ := parseInt64(e.Version) + entry := ParamLogEntry{ - Version: e.Version, + Version: version, Value: e.Value, - Type: string(e.Type), + Type: e.Type, IsCurrent: e.IsCurrent, } - if e.LastModified != nil { - entry.LastModified = e.LastModified.Format("2006-01-02T15:04:05Z07:00") + if e.UpdatedAt != nil { + entry.LastModified = e.UpdatedAt.Format("2006-01-02T15:04:05Z07:00") } entries[i] = entry @@ -210,7 +220,9 @@ func (a *App) ParamDiff(spec1Str, spec2Str string) (*ParamDiffResult, error) { return nil, err } - uc := ¶m.DiffUseCase{Client: client} + // Create adapter that implements provider.ParameterReader + adapter := awsparam.New(client) + uc := ¶m.DiffUseCase{Client: adapter} result, err := uc.Execute(a.ctx, param.DiffInput{ Spec1: spec1, diff --git a/internal/model/parameter.go b/internal/model/parameter.go index 880cd42c..957d7907 100644 --- a/internal/model/parameter.go +++ b/internal/model/parameter.go @@ -10,25 +10,27 @@ import "time" // TypedParameter is a parameter with provider-specific metadata. // This type is used at the Provider layer for type-safe access to metadata. type TypedParameter[M any] struct { - Name string - Value string - Version string - Description string - LastModified *time.Time - Tags map[string]string - Metadata M + Name string + Value string + Version string + Description string + CreatedAt *time.Time + UpdatedAt *time.Time + Tags map[string]string + Metadata M } // ToBase converts to a UseCase layer type. func (p *TypedParameter[M]) ToBase() *Parameter { return &Parameter{ - Name: p.Name, - Value: p.Value, - Version: p.Version, - Description: p.Description, - LastModified: p.LastModified, - Tags: p.Tags, - Metadata: p.Metadata, + Name: p.Name, + Value: p.Value, + Version: p.Version, + Description: p.Description, + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, + Tags: p.Tags, + Metadata: p.Metadata, } } @@ -38,13 +40,14 @@ func (p *TypedParameter[M]) ToBase() *Parameter { // Parameter is a provider-agnostic parameter for the UseCase layer. type Parameter struct { - Name string - Value string - Version string - Description string - LastModified *time.Time - Tags map[string]string - Metadata any // Provider-specific metadata (e.g., AWSParameterMeta) + Name string + Value string + Version string + Description string + CreatedAt *time.Time + UpdatedAt *time.Time + Tags map[string]string + Metadata any // Provider-specific metadata (e.g., AWSParameterMeta) } // AWSMeta returns the AWS-specific metadata if available. @@ -147,11 +150,12 @@ type AWSParameterHistory = TypedParameterHistory[AWSParameterMeta] // ParameterListItem represents a parameter in a list (without value). type ParameterListItem struct { - Name string - Description string - LastModified *time.Time - Tags map[string]string - Metadata any // Provider-specific metadata (e.g., AWSParameterListItemMeta) + Name string + Description string + CreatedAt *time.Time + UpdatedAt *time.Time + Tags map[string]string + Metadata any // Provider-specific metadata (e.g., AWSParameterListItemMeta) } // AWSMeta returns the AWS-specific metadata if available. diff --git a/internal/model/parameter_test.go b/internal/model/parameter_test.go index aeb82dc1..64413da1 100644 --- a/internal/model/parameter_test.go +++ b/internal/model/parameter_test.go @@ -14,12 +14,13 @@ func TestTypedParameter_ToBase(t *testing.T) { now := time.Now() typed := &model.TypedParameter[model.AWSParameterMeta]{ - Name: "test-param", - Value: "test-value", - Version: "1", - Description: "test description", - LastModified: &now, - Tags: map[string]string{"key": "value"}, + Name: "test-param", + Value: "test-value", + Version: "1", + Description: "test description", + CreatedAt: &now, + UpdatedAt: &now, + Tags: map[string]string{"key": "value"}, Metadata: model.AWSParameterMeta{ Type: "String", ARN: "arn:aws:ssm:us-east-1:123456789012:parameter/test-param", @@ -33,7 +34,8 @@ func TestTypedParameter_ToBase(t *testing.T) { assert.Equal(t, typed.Value, base.Value) assert.Equal(t, typed.Version, base.Version) assert.Equal(t, typed.Description, base.Description) - assert.Equal(t, typed.LastModified, base.LastModified) + assert.Equal(t, typed.CreatedAt, base.CreatedAt) + assert.Equal(t, typed.UpdatedAt, base.UpdatedAt) assert.Equal(t, typed.Tags, base.Tags) assert.IsType(t, model.AWSParameterMeta{}, base.Metadata) @@ -86,18 +88,18 @@ func TestTypedParameterHistory_ToBase(t *testing.T) { Name: "test-param", Parameters: []*model.TypedParameter[model.AWSParameterMeta]{ { - Name: "test-param", - Value: "value1", - Version: "1", - LastModified: &now, - Metadata: model.AWSParameterMeta{Tier: "Standard"}, + Name: "test-param", + Value: "value1", + Version: "1", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{Tier: "Standard"}, }, { - Name: "test-param", - Value: "value2", - Version: "2", - LastModified: &now, - Metadata: model.AWSParameterMeta{Tier: "Advanced"}, + Name: "test-param", + Value: "value2", + Version: "2", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{Tier: "Advanced"}, }, }, } diff --git a/internal/model/secret.go b/internal/model/secret.go index 97ccd94c..df3575b2 100644 --- a/internal/model/secret.go +++ b/internal/model/secret.go @@ -11,9 +11,10 @@ import "time" type TypedSecret[M any] struct { Name string Value string - VersionID string + Version string Description string - CreatedDate *time.Time + CreatedAt *time.Time + UpdatedAt *time.Time Tags map[string]string Metadata M } @@ -23,9 +24,10 @@ func (s *TypedSecret[M]) ToBase() *Secret { return &Secret{ Name: s.Name, Value: s.Value, - VersionID: s.VersionID, + Version: s.Version, Description: s.Description, - CreatedDate: s.CreatedDate, + CreatedAt: s.CreatedAt, + UpdatedAt: s.UpdatedAt, Tags: s.Tags, Metadata: s.Metadata, } @@ -39,9 +41,10 @@ func (s *TypedSecret[M]) ToBase() *Secret { type Secret struct { Name string Value string - VersionID string + Version string Description string - CreatedDate *time.Time + CreatedAt *time.Time + UpdatedAt *time.Time Tags map[string]string Metadata any // Provider-specific metadata (e.g., AWSSecretMeta) } @@ -136,25 +139,25 @@ type AzureSecret = TypedSecret[AzureKeyVaultMeta] // TypedSecretVersion represents a version of a typed secret. type TypedSecretVersion[M any] struct { - VersionID string - CreatedDate *time.Time - Metadata M + Version string + CreatedAt *time.Time + Metadata M } // ToBase converts to a UseCase layer type. func (v *TypedSecretVersion[M]) ToBase() *SecretVersion { return &SecretVersion{ - VersionID: v.VersionID, - CreatedDate: v.CreatedDate, - Metadata: v.Metadata, + Version: v.Version, + CreatedAt: v.CreatedAt, + Metadata: v.Metadata, } } // SecretVersion represents a version of a secret. type SecretVersion struct { - VersionID string - CreatedDate *time.Time - Metadata any // Provider-specific metadata + Version string + CreatedAt *time.Time + Metadata any // Provider-specific metadata } // AWSSecretVersionMeta contains AWS-specific version metadata. @@ -171,12 +174,12 @@ type AWSSecretVersion = TypedSecretVersion[AWSSecretVersionMeta] // SecretListItem represents a secret in a list (without value). type SecretListItem struct { - Name string - Description string - CreatedDate *time.Time - LastModified *time.Time - Tags map[string]string - Metadata any // Provider-specific metadata (e.g., AWSSecretListItemMeta) + Name string + Description string + CreatedAt *time.Time + UpdatedAt *time.Time + Tags map[string]string + Metadata any // Provider-specific metadata (e.g., AWSSecretListItemMeta) } // AWSMeta returns the AWS-specific metadata if available. diff --git a/internal/model/secret_test.go b/internal/model/secret_test.go index 043e0d66..73439e33 100644 --- a/internal/model/secret_test.go +++ b/internal/model/secret_test.go @@ -16,9 +16,10 @@ func TestTypedSecret_ToBase(t *testing.T) { typed := &model.TypedSecret[model.AWSSecretMeta]{ Name: "test-secret", Value: "test-value", - VersionID: "v1", + Version: "v1", Description: "test description", - CreatedDate: &now, + CreatedAt: &now, + UpdatedAt: &now, Tags: map[string]string{"key": "value"}, Metadata: model.AWSSecretMeta{ ARN: "arn:aws:secretsmanager:us-east-1:123456789012:secret:test-secret", @@ -32,9 +33,10 @@ func TestTypedSecret_ToBase(t *testing.T) { assert.Equal(t, typed.Name, base.Name) assert.Equal(t, typed.Value, base.Value) - assert.Equal(t, typed.VersionID, base.VersionID) + assert.Equal(t, typed.Version, base.Version) assert.Equal(t, typed.Description, base.Description) - assert.Equal(t, typed.CreatedDate, base.CreatedDate) + assert.Equal(t, typed.CreatedAt, base.CreatedAt) + assert.Equal(t, typed.UpdatedAt, base.UpdatedAt) assert.Equal(t, typed.Tags, base.Tags) assert.IsType(t, model.AWSSecretMeta{}, base.Metadata) @@ -84,8 +86,8 @@ func TestTypedSecretVersion_ToBase(t *testing.T) { now := time.Now() typed := &model.TypedSecretVersion[model.AWSSecretVersionMeta]{ - VersionID: "v1", - CreatedDate: &now, + Version: "v1", + CreatedAt: &now, Metadata: model.AWSSecretVersionMeta{ VersionStages: []string{"AWSCURRENT"}, }, @@ -93,7 +95,7 @@ func TestTypedSecretVersion_ToBase(t *testing.T) { base := typed.ToBase() - assert.Equal(t, typed.VersionID, base.VersionID) - assert.Equal(t, typed.CreatedDate, base.CreatedDate) + assert.Equal(t, typed.Version, base.Version) + assert.Equal(t, typed.CreatedAt, base.CreatedAt) assert.IsType(t, model.AWSSecretVersionMeta{}, base.Metadata) } diff --git a/internal/provider/aws/param/adapter.go b/internal/provider/aws/param/adapter.go index ac4a6ee8..9f5c8b7f 100644 --- a/internal/provider/aws/param/adapter.go +++ b/internal/provider/aws/param/adapter.go @@ -287,10 +287,10 @@ func convertParameter(p *paramapi.Parameter) *model.Parameter { } return &model.Parameter{ - Name: lo.FromPtr(p.Name), - Value: lo.FromPtr(p.Value), - Version: version, - LastModified: p.LastModifiedDate, + Name: lo.FromPtr(p.Name), + Value: lo.FromPtr(p.Value), + Version: version, + UpdatedAt: p.LastModifiedDate, Metadata: model.AWSParameterMeta{ Type: string(p.Type), ARN: lo.FromPtr(p.ARN), @@ -308,11 +308,11 @@ func convertParameterHistory(name string, history []paramapi.ParameterHistory) * } params[i] = &model.Parameter{ - Name: name, - Value: lo.FromPtr(h.Value), - Version: version, - Description: lo.FromPtr(h.Description), - LastModified: h.LastModifiedDate, + Name: name, + Value: lo.FromPtr(h.Value), + Version: version, + Description: lo.FromPtr(h.Description), + UpdatedAt: h.LastModifiedDate, Metadata: model.AWSParameterMeta{ Type: string(h.Type), Tier: string(h.Tier), @@ -334,9 +334,9 @@ func convertParameterMetadata(m *paramapi.ParameterMetadata) *model.ParameterLis } return &model.ParameterListItem{ - Name: lo.FromPtr(m.Name), - Description: lo.FromPtr(m.Description), - LastModified: m.LastModifiedDate, + Name: lo.FromPtr(m.Name), + Description: lo.FromPtr(m.Description), + UpdatedAt: m.LastModifiedDate, Metadata: model.AWSParameterListItemMeta{ Type: string(m.Type), }, diff --git a/internal/provider/aws/secret/adapter.go b/internal/provider/aws/secret/adapter.go index d3fdb2bb..93331292 100644 --- a/internal/provider/aws/secret/adapter.go +++ b/internal/provider/aws/secret/adapter.go @@ -298,10 +298,10 @@ func convertGetSecretValueOutput(o *secretapi.GetSecretValueOutput) *model.Secre } return &model.Secret{ - Name: lo.FromPtr(o.Name), - Value: lo.FromPtr(o.SecretString), - VersionID: lo.FromPtr(o.VersionId), - CreatedDate: o.CreatedDate, + Name: lo.FromPtr(o.Name), + Value: lo.FromPtr(o.SecretString), + Version: lo.FromPtr(o.VersionId), + CreatedAt: o.CreatedDate, Metadata: model.AWSSecretMeta{ ARN: lo.FromPtr(o.ARN), VersionStages: o.VersionStages, @@ -315,8 +315,8 @@ func convertSecretVersion(v *secretapi.SecretVersionsListEntry) *model.SecretVer } return &model.SecretVersion{ - VersionID: lo.FromPtr(v.VersionId), - CreatedDate: v.CreatedDate, + Version: lo.FromPtr(v.VersionId), + CreatedAt: v.CreatedDate, Metadata: model.AWSSecretVersionMeta{ VersionStages: v.VersionStages, }, @@ -329,11 +329,11 @@ func convertSecretListEntry(e *secretapi.SecretListEntry) *model.SecretListItem } return &model.SecretListItem{ - Name: lo.FromPtr(e.Name), - Description: lo.FromPtr(e.Description), - CreatedDate: e.CreatedDate, - LastModified: e.LastChangedDate, - Tags: convertFromAWSTags(e.Tags), + Name: lo.FromPtr(e.Name), + Description: lo.FromPtr(e.Description), + CreatedAt: e.CreatedDate, + UpdatedAt: e.LastChangedDate, + Tags: convertFromAWSTags(e.Tags), Metadata: model.AWSSecretListItemMeta{ ARN: lo.FromPtr(e.ARN), DeletedDate: e.DeletedDate, @@ -347,11 +347,11 @@ func convertDescribeSecretOutput(o *secretapi.DescribeSecretOutput) *model.Secre } return &model.SecretListItem{ - Name: lo.FromPtr(o.Name), - Description: lo.FromPtr(o.Description), - CreatedDate: o.CreatedDate, - LastModified: o.LastChangedDate, - Tags: convertFromAWSTags(o.Tags), + Name: lo.FromPtr(o.Name), + Description: lo.FromPtr(o.Description), + CreatedAt: o.CreatedDate, + UpdatedAt: o.LastChangedDate, + Tags: convertFromAWSTags(o.Tags), Metadata: model.AWSSecretListItemMeta{ ARN: lo.FromPtr(o.ARN), DeletedDate: o.DeletedDate, diff --git a/internal/provider/secret_test.go b/internal/provider/secret_test.go index 1576c287..16855121 100644 --- a/internal/provider/secret_test.go +++ b/internal/provider/secret_test.go @@ -32,9 +32,9 @@ func TestWrapTypedSecretReader_GetSecret(t *testing.T) { mock := &mockTypedSecretReader{ getTypedSecretFunc: func(_ context.Context, name, versionID, _ string) (*model.TypedSecret[model.AWSSecretMeta], error) { return &model.TypedSecret[model.AWSSecretMeta]{ - Name: name, - Value: "test-value", - VersionID: versionID, + Name: name, + Value: "test-value", + Version: versionID, Metadata: model.AWSSecretMeta{ VersionStages: []string{"AWSCURRENT"}, }, @@ -48,7 +48,7 @@ func TestWrapTypedSecretReader_GetSecret(t *testing.T) { require.NoError(t, err) assert.Equal(t, "test-secret", secret.Name) assert.Equal(t, "test-value", secret.Value) - assert.Equal(t, "v1", secret.VersionID) + assert.Equal(t, "v1", secret.Version) }) t.Run("error", func(t *testing.T) { diff --git a/internal/staging/param.go b/internal/staging/param.go index 6fca4872..4eda7375 100644 --- a/internal/staging/param.go +++ b/internal/staging/param.go @@ -10,6 +10,8 @@ import ( "github.com/mpyw/suve/internal/api/paramapi" "github.com/mpyw/suve/internal/infra" + "github.com/mpyw/suve/internal/provider" + awsparam "github.com/mpyw/suve/internal/provider/aws/param" "github.com/mpyw/suve/internal/tagging" "github.com/mpyw/suve/internal/version/paramversion" ) @@ -28,9 +30,11 @@ type ParamClient interface { // ParamStrategy implements ServiceStrategy for SSM Parameter Store. type ParamStrategy struct { Client ParamClient + Reader provider.ParameterReader // for version resolution } // NewParamStrategy creates a new SSM Parameter Store strategy. +// Note: Reader must be set separately for version resolution to work. func NewParamStrategy(client ParamClient) *ParamStrategy { return &ParamStrategy{Client: client} } @@ -189,14 +193,14 @@ func (s *ParamStrategy) FetchLastModified(ctx context.Context, name string) (tim func (s *ParamStrategy) FetchCurrent(ctx context.Context, name string) (*FetchResult, error) { spec := ¶mversion.Spec{Name: name} - param, err := paramversion.GetParameterWithVersion(ctx, s.Client, spec) + param, err := paramversion.GetParameterWithVersion(ctx, s.Reader, spec) if err != nil { return nil, err } return &FetchResult{ - Value: lo.FromPtr(param.Value), - Identifier: fmt.Sprintf("#%d", param.Version), + Value: param.Value, + Identifier: "#" + param.Version, }, nil } @@ -248,7 +252,7 @@ func (s *ParamStrategy) ParseName(input string) (string, error) { func (s *ParamStrategy) FetchCurrentValue(ctx context.Context, name string) (*EditFetchResult, error) { spec := ¶mversion.Spec{Name: name} - param, err := paramversion.GetParameterWithVersion(ctx, s.Client, spec) + param, err := paramversion.GetParameterWithVersion(ctx, s.Reader, spec) if err != nil { if pnf := (*paramapi.ParameterNotFound)(nil); errors.As(err, &pnf) { return nil, &ResourceNotFoundError{Err: err} @@ -258,11 +262,11 @@ func (s *ParamStrategy) FetchCurrentValue(ctx context.Context, name string) (*Ed } result := &EditFetchResult{ - Value: lo.FromPtr(param.Value), + Value: param.Value, } - if param.LastModifiedDate != nil { - result.LastModified = *param.LastModifiedDate + if param.UpdatedAt != nil { + result.LastModified = *param.UpdatedAt } return result, nil @@ -287,22 +291,29 @@ func (s *ParamStrategy) FetchVersion(ctx context.Context, input string) (value s return "", "", err } - param, err := paramversion.GetParameterWithVersion(ctx, s.Client, spec) + param, err := paramversion.GetParameterWithVersion(ctx, s.Reader, spec) if err != nil { return "", "", err } - return lo.FromPtr(param.Value), fmt.Sprintf("#%d", param.Version), nil + return param.Value, "#" + param.Version, nil } // ParamFactory creates a FullStrategy with an initialized AWS client. func ParamFactory(ctx context.Context) (FullStrategy, error) { + // Create raw client for apply operations (paramapi interface) client, err := infra.NewParamClient(ctx) if err != nil { return nil, fmt.Errorf("failed to initialize AWS client: %w", err) } - return NewParamStrategy(client), nil + // Create adapter for version resolution (provider interface) + adapter := awsparam.New(client) + + return &ParamStrategy{ + Client: client, + Reader: adapter, + }, nil } // ParamParserFactory creates a Parser without an AWS client. diff --git a/internal/staging/param_test.go b/internal/staging/param_test.go index c4aa4010..c99f0421 100644 --- a/internal/staging/param_test.go +++ b/internal/staging/param_test.go @@ -3,6 +3,7 @@ package staging_test import ( "context" "errors" + "fmt" "testing" "time" @@ -12,6 +13,7 @@ import ( "github.com/mpyw/suve/internal/api/paramapi" "github.com/mpyw/suve/internal/maputil" + "github.com/mpyw/suve/internal/model" "github.com/mpyw/suve/internal/staging" ) @@ -109,6 +111,32 @@ func (m *paramMockClient) ListTagsForResource( return ¶mapi.ListTagsForResourceOutput{}, nil } +// paramReaderMock implements provider.ParameterReader for testing. +type paramReaderMock struct { + getParameterFunc func(ctx context.Context, name string, version string) (*model.Parameter, error) + getParameterHistoryFunc func(ctx context.Context, name string) (*model.ParameterHistory, error) +} + +func (m *paramReaderMock) GetParameter(ctx context.Context, name string, version string) (*model.Parameter, error) { + if m.getParameterFunc != nil { + return m.getParameterFunc(ctx, name, version) + } + + return nil, fmt.Errorf("GetParameter not mocked") +} + +func (m *paramReaderMock) GetParameterHistory(ctx context.Context, name string) (*model.ParameterHistory, error) { + if m.getParameterHistoryFunc != nil { + return m.getParameterHistoryFunc(ctx, name) + } + + return nil, fmt.Errorf("GetParameterHistory not mocked") +} + +func (m *paramReaderMock) ListParameters(_ context.Context, _ string, _ bool) ([]*model.ParameterListItem, error) { + return nil, fmt.Errorf("ListParameters not mocked") +} + func TestParamStrategy_BasicMethods(t *testing.T) { t.Parallel() @@ -326,19 +354,19 @@ func TestParamStrategy_FetchCurrent(t *testing.T) { t.Run("success", func(t *testing.T) { t.Parallel() - mock := ¶mMockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/param"), - Value: lo.ToPtr("current-value"), - Version: 5, - }, + reader := ¶mReaderMock{ + getParameterFunc: func(_ context.Context, name string, _ string) (*model.Parameter, error) { + assert.Equal(t, "/app/param", name) + + return &model.Parameter{ + Name: "/app/param", + Value: "current-value", + Version: "5", }, nil }, } - s := staging.NewParamStrategy(mock) + s := &staging.ParamStrategy{Reader: reader} result, err := s.FetchCurrent(t.Context(), "/app/param") require.NoError(t, err) assert.Equal(t, "current-value", result.Value) @@ -348,13 +376,13 @@ func TestParamStrategy_FetchCurrent(t *testing.T) { t.Run("error", func(t *testing.T) { t.Parallel() - mock := ¶mMockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { + reader := ¶mReaderMock{ + getParameterFunc: func(_ context.Context, _ string, _ string) (*model.Parameter, error) { return nil, errors.New("not found") }, } - s := staging.NewParamStrategy(mock) + s := &staging.ParamStrategy{Reader: reader} _, err := s.FetchCurrent(t.Context(), "/app/param") require.Error(t, err) }) @@ -413,18 +441,20 @@ func TestParamStrategy_FetchCurrentValue(t *testing.T) { t.Run("success", func(t *testing.T) { t.Parallel() - mock := ¶mMockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Value: lo.ToPtr("fetched-value"), - LastModifiedDate: &now, - }, + reader := ¶mReaderMock{ + getParameterFunc: func(_ context.Context, name string, _ string) (*model.Parameter, error) { + assert.Equal(t, "/app/param", name) + + return &model.Parameter{ + Name: "/app/param", + Value: "fetched-value", + Version: "1", + UpdatedAt: &now, }, nil }, } - s := staging.NewParamStrategy(mock) + s := &staging.ParamStrategy{Reader: reader} result, err := s.FetchCurrentValue(t.Context(), "/app/param") require.NoError(t, err) assert.Equal(t, "fetched-value", result.Value) @@ -434,13 +464,13 @@ func TestParamStrategy_FetchCurrentValue(t *testing.T) { t.Run("error", func(t *testing.T) { t.Parallel() - mock := ¶mMockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { + reader := ¶mReaderMock{ + getParameterFunc: func(_ context.Context, _ string, _ string) (*model.Parameter, error) { return nil, errors.New("fetch error") }, } - s := staging.NewParamStrategy(mock) + s := &staging.ParamStrategy{Reader: reader} _, err := s.FetchCurrentValue(t.Context(), "/app/param") require.Error(t, err) }) @@ -492,20 +522,20 @@ func TestParamStrategy_FetchVersion(t *testing.T) { t.Run("success with version", func(t *testing.T) { t.Parallel() - mock := ¶mMockClient{ - getParameterFunc: func(_ context.Context, params *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - // Version selector uses GetParameter with name:version format - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: params.Name, - Value: lo.ToPtr("v2"), - Version: 2, - }, + reader := ¶mReaderMock{ + getParameterFunc: func(_ context.Context, name string, version string) (*model.Parameter, error) { + assert.Equal(t, "/app/param", name) + assert.Equal(t, "2", version) + + return &model.Parameter{ + Name: "/app/param", + Value: "v2", + Version: "2", }, nil }, } - s := staging.NewParamStrategy(mock) + s := &staging.ParamStrategy{Reader: reader} value, label, err := s.FetchVersion(t.Context(), "/app/param#2") require.NoError(t, err) assert.Equal(t, "v2", value) @@ -515,21 +545,22 @@ func TestParamStrategy_FetchVersion(t *testing.T) { t.Run("success with shift", func(t *testing.T) { t.Parallel() - mock := ¶mMockClient{ - getParameterHistoryFunc: func( - _ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options), - ) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Version: 1, Value: lo.ToPtr("v1")}, - {Version: 2, Value: lo.ToPtr("v2")}, - {Version: 3, Value: lo.ToPtr("v3")}, + reader := ¶mReaderMock{ + getParameterHistoryFunc: func(_ context.Context, name string) (*model.ParameterHistory, error) { + assert.Equal(t, "/app/param", name) + + return &model.ParameterHistory{ + Name: "/app/param", + Parameters: []*model.Parameter{ + {Name: "/app/param", Version: "1", Value: "v1"}, + {Name: "/app/param", Version: "2", Value: "v2"}, + {Name: "/app/param", Version: "3", Value: "v3"}, }, }, nil }, } - s := staging.NewParamStrategy(mock) + s := &staging.ParamStrategy{Reader: reader} value, label, err := s.FetchVersion(t.Context(), "/app/param~1") require.NoError(t, err) assert.Equal(t, "v2", value) @@ -547,15 +578,13 @@ func TestParamStrategy_FetchVersion(t *testing.T) { t.Run("fetch error", func(t *testing.T) { t.Parallel() - mock := ¶mMockClient{ - getParameterHistoryFunc: func( - _ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options), - ) (*paramapi.GetParameterHistoryOutput, error) { + reader := ¶mReaderMock{ + getParameterFunc: func(_ context.Context, _ string, _ string) (*model.Parameter, error) { return nil, errors.New("fetch error") }, } - s := staging.NewParamStrategy(mock) + s := &staging.ParamStrategy{Reader: reader} _, _, err := s.FetchVersion(t.Context(), "/app/param#2") require.Error(t, err) }) @@ -859,18 +888,20 @@ func TestParamStrategy_ApplyTags(t *testing.T) { func TestParamStrategy_FetchCurrentValue_NoLastModified(t *testing.T) { t.Parallel() - mock := ¶mMockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Value: lo.ToPtr("value"), - LastModifiedDate: nil, - }, + reader := ¶mReaderMock{ + getParameterFunc: func(_ context.Context, name string, _ string) (*model.Parameter, error) { + assert.Equal(t, "/app/param", name) + + return &model.Parameter{ + Name: "/app/param", + Value: "value", + Version: "1", + UpdatedAt: nil, }, nil }, } - s := staging.NewParamStrategy(mock) + s := &staging.ParamStrategy{Reader: reader} result, err := s.FetchCurrentValue(t.Context(), "/app/param") require.NoError(t, err) assert.Equal(t, "value", result.Value) diff --git a/internal/usecase/param/diff.go b/internal/usecase/param/diff.go index aa3bb18f..12375593 100644 --- a/internal/usecase/param/diff.go +++ b/internal/usecase/param/diff.go @@ -2,17 +2,17 @@ package param import ( "context" + "strconv" - "github.com/samber/lo" - - "github.com/mpyw/suve/internal/api/paramapi" + "github.com/mpyw/suve/internal/provider" "github.com/mpyw/suve/internal/version/paramversion" ) // DiffClient is the interface for the diff use case. +// +//nolint:iface // Intentionally aliases ParameterReader for type clarity in DiffUseCase. type DiffClient interface { - paramapi.GetParameterAPI - paramapi.GetParameterHistoryAPI + provider.ParameterReader } // DiffInput holds input for the diff use case. @@ -48,12 +48,15 @@ func (u *DiffUseCase) Execute(ctx context.Context, input DiffInput) (*DiffOutput return nil, err } + oldVersion, _ := strconv.ParseInt(param1.Version, 10, 64) + newVersion, _ := strconv.ParseInt(param2.Version, 10, 64) + return &DiffOutput{ - OldName: lo.FromPtr(param1.Name), - OldVersion: param1.Version, - OldValue: lo.FromPtr(param1.Value), - NewName: lo.FromPtr(param2.Name), - NewVersion: param2.Version, - NewValue: lo.FromPtr(param2.Value), + OldName: param1.Name, + OldVersion: oldVersion, + OldValue: param1.Value, + NewName: param2.Name, + NewVersion: newVersion, + NewValue: param2.Value, }, nil } diff --git a/internal/usecase/param/diff_test.go b/internal/usecase/param/diff_test.go index 1c39cb50..6fab4458 100644 --- a/internal/usecase/param/diff_test.go +++ b/internal/usecase/param/diff_test.go @@ -2,63 +2,62 @@ package param_test import ( "context" + "fmt" "testing" - "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mpyw/suve/internal/api/paramapi" + "github.com/mpyw/suve/internal/model" "github.com/mpyw/suve/internal/usecase/param" "github.com/mpyw/suve/internal/version/paramversion" ) type mockDiffClient struct { - getParameterResults []*paramapi.GetParameterOutput - getParameterErrs []error - getParameterCalls int - // historyParams stores the base data; each call returns a fresh copy - historyParams []paramapi.ParameterHistory - getHistoryErr error + getParameterFunc func(ctx context.Context, name string, version string) (*model.Parameter, error) + getParameterHistoryFunc func(ctx context.Context, name string) (*model.ParameterHistory, error) } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockDiffClient) GetParameter(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - idx := m.getParameterCalls - m.getParameterCalls++ - - if idx < len(m.getParameterErrs) && m.getParameterErrs[idx] != nil { - return nil, m.getParameterErrs[idx] - } - - if idx < len(m.getParameterResults) { - return m.getParameterResults[idx], nil +func (m *mockDiffClient) GetParameter(ctx context.Context, name string, version string) (*model.Parameter, error) { + if m.getParameterFunc != nil { + return m.getParameterFunc(ctx, name, version) } - return nil, errUnexpectedCall + return nil, fmt.Errorf("GetParameter not mocked") } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockDiffClient) GetParameterHistory(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - if m.getHistoryErr != nil { - return nil, m.getHistoryErr +func (m *mockDiffClient) GetParameterHistory(ctx context.Context, name string) (*model.ParameterHistory, error) { + if m.getParameterHistoryFunc != nil { + return m.getParameterHistoryFunc(ctx, name) } - // Return a fresh copy to avoid in-place mutations affecting subsequent calls - params := make([]paramapi.ParameterHistory, len(m.historyParams)) - copy(params, m.historyParams) + return nil, fmt.Errorf("GetParameterHistory not mocked") +} - return ¶mapi.GetParameterHistoryOutput{Parameters: params}, nil +func (m *mockDiffClient) ListParameters(_ context.Context, _ string, _ bool) ([]*model.ParameterListItem, error) { + return nil, fmt.Errorf("ListParameters not mocked") } func TestDiffUseCase_Execute(t *testing.T) { t.Parallel() // #VERSION specs without shift use GetParameter (with name:version format) + callCount := 0 client := &mockDiffClient{ - getParameterResults: []*paramapi.GetParameterOutput{ - {Parameter: ¶mapi.Parameter{Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("old-value"), Version: 1}}, - {Parameter: ¶mapi.Parameter{Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("new-value"), Version: 2}}, + getParameterFunc: func(_ context.Context, name string, version string) (*model.Parameter, error) { + callCount++ + + assert.Equal(t, "/app/config", name) + + if callCount == 1 { + assert.Equal(t, "1", version) + + return &model.Parameter{Name: "/app/config", Value: "old-value", Version: "1"}, nil + } + + assert.Equal(t, "2", version) + + return &model.Parameter{Name: "/app/config", Value: "new-value", Version: "2"}, nil }, } @@ -84,7 +83,9 @@ func TestDiffUseCase_Execute_Spec1Error(t *testing.T) { t.Parallel() client := &mockDiffClient{ - getParameterErrs: []error{errGetParameter}, + getParameterFunc: func(_ context.Context, _ string, _ string) (*model.Parameter, error) { + return nil, errGetParameter + }, } uc := ¶m.DiffUseCase{Client: client} @@ -102,11 +103,16 @@ func TestDiffUseCase_Execute_Spec1Error(t *testing.T) { func TestDiffUseCase_Execute_Spec2Error(t *testing.T) { t.Parallel() + callCount := 0 client := &mockDiffClient{ - getParameterResults: []*paramapi.GetParameterOutput{ - {Parameter: ¶mapi.Parameter{Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("old-value"), Version: 1}}, + getParameterFunc: func(_ context.Context, _ string, _ string) (*model.Parameter, error) { + callCount++ + if callCount == 1 { + return &model.Parameter{Name: "/app/config", Value: "old-value", Version: "1"}, nil + } + + return nil, errGetParameter }, - getParameterErrs: []error{nil, errGetParameter}, } uc := ¶m.DiffUseCase{Client: client} @@ -125,10 +131,19 @@ func TestDiffUseCase_Execute_WithLatest(t *testing.T) { t.Parallel() // Both specs without shift use GetParameter + callCount := 0 client := &mockDiffClient{ - getParameterResults: []*paramapi.GetParameterOutput{ - {Parameter: ¶mapi.Parameter{Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("old-value"), Version: 3}}, - {Parameter: ¶mapi.Parameter{Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("latest-value"), Version: 5}}, + getParameterFunc: func(_ context.Context, _ string, version string) (*model.Parameter, error) { + callCount++ + if callCount == 1 { + assert.Equal(t, "3", version) + + return &model.Parameter{Name: "/app/config", Value: "old-value", Version: "3"}, nil + } + + assert.Empty(t, version) // latest + + return &model.Parameter{Name: "/app/config", Value: "latest-value", Version: "5"}, nil }, } @@ -151,10 +166,17 @@ func TestDiffUseCase_Execute_WithShift(t *testing.T) { // Specs with shift use GetParameterHistory client := &mockDiffClient{ - historyParams: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v1"), Version: 1}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v2"), Version: 2}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v3"), Version: 3}, + getParameterHistoryFunc: func(_ context.Context, name string) (*model.ParameterHistory, error) { + assert.Equal(t, "/app/config", name) + + return &model.ParameterHistory{ + Name: "/app/config", + Parameters: []*model.Parameter{ + {Name: "/app/config", Value: "v1", Version: "1"}, + {Name: "/app/config", Value: "v2", Version: "2"}, + {Name: "/app/config", Value: "v3", Version: "3"}, + }, + }, nil }, } @@ -178,7 +200,9 @@ func TestDiffUseCase_Execute_WithShift_Error(t *testing.T) { t.Parallel() client := &mockDiffClient{ - getHistoryErr: errHistoryFailed, + getParameterHistoryFunc: func(_ context.Context, _ string) (*model.ParameterHistory, error) { + return nil, errHistoryFailed + }, } uc := ¶m.DiffUseCase{Client: client} diff --git a/internal/usecase/param/helper_test.go b/internal/usecase/param/helper_test.go index 09d954aa..4f7a03ed 100644 --- a/internal/usecase/param/helper_test.go +++ b/internal/usecase/param/helper_test.go @@ -12,5 +12,4 @@ var ( errAddTagsFailed = errors.New("add tags failed") errRemoveTagsFailed = errors.New("remove tags failed") errAccessDenied = errors.New("access denied") - errUnexpectedCall = errors.New("unexpected GetParameter call") ) diff --git a/internal/usecase/param/log.go b/internal/usecase/param/log.go index 2f3476c7..8b3e2e32 100644 --- a/internal/usecase/param/log.go +++ b/internal/usecase/param/log.go @@ -4,16 +4,18 @@ import ( "context" "fmt" "slices" + "strconv" "time" - "github.com/samber/lo" - - "github.com/mpyw/suve/internal/api/paramapi" + "github.com/mpyw/suve/internal/model" + "github.com/mpyw/suve/internal/provider" ) // LogClient is the interface for the log use case. +// +//nolint:iface // Intentionally aliases ParameterReader for type clarity in LogUseCase. type LogClient interface { - paramapi.GetParameterHistoryAPI + provider.ParameterReader } // LogInput holds input for the log use case. @@ -27,11 +29,11 @@ type LogInput struct { // LogEntry represents a single version entry. type LogEntry struct { - Version int64 - Type paramapi.ParameterType - Value string - LastModified *time.Time - IsCurrent bool + Version string + Type string // Parameter type (e.g., "String", "SecureString") + Value string + UpdatedAt *time.Time + IsCurrent bool } // LogOutput holds the result of the log use case. @@ -47,63 +49,97 @@ type LogUseCase struct { // Execute runs the log use case. func (u *LogUseCase) Execute(ctx context.Context, input LogInput) (*LogOutput, error) { - result, err := u.Client.GetParameterHistory(ctx, ¶mapi.GetParameterHistoryInput{ - Name: lo.ToPtr(input.Name), - WithDecryption: lo.ToPtr(true), - MaxResults: lo.ToPtr(input.MaxResults), - }) + history, err := u.Client.GetParameterHistory(ctx, input.Name) if err != nil { return nil, fmt.Errorf("failed to get parameter history: %w", err) } - params := result.Parameters + params := history.Parameters if len(params) == 0 { return &LogOutput{Name: input.Name}, nil } - // Find max version using lo.MaxBy - maxVersion := lo.MaxBy(params, func(a, b paramapi.ParameterHistory) bool { - return a.Version > b.Version - }).Version - - // Apply date filters using lo.Filter - filtered := lo.Filter(params, func(h paramapi.ParameterHistory, _ int) bool { - // Skip entries without LastModifiedDate when date filters are applied - if input.Since != nil || input.Until != nil { - if h.LastModifiedDate == nil { - return false - } + // Find max version for IsCurrent flag + maxVersion := findMaxVersion(params) - if input.Since != nil && h.LastModifiedDate.Before(*input.Since) { - return false - } + // Apply date filters + filtered := filterByDate(params, input.Since, input.Until) - if input.Until != nil && h.LastModifiedDate.After(*input.Until) { - return false - } + // Convert to entries + entries := make([]LogEntry, len(filtered)) + for i, p := range filtered { + entry := LogEntry{ + Version: p.Version, + Value: p.Value, + UpdatedAt: p.UpdatedAt, + IsCurrent: p.Version == maxVersion, } - return true - }) - - // Convert to entries using lo.Map - entries := lo.Map(filtered, func(h paramapi.ParameterHistory, _ int) LogEntry { - return LogEntry{ - Version: h.Version, - Type: h.Type, - Value: lo.FromPtr(h.Value), - LastModified: h.LastModifiedDate, - IsCurrent: h.Version == maxVersion, + // Extract Type from AWS metadata if available + if meta := p.AWSMeta(); meta != nil { + entry.Type = meta.Type } - }) + + entries[i] = entry + } // AWS returns oldest first; reverse to show newest first (unless --reverse) if !input.Reverse { slices.Reverse(entries) } + // Apply MaxResults limit after sorting + if input.MaxResults > 0 && len(entries) > int(input.MaxResults) { + entries = entries[:input.MaxResults] + } + return &LogOutput{ Name: input.Name, Entries: entries, }, nil } + +// findMaxVersion returns the maximum version string from the parameters. +func findMaxVersion(params []*model.Parameter) string { + maxVersion := "" + maxVersionNum := int64(-1) + + for _, p := range params { + if v, err := strconv.ParseInt(p.Version, 10, 64); err == nil { + if v > maxVersionNum { + maxVersionNum = v + maxVersion = p.Version + } + } + } + + return maxVersion +} + +// filterByDate filters parameters by modification date range. +func filterByDate(params []*model.Parameter, since, until *time.Time) []*model.Parameter { + if since == nil && until == nil { + return params + } + + filtered := make([]*model.Parameter, 0, len(params)) + + for _, p := range params { + // Skip entries without LastModified when date filters are applied + if p.UpdatedAt == nil { + continue + } + + if since != nil && p.UpdatedAt.Before(*since) { + continue + } + + if until != nil && p.UpdatedAt.After(*until) { + continue + } + + filtered = append(filtered, p) + } + + return filtered +} diff --git a/internal/usecase/param/log_test.go b/internal/usecase/param/log_test.go index cfc6f1f9..30113620 100644 --- a/internal/usecase/param/log_test.go +++ b/internal/usecase/param/log_test.go @@ -5,46 +5,71 @@ import ( "testing" "time" - "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mpyw/suve/internal/api/paramapi" + "github.com/mpyw/suve/internal/model" "github.com/mpyw/suve/internal/usecase/param" ) type mockLogClient struct { - getHistoryResult *paramapi.GetParameterHistoryOutput - getHistoryErr error + getParameterResult *model.Parameter + getParameterErr error + getHistoryResult *model.ParameterHistory + getHistoryErr error + listParametersErr error } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockLogClient) GetParameterHistory(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { +func (m *mockLogClient) GetParameter(_ context.Context, _ string, _ string) (*model.Parameter, error) { + if m.getParameterErr != nil { + return nil, m.getParameterErr + } + + return m.getParameterResult, nil +} + +func (m *mockLogClient) GetParameterHistory(_ context.Context, _ string) (*model.ParameterHistory, error) { if m.getHistoryErr != nil { return nil, m.getHistoryErr } + if m.getHistoryResult == nil { + return &model.ParameterHistory{}, nil + } + return m.getHistoryResult, nil } +func (m *mockLogClient) ListParameters(_ context.Context, _ string, _ bool) ([]*model.ParameterListItem, error) { + if m.listParametersErr != nil { + return nil, m.listParametersErr + } + + return nil, nil +} + func TestLogUseCase_Execute(t *testing.T) { t.Parallel() now := time.Now() client := &mockLogClient{ - getHistoryResult: ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ + getHistoryResult: &model.ParameterHistory{ + Name: "/app/config", + Parameters: []*model.Parameter{ { - Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v1"), Version: 1, - Type: paramapi.ParameterTypeString, LastModifiedDate: lo.ToPtr(now.Add(-2 * time.Hour)), + Name: "/app/config", Value: "v1", Version: "1", + UpdatedAt: timePtr(now.Add(-2 * time.Hour)), + Metadata: model.AWSParameterMeta{Type: "String"}, }, { - Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v2"), Version: 2, - Type: paramapi.ParameterTypeString, LastModifiedDate: lo.ToPtr(now.Add(-1 * time.Hour)), + Name: "/app/config", Value: "v2", Version: "2", + UpdatedAt: timePtr(now.Add(-1 * time.Hour)), + Metadata: model.AWSParameterMeta{Type: "String"}, }, { - Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v3"), Version: 3, - Type: paramapi.ParameterTypeString, LastModifiedDate: lo.ToPtr(now), + Name: "/app/config", Value: "v3", Version: "3", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{Type: "String"}, }, }, }, @@ -60,9 +85,9 @@ func TestLogUseCase_Execute(t *testing.T) { assert.Len(t, output.Entries, 3) // Newest first (default order) - assert.Equal(t, int64(3), output.Entries[0].Version) - assert.Equal(t, int64(2), output.Entries[1].Version) - assert.Equal(t, int64(1), output.Entries[2].Version) + assert.Equal(t, "3", output.Entries[0].Version) + assert.Equal(t, "2", output.Entries[1].Version) + assert.Equal(t, "1", output.Entries[2].Version) // IsCurrent flag assert.True(t, output.Entries[0].IsCurrent) @@ -74,8 +99,9 @@ func TestLogUseCase_Execute_Empty(t *testing.T) { t.Parallel() client := &mockLogClient{ - getHistoryResult: ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/config", + Parameters: []*model.Parameter{}, }, } @@ -110,11 +136,12 @@ func TestLogUseCase_Execute_Reverse(t *testing.T) { now := time.Now() client := &mockLogClient{ - getHistoryResult: ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-2 * time.Hour))}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now.Add(-1 * time.Hour))}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v3"), Version: 3, LastModifiedDate: lo.ToPtr(now)}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/config", + Parameters: []*model.Parameter{ + {Name: "/app/config", Value: "v1", Version: "1", UpdatedAt: timePtr(now.Add(-2 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v2", Version: "2", UpdatedAt: timePtr(now.Add(-1 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v3", Version: "3", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, }, }, } @@ -128,9 +155,9 @@ func TestLogUseCase_Execute_Reverse(t *testing.T) { require.NoError(t, err) // Oldest first when Reverse is true (keeps AWS order) - assert.Equal(t, int64(1), output.Entries[0].Version) - assert.Equal(t, int64(2), output.Entries[1].Version) - assert.Equal(t, int64(3), output.Entries[2].Version) + assert.Equal(t, "1", output.Entries[0].Version) + assert.Equal(t, "2", output.Entries[1].Version) + assert.Equal(t, "3", output.Entries[2].Version) } func TestLogUseCase_Execute_SinceFilter(t *testing.T) { @@ -138,11 +165,12 @@ func TestLogUseCase_Execute_SinceFilter(t *testing.T) { now := time.Now() client := &mockLogClient{ - getHistoryResult: ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-3 * time.Hour))}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now.Add(-1 * time.Hour))}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v3"), Version: 3, LastModifiedDate: lo.ToPtr(now)}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/config", + Parameters: []*model.Parameter{ + {Name: "/app/config", Value: "v1", Version: "1", UpdatedAt: timePtr(now.Add(-3 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v2", Version: "2", UpdatedAt: timePtr(now.Add(-1 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v3", Version: "3", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, }, }, } @@ -158,8 +186,8 @@ func TestLogUseCase_Execute_SinceFilter(t *testing.T) { // v1 is before the since filter, so only v2 and v3 should be included assert.Len(t, output.Entries, 2) - assert.Equal(t, int64(3), output.Entries[0].Version) - assert.Equal(t, int64(2), output.Entries[1].Version) + assert.Equal(t, "3", output.Entries[0].Version) + assert.Equal(t, "2", output.Entries[1].Version) } func TestLogUseCase_Execute_UntilFilter(t *testing.T) { @@ -167,11 +195,12 @@ func TestLogUseCase_Execute_UntilFilter(t *testing.T) { now := time.Now() client := &mockLogClient{ - getHistoryResult: ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-3 * time.Hour))}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now.Add(-1 * time.Hour))}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v3"), Version: 3, LastModifiedDate: lo.ToPtr(now)}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/config", + Parameters: []*model.Parameter{ + {Name: "/app/config", Value: "v1", Version: "1", UpdatedAt: timePtr(now.Add(-3 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v2", Version: "2", UpdatedAt: timePtr(now.Add(-1 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v3", Version: "3", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, }, }, } @@ -187,8 +216,8 @@ func TestLogUseCase_Execute_UntilFilter(t *testing.T) { // v3 is after the until filter, so only v1 and v2 should be included assert.Len(t, output.Entries, 2) - assert.Equal(t, int64(2), output.Entries[0].Version) - assert.Equal(t, int64(1), output.Entries[1].Version) + assert.Equal(t, "2", output.Entries[0].Version) + assert.Equal(t, "1", output.Entries[1].Version) } func TestLogUseCase_Execute_DateRangeFilter(t *testing.T) { @@ -196,11 +225,12 @@ func TestLogUseCase_Execute_DateRangeFilter(t *testing.T) { now := time.Now() client := &mockLogClient{ - getHistoryResult: ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-4 * time.Hour))}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now.Add(-2 * time.Hour))}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v3"), Version: 3, LastModifiedDate: lo.ToPtr(now)}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/config", + Parameters: []*model.Parameter{ + {Name: "/app/config", Value: "v1", Version: "1", UpdatedAt: timePtr(now.Add(-4 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v2", Version: "2", UpdatedAt: timePtr(now.Add(-2 * time.Hour)), Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v3", Version: "3", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, }, }, } @@ -218,16 +248,17 @@ func TestLogUseCase_Execute_DateRangeFilter(t *testing.T) { // Only v2 should be within the range assert.Len(t, output.Entries, 1) - assert.Equal(t, int64(2), output.Entries[0].Version) + assert.Equal(t, "2", output.Entries[0].Version) } func TestLogUseCase_Execute_NoLastModifiedDate(t *testing.T) { t.Parallel() client := &mockLogClient{ - getHistoryResult: ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: nil}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/config", + Parameters: []*model.Parameter{ + {Name: "/app/config", Value: "v1", Version: "1", UpdatedAt: nil, Metadata: model.AWSParameterMeta{Type: "String"}}, }, }, } @@ -239,7 +270,7 @@ func TestLogUseCase_Execute_NoLastModifiedDate(t *testing.T) { }) require.NoError(t, err) assert.Len(t, output.Entries, 1) - assert.Nil(t, output.Entries[0].LastModified) + assert.Nil(t, output.Entries[0].UpdatedAt) } func TestLogUseCase_Execute_FilterWithNilLastModifiedDate(t *testing.T) { @@ -247,10 +278,11 @@ func TestLogUseCase_Execute_FilterWithNilLastModifiedDate(t *testing.T) { now := time.Now() client := &mockLogClient{ - getHistoryResult: ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: nil}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now)}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/config", + Parameters: []*model.Parameter{ + {Name: "/app/config", Value: "v1", Version: "1", UpdatedAt: nil, Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v2", Version: "2", UpdatedAt: &now, Metadata: model.AWSParameterMeta{Type: "String"}}, }, }, } @@ -264,7 +296,11 @@ func TestLogUseCase_Execute_FilterWithNilLastModifiedDate(t *testing.T) { }) require.NoError(t, err) - // v1 has nil LastModifiedDate, so it is skipped when date filter is applied; only v2 remains + // v1 has nil LastModified, so it is skipped when date filter is applied; only v2 remains assert.Len(t, output.Entries, 1) - assert.Equal(t, int64(2), output.Entries[0].Version) + assert.Equal(t, "2", output.Entries[0].Version) +} + +func timePtr(t time.Time) *time.Time { + return &t } diff --git a/internal/usecase/param/show.go b/internal/usecase/param/show.go index 741567f7..eec4c793 100644 --- a/internal/usecase/param/show.go +++ b/internal/usecase/param/show.go @@ -5,17 +5,14 @@ import ( "context" "time" - "github.com/samber/lo" - - "github.com/mpyw/suve/internal/api/paramapi" + "github.com/mpyw/suve/internal/provider" "github.com/mpyw/suve/internal/version/paramversion" ) // ShowClient is the interface for the show use case. type ShowClient interface { - paramapi.GetParameterAPI - paramapi.GetParameterHistoryAPI - paramapi.ListTagsForResourceAPI + provider.ParameterReader + provider.ParameterTagger } // ShowInput holds input for the show use case. @@ -31,13 +28,13 @@ type ShowTag struct { // ShowOutput holds the result of the show use case. type ShowOutput struct { - Name string - Value string - Version int64 - Type paramapi.ParameterType - Description string - LastModified *time.Time - Tags []ShowTag + Name string + Value string + Version string + Type string // Parameter type (e.g., "String", "SecureString") + Description string + UpdatedAt *time.Time + Tags []ShowTag } // ShowUseCase executes show operations. @@ -53,28 +50,24 @@ func (u *ShowUseCase) Execute(ctx context.Context, input ShowInput) (*ShowOutput } output := &ShowOutput{ - Name: lo.FromPtr(param.Name), - Value: lo.FromPtr(param.Value), + Name: param.Name, + Value: param.Value, Version: param.Version, - Type: param.Type, - Description: lo.FromPtr(param.Description), + Description: param.Description, + UpdatedAt: param.UpdatedAt, } - if param.LastModifiedDate != nil { - output.LastModified = param.LastModifiedDate + + // Extract Type from AWS metadata if available + if meta := param.AWSMeta(); meta != nil { + output.Type = meta.Type } // Fetch tags - tagsOutput, err := u.Client.ListTagsForResource(ctx, ¶mapi.ListTagsForResourceInput{ - ResourceType: paramapi.ResourceTypeForTaggingParameter, - ResourceId: param.Name, - }) - if err == nil && tagsOutput != nil { - output.Tags = lo.Map(tagsOutput.TagList, func(tag paramapi.Tag, _ int) ShowTag { - return ShowTag{ - Key: lo.FromPtr(tag.Key), - Value: lo.FromPtr(tag.Value), - } - }) + tags, err := u.Client.GetTags(ctx, param.Name) + if err == nil && tags != nil { + for k, v := range tags { + output.Tags = append(output.Tags, ShowTag{Key: k, Value: v}) + } } return output, nil diff --git a/internal/usecase/param/show_test.go b/internal/usecase/param/show_test.go index a217772f..afab7d4e 100644 --- a/internal/usecase/param/show_test.go +++ b/internal/usecase/param/show_test.go @@ -5,26 +5,25 @@ import ( "testing" "time" - "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mpyw/suve/internal/api/paramapi" + "github.com/mpyw/suve/internal/model" "github.com/mpyw/suve/internal/usecase/param" "github.com/mpyw/suve/internal/version/paramversion" ) type mockShowClient struct { - getParameterResult *paramapi.GetParameterOutput + getParameterResult *model.Parameter getParameterErr error - getHistoryResult *paramapi.GetParameterHistoryOutput + getHistoryResult *model.ParameterHistory getHistoryErr error - listTagsResult *paramapi.ListTagsForResourceOutput - listTagsErr error + listParametersErr error + getTagsResult map[string]string + getTagsErr error } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockShowClient) GetParameter(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { +func (m *mockShowClient) GetParameter(_ context.Context, _ string, _ string) (*model.Parameter, error) { if m.getParameterErr != nil { return nil, m.getParameterErr } @@ -32,30 +31,40 @@ func (m *mockShowClient) GetParameter(_ context.Context, _ *paramapi.GetParamete return m.getParameterResult, nil } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockShowClient) GetParameterHistory(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { +func (m *mockShowClient) GetParameterHistory(_ context.Context, _ string) (*model.ParameterHistory, error) { if m.getHistoryErr != nil { return nil, m.getHistoryErr } if m.getHistoryResult == nil { - return ¶mapi.GetParameterHistoryOutput{}, nil + return &model.ParameterHistory{}, nil } return m.getHistoryResult, nil } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockShowClient) ListTagsForResource(_ context.Context, _ *paramapi.ListTagsForResourceInput, _ ...func(*paramapi.Options)) (*paramapi.ListTagsForResourceOutput, error) { - if m.listTagsErr != nil { - return nil, m.listTagsErr +func (m *mockShowClient) ListParameters(_ context.Context, _ string, _ bool) ([]*model.ParameterListItem, error) { + if m.listParametersErr != nil { + return nil, m.listParametersErr } - if m.listTagsResult != nil { - return m.listTagsResult, nil + return nil, nil +} + +func (m *mockShowClient) GetTags(_ context.Context, _ string) (map[string]string, error) { + if m.getTagsErr != nil { + return nil, m.getTagsErr } - return ¶mapi.ListTagsForResourceOutput{}, nil + return m.getTagsResult, nil +} + +func (m *mockShowClient) AddTags(_ context.Context, _ string, _ map[string]string) error { + return nil +} + +func (m *mockShowClient) RemoveTags(_ context.Context, _ string, _ []string) error { + return nil } func TestShowUseCase_Execute(t *testing.T) { @@ -63,13 +72,13 @@ func TestShowUseCase_Execute(t *testing.T) { now := time.Now() client := &mockShowClient{ - getParameterResult: ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/config"), - Value: lo.ToPtr("secret-value"), - Version: 5, - Type: paramapi.ParameterTypeSecureString, - LastModifiedDate: &now, + getParameterResult: &model.Parameter{ + Name: "/app/config", + Value: "secret-value", + Version: "5", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{ + Type: "SecureString", }, }, } @@ -85,9 +94,9 @@ func TestShowUseCase_Execute(t *testing.T) { require.NoError(t, err) assert.Equal(t, "/app/config", output.Name) assert.Equal(t, "secret-value", output.Value) - assert.Equal(t, int64(5), output.Version) - assert.Equal(t, paramapi.ParameterTypeSecureString, output.Type) - assert.NotNil(t, output.LastModified) + assert.Equal(t, "5", output.Version) + assert.Equal(t, "SecureString", output.Type) + assert.NotNil(t, output.UpdatedAt) } func TestShowUseCase_Execute_WithVersion(t *testing.T) { @@ -95,12 +104,12 @@ func TestShowUseCase_Execute_WithVersion(t *testing.T) { // #VERSION spec without shift uses GetParameter (SSM supports name:version format) client := &mockShowClient{ - getParameterResult: ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/config"), - Value: lo.ToPtr("old-value"), - Version: 3, - Type: paramapi.ParameterTypeString, + getParameterResult: &model.Parameter{ + Name: "/app/config", + Value: "old-value", + Version: "3", + Metadata: model.AWSParameterMeta{ + Type: "String", }, }, } @@ -116,7 +125,7 @@ func TestShowUseCase_Execute_WithVersion(t *testing.T) { require.NoError(t, err) assert.Equal(t, "/app/config", output.Name) assert.Equal(t, "old-value", output.Value) - assert.Equal(t, int64(3), output.Version) + assert.Equal(t, "3", output.Version) } func TestShowUseCase_Execute_WithShift(t *testing.T) { @@ -124,11 +133,12 @@ func TestShowUseCase_Execute_WithShift(t *testing.T) { // Spec with shift uses GetParameterHistory client := &mockShowClient{ - getHistoryResult: ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v1"), Version: 1, Type: paramapi.ParameterTypeString}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v2"), Version: 2, Type: paramapi.ParameterTypeString}, - {Name: lo.ToPtr("/app/config"), Value: lo.ToPtr("v3"), Version: 3, Type: paramapi.ParameterTypeString}, + getHistoryResult: &model.ParameterHistory{ + Name: "/app/config", + Parameters: []*model.Parameter{ + {Name: "/app/config", Value: "v1", Version: "1", Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v2", Version: "2", Metadata: model.AWSParameterMeta{Type: "String"}}, + {Name: "/app/config", Value: "v3", Version: "3", Metadata: model.AWSParameterMeta{Type: "String"}}, }, }, } @@ -144,7 +154,7 @@ func TestShowUseCase_Execute_WithShift(t *testing.T) { require.NoError(t, err) assert.Equal(t, "/app/config", output.Name) assert.Equal(t, "v2", output.Value) // v3 - 1 = v2 - assert.Equal(t, int64(2), output.Version) + assert.Equal(t, "2", output.Version) } func TestShowUseCase_Execute_Error(t *testing.T) { @@ -165,16 +175,16 @@ func TestShowUseCase_Execute_Error(t *testing.T) { require.Error(t, err) } -func TestShowUseCase_Execute_NoLastModified(t *testing.T) { +func TestShowUseCase_Execute_NoUpdatedAt(t *testing.T) { t.Parallel() client := &mockShowClient{ - getParameterResult: ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/config"), - Value: lo.ToPtr("value"), - Version: 1, - Type: paramapi.ParameterTypeString, + getParameterResult: &model.Parameter{ + Name: "/app/config", + Value: "value", + Version: "1", + Metadata: model.AWSParameterMeta{ + Type: "String", }, }, } @@ -188,26 +198,24 @@ func TestShowUseCase_Execute_NoLastModified(t *testing.T) { Spec: spec, }) require.NoError(t, err) - assert.Nil(t, output.LastModified) + assert.Nil(t, output.UpdatedAt) } func TestShowUseCase_Execute_WithTags(t *testing.T) { t.Parallel() client := &mockShowClient{ - getParameterResult: ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/app/config"), - Value: lo.ToPtr("value"), - Version: 1, - Type: paramapi.ParameterTypeString, + getParameterResult: &model.Parameter{ + Name: "/app/config", + Value: "value", + Version: "1", + Metadata: model.AWSParameterMeta{ + Type: "String", }, }, - listTagsResult: ¶mapi.ListTagsForResourceOutput{ - TagList: []paramapi.Tag{ - {Key: lo.ToPtr("env"), Value: lo.ToPtr("prod")}, - {Key: lo.ToPtr("team"), Value: lo.ToPtr("backend")}, - }, + getTagsResult: map[string]string{ + "env": "prod", + "team": "backend", }, } @@ -221,8 +229,13 @@ func TestShowUseCase_Execute_WithTags(t *testing.T) { }) require.NoError(t, err) assert.Len(t, output.Tags, 2) - assert.Equal(t, "env", output.Tags[0].Key) - assert.Equal(t, "prod", output.Tags[0].Value) - assert.Equal(t, "team", output.Tags[1].Key) - assert.Equal(t, "backend", output.Tags[1].Value) + + // Tags are now from a map, so order is not guaranteed + tagMap := make(map[string]string) + for _, tag := range output.Tags { + tagMap[tag.Key] = tag.Value + } + + assert.Equal(t, "prod", tagMap["env"]) + assert.Equal(t, "backend", tagMap["team"]) } diff --git a/internal/version/paramversion/version.go b/internal/version/paramversion/version.go index 1d0940de..56493efc 100644 --- a/internal/version/paramversion/version.go +++ b/internal/version/paramversion/version.go @@ -5,21 +5,15 @@ import ( "context" "fmt" "slices" + "strconv" - "github.com/samber/lo" - - "github.com/mpyw/suve/internal/api/paramapi" + "github.com/mpyw/suve/internal/model" + "github.com/mpyw/suve/internal/provider" ) -// Client is the interface for GetParameterWithVersion. -type Client interface { - paramapi.GetParameterAPI - paramapi.GetParameterHistoryAPI -} - // GetParameterWithVersion retrieves a parameter with version/shift support. // SecureString values are always decrypted. -func GetParameterWithVersion(ctx context.Context, client Client, spec *Spec) (*paramapi.ParameterHistory, error) { +func GetParameterWithVersion(ctx context.Context, client provider.ParameterReader, spec *Spec) (*model.Parameter, error) { if spec.HasShift() { return getParameterWithShift(ctx, client, spec) } @@ -27,11 +21,8 @@ func GetParameterWithVersion(ctx context.Context, client Client, spec *Spec) (*p return getParameterDirect(ctx, client, spec) } -func getParameterWithShift(ctx context.Context, client paramapi.GetParameterHistoryAPI, spec *Spec) (*paramapi.ParameterHistory, error) { - history, err := client.GetParameterHistory(ctx, ¶mapi.GetParameterHistoryInput{ - Name: lo.ToPtr(spec.Name), - WithDecryption: lo.ToPtr(true), - }) +func getParameterWithShift(ctx context.Context, client provider.ParameterReader, spec *Spec) (*model.Parameter, error) { + history, err := client.GetParameterHistory(ctx, spec.Name) if err != nil { return nil, fmt.Errorf("failed to get parameter history: %w", err) } @@ -40,18 +31,26 @@ func getParameterWithShift(ctx context.Context, client paramapi.GetParameterHist return nil, fmt.Errorf("parameter not found: %s", spec.Name) } - // Reverse to get newest first - params := history.Parameters + // Copy and reverse to get newest first + params := make([]*model.Parameter, len(history.Parameters)) + copy(params, history.Parameters) slices.Reverse(params) baseIdx := 0 if spec.Absolute.Version != nil { - var found bool + targetVersion := strconv.FormatInt(*spec.Absolute.Version, 10) + found := false + + for i, p := range params { + if p.Version == targetVersion { + baseIdx = i + found = true + + break + } + } - _, baseIdx, found = lo.FindIndexOf(params, func(p paramapi.ParameterHistory) bool { - return p.Version == *spec.Absolute.Version - }) if !found { return nil, fmt.Errorf("version %d not found", *spec.Absolute.Version) } @@ -62,32 +61,19 @@ func getParameterWithShift(ctx context.Context, client paramapi.GetParameterHist return nil, fmt.Errorf("version shift out of range: ~%d", spec.Shift) } - return ¶ms[targetIdx], nil + return params[targetIdx], nil } -func getParameterDirect(ctx context.Context, client paramapi.GetParameterAPI, spec *Spec) (*paramapi.ParameterHistory, error) { - var nameWithVersion string +func getParameterDirect(ctx context.Context, client provider.ParameterReader, spec *Spec) (*model.Parameter, error) { + version := "" if spec.Absolute.Version != nil { - nameWithVersion = fmt.Sprintf("%s:%d", spec.Name, *spec.Absolute.Version) - } else { - nameWithVersion = spec.Name + version = strconv.FormatInt(*spec.Absolute.Version, 10) } - result, err := client.GetParameter(ctx, ¶mapi.GetParameterInput{ - Name: lo.ToPtr(nameWithVersion), - WithDecryption: lo.ToPtr(true), - }) + param, err := client.GetParameter(ctx, spec.Name, version) if err != nil { return nil, fmt.Errorf("failed to get parameter: %w", err) } - param := result.Parameter - - return ¶mapi.ParameterHistory{ - Name: param.Name, - Value: param.Value, - Type: param.Type, - Version: param.Version, - LastModifiedDate: param.LastModifiedDate, - }, nil + return param, nil } diff --git a/internal/version/paramversion/version_test.go b/internal/version/paramversion/version_test.go index 68f28a88..e3793150 100644 --- a/internal/version/paramversion/version_test.go +++ b/internal/version/paramversion/version_test.go @@ -6,54 +6,53 @@ import ( "testing" "time" - "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/mpyw/suve/internal/api/paramapi" + "github.com/mpyw/suve/internal/model" "github.com/mpyw/suve/internal/version/paramversion" ) -//nolint:lll // mock struct fields match AWS SDK interface signatures type mockClient struct { - getParameterFunc func(ctx context.Context, params *paramapi.GetParameterInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) - getParameterHistoryFunc func(ctx context.Context, params *paramapi.GetParameterHistoryInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) + getParameterFunc func(ctx context.Context, name string, version string) (*model.Parameter, error) + getParameterHistoryFunc func(ctx context.Context, name string) (*model.ParameterHistory, error) } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockClient) GetParameter(ctx context.Context, params *paramapi.GetParameterInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { +func (m *mockClient) GetParameter(ctx context.Context, name string, version string) (*model.Parameter, error) { if m.getParameterFunc != nil { - return m.getParameterFunc(ctx, params, optFns...) + return m.getParameterFunc(ctx, name, version) } return nil, fmt.Errorf("GetParameter not mocked") } -//nolint:lll // mock function signature must match AWS SDK interface -func (m *mockClient) GetParameterHistory(ctx context.Context, params *paramapi.GetParameterHistoryInput, optFns ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { +func (m *mockClient) GetParameterHistory(ctx context.Context, name string) (*model.ParameterHistory, error) { if m.getParameterHistoryFunc != nil { - return m.getParameterHistoryFunc(ctx, params, optFns...) + return m.getParameterHistoryFunc(ctx, name) } return nil, fmt.Errorf("GetParameterHistory not mocked") } +func (m *mockClient) ListParameters(_ context.Context, _ string, _ bool) ([]*model.ParameterListItem, error) { + return nil, fmt.Errorf("ListParameters not mocked") +} + func TestGetParameterWithVersion_Latest(t *testing.T) { t.Parallel() now := time.Now() mock := &mockClient{ - getParameterFunc: func(_ context.Context, params *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - assert.Equal(t, "/my/param", lo.FromPtr(params.Name)) - - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("test-value"), - Version: 3, - Type: paramapi.ParameterTypeString, - LastModifiedDate: &now, - }, + getParameterFunc: func(_ context.Context, name string, version string) (*model.Parameter, error) { + assert.Equal(t, "/my/param", name) + assert.Empty(t, version) + + return &model.Parameter{ + Name: "/my/param", + Value: "test-value", + Version: "3", + UpdatedAt: &now, + Metadata: model.AWSParameterMeta{Type: "String"}, }, nil }, } @@ -62,25 +61,24 @@ func TestGetParameterWithVersion_Latest(t *testing.T) { result, err := paramversion.GetParameterWithVersion(t.Context(), mock, spec) require.NoError(t, err) - assert.Equal(t, "/my/param", lo.FromPtr(result.Name)) - assert.Equal(t, "test-value", lo.FromPtr(result.Value)) - assert.Equal(t, int64(3), result.Version) + assert.Equal(t, "/my/param", result.Name) + assert.Equal(t, "test-value", result.Value) + assert.Equal(t, "3", result.Version) } func TestGetParameterWithVersion_SpecificVersion(t *testing.T) { t.Parallel() mock := &mockClient{ - getParameterFunc: func(_ context.Context, params *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - assert.Equal(t, "/my/param:2", lo.FromPtr(params.Name)) - - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("old-value"), - Version: 2, - Type: paramapi.ParameterTypeString, - }, + getParameterFunc: func(_ context.Context, name string, version string) (*model.Parameter, error) { + assert.Equal(t, "/my/param", name) + assert.Equal(t, "2", version) + + return &model.Parameter{ + Name: "/my/param", + Value: "old-value", + Version: "2", + Metadata: model.AWSParameterMeta{Type: "String"}, }, nil }, } @@ -90,8 +88,8 @@ func TestGetParameterWithVersion_SpecificVersion(t *testing.T) { result, err := paramversion.GetParameterWithVersion(t.Context(), mock, spec) require.NoError(t, err) - assert.Equal(t, "old-value", lo.FromPtr(result.Value)) - assert.Equal(t, int64(2), result.Version) + assert.Equal(t, "old-value", result.Value) + assert.Equal(t, "2", result.Version) } func TestGetParameterWithVersion_Shift(t *testing.T) { @@ -99,15 +97,15 @@ func TestGetParameterWithVersion_Shift(t *testing.T) { now := time.Now() mock := &mockClient{ - //nolint:lll // inline mock function - getParameterHistoryFunc: func(_ context.Context, params *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - assert.Equal(t, "/my/param", lo.FromPtr(params.Name)) + getParameterHistoryFunc: func(_ context.Context, name string) (*model.ParameterHistory, error) { + assert.Equal(t, "/my/param", name) // History is returned oldest first by AWS - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-2 * time.Hour))}, - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v3"), Version: 3, LastModifiedDate: &now}, + return &model.ParameterHistory{ + Name: "/my/param", + Parameters: []*model.Parameter{ + {Name: "/my/param", Value: "v1", Version: "1", UpdatedAt: timePtr(now.Add(-2 * time.Hour))}, + {Name: "/my/param", Value: "v2", Version: "2", UpdatedAt: timePtr(now.Add(-time.Hour))}, + {Name: "/my/param", Value: "v3", Version: "3", UpdatedAt: &now}, }, }, nil }, @@ -118,8 +116,8 @@ func TestGetParameterWithVersion_Shift(t *testing.T) { require.NoError(t, err) // Shift 1 means one version back from latest (v3), so v2 - assert.Equal(t, "v2", lo.FromPtr(result.Value)) - assert.Equal(t, int64(2), result.Version) + assert.Equal(t, "v2", result.Value) + assert.Equal(t, "2", result.Version) } func TestGetParameterWithVersion_ShiftFromSpecificVersion(t *testing.T) { @@ -127,13 +125,13 @@ func TestGetParameterWithVersion_ShiftFromSpecificVersion(t *testing.T) { now := time.Now() mock := &mockClient{ - //nolint:lll // inline mock function - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: lo.ToPtr(now.Add(-2 * time.Hour))}, - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v2"), Version: 2, LastModifiedDate: lo.ToPtr(now.Add(-time.Hour))}, - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v3"), Version: 3, LastModifiedDate: &now}, + getParameterHistoryFunc: func(_ context.Context, _ string) (*model.ParameterHistory, error) { + return &model.ParameterHistory{ + Name: "/my/param", + Parameters: []*model.Parameter{ + {Name: "/my/param", Value: "v1", Version: "1", UpdatedAt: timePtr(now.Add(-2 * time.Hour))}, + {Name: "/my/param", Value: "v2", Version: "2", UpdatedAt: timePtr(now.Add(-time.Hour))}, + {Name: "/my/param", Value: "v3", Version: "3", UpdatedAt: &now}, }, }, nil }, @@ -145,7 +143,7 @@ func TestGetParameterWithVersion_ShiftFromSpecificVersion(t *testing.T) { require.NoError(t, err) // Version 3, shift 2 means v3 -> v2 -> v1 - assert.Equal(t, "v1", lo.FromPtr(result.Value)) + assert.Equal(t, "v1", result.Value) } func TestGetParameterWithVersion_ShiftOutOfRange(t *testing.T) { @@ -153,11 +151,11 @@ func TestGetParameterWithVersion_ShiftOutOfRange(t *testing.T) { now := time.Now() mock := &mockClient{ - //nolint:lll // mock function signature - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: &now}, + getParameterHistoryFunc: func(_ context.Context, _ string) (*model.ParameterHistory, error) { + return &model.ParameterHistory{ + Name: "/my/param", + Parameters: []*model.Parameter{ + {Name: "/my/param", Value: "v1", Version: "1", UpdatedAt: &now}, }, }, nil }, @@ -175,11 +173,11 @@ func TestGetParameterWithVersion_VersionNotFound(t *testing.T) { now := time.Now() mock := &mockClient{ - //nolint:lll // mock function signature - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{ - {Name: lo.ToPtr("/my/param"), Value: lo.ToPtr("v1"), Version: 1, LastModifiedDate: &now}, + getParameterHistoryFunc: func(_ context.Context, _ string) (*model.ParameterHistory, error) { + return &model.ParameterHistory{ + Name: "/my/param", + Parameters: []*model.Parameter{ + {Name: "/my/param", Value: "v1", Version: "1", UpdatedAt: &now}, }, }, nil }, @@ -197,10 +195,10 @@ func TestGetParameterWithVersion_EmptyHistory(t *testing.T) { t.Parallel() mock := &mockClient{ - //nolint:lll // mock function signature - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { - return ¶mapi.GetParameterHistoryOutput{ - Parameters: []paramapi.ParameterHistory{}, + getParameterHistoryFunc: func(_ context.Context, _ string) (*model.ParameterHistory, error) { + return &model.ParameterHistory{ + Name: "/my/param", + Parameters: []*model.Parameter{}, }, nil }, } @@ -216,7 +214,7 @@ func TestGetParameterWithVersion_GetParameterError(t *testing.T) { t.Parallel() mock := &mockClient{ - getParameterFunc: func(_ context.Context, _ *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { + getParameterFunc: func(_ context.Context, _ string, _ string) (*model.Parameter, error) { return nil, fmt.Errorf("AWS error") }, } @@ -232,8 +230,7 @@ func TestGetParameterWithVersion_GetParameterHistoryError(t *testing.T) { t.Parallel() mock := &mockClient{ - //nolint:lll // mock function signature - getParameterHistoryFunc: func(_ context.Context, _ *paramapi.GetParameterHistoryInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterHistoryOutput, error) { + getParameterHistoryFunc: func(_ context.Context, _ string) (*model.ParameterHistory, error) { return nil, fmt.Errorf("AWS error") }, } @@ -245,28 +242,6 @@ func TestGetParameterWithVersion_GetParameterHistoryError(t *testing.T) { assert.Equal(t, "failed to get parameter history: AWS error", err.Error()) } -func TestGetParameterWithVersion_AlwaysDecrypts(t *testing.T) { - t.Parallel() - - mock := &mockClient{ - getParameterFunc: func(_ context.Context, params *paramapi.GetParameterInput, _ ...func(*paramapi.Options)) (*paramapi.GetParameterOutput, error) { - // Verify that WithDecryption is always true - assert.True(t, lo.FromPtr(params.WithDecryption)) - - return ¶mapi.GetParameterOutput{ - Parameter: ¶mapi.Parameter{ - Name: lo.ToPtr("/my/param"), - Value: lo.ToPtr("decrypted-value"), - Version: 1, - Type: paramapi.ParameterTypeSecureString, - }, - }, nil - }, - } - - spec := ¶mversion.Spec{Name: "/my/param"} - result, err := paramversion.GetParameterWithVersion(t.Context(), mock, spec) - - require.NoError(t, err) - assert.Equal(t, "decrypted-value", lo.FromPtr(result.Value)) +func timePtr(t time.Time) *time.Time { + return &t }