From 48b508edef5b0d72430090a72bcff15684f9f95d Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 18 Mar 2026 14:32:53 +0100 Subject: [PATCH 01/34] Improvements on test framework. --- cmd/common_test.go | 227 ++++++++++++++++++++------------- cmd/ip_test.go | 303 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 12 +- go.sum | 26 +++- 4 files changed, 477 insertions(+), 91 deletions(-) create mode 100644 cmd/ip_test.go diff --git a/cmd/common_test.go b/cmd/common_test.go index 5befb1a..fd6a536 100644 --- a/cmd/common_test.go +++ b/cmd/common_test.go @@ -2,58 +2,61 @@ package cmd import ( "bytes" - "encoding/json" + "context" "fmt" "io" + "log/slog" "os" "strings" "testing" "slices" + "buf.build/go/protoyaml" + "connectrpc.com/connect" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - apitests "github.com/metal-stack/api/go/tests" + client "github.com/metal-stack/api/go/client" "github.com/metal-stack/cli/cmd/completion" "github.com/metal-stack/cli/cmd/config" - "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/spf13/afero" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/runtime/protoimpl" - "sigs.k8s.io/yaml" ) -type Test[R any] struct { +type Test[Request, Response any] struct { Name string - Cmd func(want R) []string + Cmd func() []string - ClientMocks *apitests.ClientMockFns - FsMocks func(fs afero.Fs, want R) - MockStdin *bytes.Buffer + Client client.Client + FsMocks func(fs afero.Fs) + MockStdin *bytes.Buffer - DisableMockClient bool // can switch off mock client creation + // DisableMockClient bool // can switch off mock client creation WantErr error - Want R // for json and yaml - WantTable *string // for table printer - WantWideTable *string // for wide table printer - Template *string // for template printer - WantTemplate *string // for template printer - WantMarkdown *string // for markdown printer + WantRequest Request + WantResponse Response // for client return and json and yaml + WantTable *string // for table printer + WantWideTable *string // for wide table printer + Template *string // for template printer + WantTemplate *string // for template printer + WantMarkdown *string // for markdown printer } -func (c *Test[R]) TestCmd(t *testing.T) { +func (c *Test[Request, Response]) TestCmd(t *testing.T) { require.NotEmpty(t, c.Name, "test name must not be empty") require.NotEmpty(t, c.Cmd, "cmd must not be empty") if c.WantErr != nil { - _, _, conf := c.newMockConfig(t) + _, _, conf := c.newCmdConfig(t) cmd := newRootCmd(conf) - os.Args = append([]string{config.BinaryName}, c.Cmd(c.Want)...) + os.Args = append([]string{config.BinaryName}, c.Cmd()...) err := cmd.Execute() if diff := cmp.Diff(c.WantErr, err, testcommon.IgnoreUnexported(), testcommon.ErrorStringComparer()); diff != "" { @@ -63,10 +66,10 @@ func (c *Test[R]) TestCmd(t *testing.T) { for _, format := range outputFormats(c) { t.Run(fmt.Sprintf("%v", format.Args()), func(t *testing.T) { - _, out, conf := c.newMockConfig(t) + _, out, conf := c.newCmdConfig(t) cmd := newRootCmd(conf) - os.Args = append([]string{config.BinaryName}, c.Cmd(c.Want)...) + os.Args = append([]string{config.BinaryName}, c.Cmd()...) os.Args = append(os.Args, format.Args()...) err := cmd.Execute() @@ -77,13 +80,28 @@ func (c *Test[R]) TestCmd(t *testing.T) { } } -func (c *Test[R]) newMockConfig(t *testing.T) (any, *bytes.Buffer, *config.Config) { - mock := apitests.New(t) +func (c *Test[Request, Response]) newCmdConfig(t *testing.T) (any, *bytes.Buffer, *config.Config) { + interceptors := []connect.Interceptor{ + &testClientInterceptor[Request, Response]{ + t: t, + response: c.WantResponse, + request: c.WantRequest, + }, + // validate.NewInterceptor(), + } + + cl, err := client.New(&client.DialConfig{ + BaseURL: "http://this-is-just-for-testing", + Interceptors: interceptors, + UserAgent: "cli-test", + Log: slog.Default(), + }) + require.NoError(t, err) fs := afero.NewMemMapFs() - if c.FsMocks != nil { - c.FsMocks(fs, c.Want) - } + // if c.FsMocks != nil { + // c.FsMocks(fs, c.Want) + // } var in io.Reader if c.MockStdin != nil { @@ -98,14 +116,10 @@ func (c *Test[R]) newMockConfig(t *testing.T) (any, *bytes.Buffer, *config.Confi In: in, PromptOut: io.Discard, Completion: &completion.Completion{}, - Client: mock.Client(c.ClientMocks), + Client: cl, } ) - if c.DisableMockClient { - config.Client = nil - } - return nil, &out, config } @@ -131,50 +145,50 @@ func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { }) } -func MustMarshal(t *testing.T, d any) []byte { - b, err := json.MarshalIndent(d, "", " ") - require.NoError(t, err) - return b -} - -func MustMarshalToMultiYAML[R any](t *testing.T, data []R) []byte { - var parts []string - for _, elem := range data { - parts = append(parts, string(MustMarshal(t, elem))) - } - return []byte(strings.Join(parts, "\n---\n")) -} - -func MustJsonDeepCopy[O any](t *testing.T, object O) O { - raw, err := json.Marshal(&object) - require.NoError(t, err) - var copy O - err = json.Unmarshal(raw, ©) - require.NoError(t, err) - return copy -} - -func outputFormats[R any](c *Test[R]) []outputFormat[R] { - var formats []outputFormat[R] - - if !pointer.IsZero(c.Want) { - formats = append(formats, &jsonOutputFormat[R]{want: c.Want}, &yamlOutputFormat[R]{want: c.Want}) - } +// func MustMarshal(t *testing.T, d any) []byte { +// b, err := protoyaml.Marshal() +// require.NoError(t, err) +// return b +// } + +// func MustMarshalToMultiYAML[R any](t *testing.T, data []R) []byte { +// var parts []string +// for _, elem := range data { +// parts = append(parts, string(MustMarshal(t, elem))) +// } +// return []byte(strings.Join(parts, "\n---\n")) +// } + +// func MustJsonDeepCopy[O any](t *testing.T, object O) O { +// raw, err := protoyaml.Marshal(&object) +// require.NoError(t, err) +// var copy O +// err = json.Unmarshal(raw, ©) +// require.NoError(t, err) +// return copy +// } + +func outputFormats[Request, Response any](c *Test[Request, Response]) []outputFormat[Response] { + var formats []outputFormat[Response] + + // if !pointer.IsZero(c.Want) { + // formats = append(formats, &jsonOutputFormat[Request, Response]{want: c.Want}, &yamlOutputFormat[Request, Response]{want: c.Want}) + // } if c.WantTable != nil { - formats = append(formats, &tableOutputFormat[R]{table: *c.WantTable}) + formats = append(formats, &tableOutputFormat[Response]{table: *c.WantTable}) } if c.WantWideTable != nil { - formats = append(formats, &wideTableOutputFormat[R]{table: *c.WantWideTable}) + formats = append(formats, &wideTableOutputFormat[Response]{table: *c.WantWideTable}) } if c.Template != nil && c.WantTemplate != nil { - formats = append(formats, &templateOutputFormat[R]{template: *c.Template, templateOutput: *c.WantTemplate}) + formats = append(formats, &templateOutputFormat[Response]{template: *c.Template, templateOutput: *c.WantTemplate}) } if c.WantMarkdown != nil { - formats = append(formats, &markdownOutputFormat[R]{table: *c.WantMarkdown}) + formats = append(formats, &markdownOutputFormat[Response]{table: *c.WantMarkdown}) } return formats @@ -185,37 +199,56 @@ type outputFormat[R any] interface { Validate(t *testing.T, output []byte) } -type jsonOutputFormat[R any] struct { - want R -} +// type jsonOutputFormat[R any] struct { +// want R +// } -func (o *jsonOutputFormat[R]) Args() []string { - return []string{"-o", "jsonraw"} -} +// func (o *jsonOutputFormat[R]) Args() []string { +// return []string{"-o", "jsonraw"} +// } -func (o *jsonOutputFormat[R]) Validate(t *testing.T, output []byte) { - var got R +// func (o *jsonOutputFormat[R]) Validate(t *testing.T, output []byte) { +// var got R - err := json.Unmarshal(output, &got) - require.NoError(t, err, string(output)) +// err := json.Unmarshal(output, &got) +// require.NoError(t, err, string(output)) - if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { - t.Errorf("diff (+got -want):\n %s", diff) - } -} +// if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { +// t.Errorf("diff (+got -want):\n %s", diff) +// } +// } + +// type yamlOutputFormat[R any] struct { +// want R +// } -type yamlOutputFormat[R any] struct { +// func (o *yamlOutputFormat[R]) Args() []string { +// return []string{"-o", "yamlraw"} +// } + +// func (o *yamlOutputFormat[R]) Validate(t *testing.T, output []byte) { +// var got R + +// err := protoyaml.Unmarshal(output, &got) +// require.NoError(t, err) + +// if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { +// t.Errorf("diff (+got -want):\n %s", diff) +// } +// } + +type yamlOutputFormat[R proto.Message] struct { want R } func (o *yamlOutputFormat[R]) Args() []string { - return []string{"-o", "yamlraw"} + return []string{"-o", "yaml"} } func (o *yamlOutputFormat[R]) Validate(t *testing.T, output []byte) { var got R - err := yaml.Unmarshal(output, &got) + err := protoyaml.Unmarshal(output, got) require.NoError(t, err) if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { @@ -259,9 +292,10 @@ func (o *templateOutputFormat[R]) Args() []string { func (o *templateOutputFormat[R]) Validate(t *testing.T, output []byte) { t.Logf("got following template output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", string(output)) - if diff := cmp.Diff(strings.TrimSpace(o.templateOutput), strings.TrimSpace(string(output))); diff != "" { - t.Errorf("diff (+got -want):\n %s", diff) - } + assert.Equal(t, strings.TrimSpace(o.templateOutput), strings.TrimSpace(string(output))) + // if diff := cmp.Diff(strings.TrimSpace(o.templateOutput), strings.TrimSpace(string(output))); diff != "" { + // t.Errorf("diff (+got -want):\n %s", diff) + // } } type markdownOutputFormat[R any] struct { @@ -295,7 +329,7 @@ func validateTableRows(t *testing.T, want, got string) { t.Logf("got following table output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", trimmedGot) - t.Log(cmp.Diff(trimmedWant, trimmedGot)) + // t.Log(cmp.Diff(trimmedWant, trimmedGot)) require.Equal(t, len(wantRows), len(gotRows), "tables have different lengths") @@ -310,3 +344,26 @@ func validateTableRows(t *testing.T, want, got string) { } } } + +type testClientInterceptor[Request, Response any] struct { + t *testing.T + request Request + response Response +} + +func (t *testClientInterceptor[Request, Response]) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, ar connect.AnyRequest) (connect.AnyResponse, error) { + assert.Equal(t.t, &t.request, ar.Any()) + return connect.NewResponse(&t.response), nil + } +} + +func (t *testClientInterceptor[Request, Response]) WrapStreamingClient(connect.StreamingClientFunc) connect.StreamingClientFunc { + t.t.Errorf("streaming not supported") + return nil +} + +func (t *testClientInterceptor[Request, Response]) WrapStreamingHandler(connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + t.t.Errorf("streaming not supported") + return nil +} diff --git a/cmd/ip_test.go b/cmd/ip_test.go new file mode 100644 index 0000000..91ba069 --- /dev/null +++ b/cmd/ip_test.go @@ -0,0 +1,303 @@ +package cmd + +import ( + "testing" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/tag" +) + +var ( + ip1 = func() *apiv2.IP { + return &apiv2.IP{ + Uuid: "2e0144a2-09ef-42b7-b629-4263295db6e8", + Ip: "1.1.1.1", + Name: "a", + Description: "a description", + Project: "ce19a655-7933-4745-8f3e-9592b4a90488", + Type: apiv2.IPType_IP_TYPE_STATIC, + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: map[string]string{ + tag.ClusterServiceFQN: "/default/ingress-nginx", + }, + }, + }, + } + } + ip2 = func() *apiv2.IP { + return &apiv2.IP{ + Uuid: "9cef40ec-29c6-4dfa-aee8-47ee1f49223d", + Ip: "4.3.2.1", + Name: "b", + Description: "b description", + Project: "46bdfc45-9c8d-4268-b359-b40e3079d384", + Type: apiv2.IPType_IP_TYPE_EPHEMERAL, + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "a": "b", + }, + }, + }, + } + } +) + +// func Test_IPCmd_MultiResult(t *testing.T) { +// tests := []*Test[[]*apiv2.IP]{ +// { +// Name: "list", +// Cmd: func(want []*apiv2.IP) []string { +// return []string{"ip", "list", "--project", "a"} +// }, +// // ClientMocks: &apitests.ClientMockFns{ +// // Apiv1Mocks: &apitests.Apiv1MockFns{ +// // IP: func(m *mock.Mock) { +// // m.On("List", mock.Anything, connect.NewRequest(&apiv2.IPServiceListRequest{ +// // Project: "a", +// // })).Return(&connect.Response[apiv2.IPServiceListResponse]{ +// // Msg: &apiv2.IPServiceListResponse{ +// // Ips: []*apiv2.IP{ +// // ip2(), +// // ip1(), +// // }, +// // }, +// // }, nil) +// // }, +// // }, +// // }, +// Want: []*apiv2.IP{ +// ip1(), +// ip2(), +// }, +// WantTable: new(` +// IP PROJECT ID TYPE NAME ATTACHED SERVICE +// 1.1.1.1 a 2e0144a2-09ef-42b7-b629-4263295db6e8 static a ingress-nginx +// 4.3.2.1 b 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b +// `), +// WantWideTable: new(` +// IP PROJECT ID TYPE NAME DESCRIPTION LABELS +// 1.1.1.1 a 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx +// 4.3.2.1 b 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b b description a=b +// `), +// Template: new("{{ .ip }} {{ .project }}"), +// WantTemplate: new(` +// 1.1.1.1 a +// 4.3.2.1 b +// `), +// WantMarkdown: new(` +// | IP | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | +// |---------|---------|--------------------------------------|-----------|------|------------------| +// | 1.1.1.1 | a | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | ingress-nginx | +// | 4.3.2.1 | b | 9cef40ec-29c6-4dfa-aee8-47ee1f49223d | ephemeral | b | | +// `), +// }, +// // { +// // Name: "apply", +// // Cmd: func(want []*apiv2.IP) []string { +// // return appendFromFileCommonArgs("ip", "apply") +// // }, +// // FsMocks: func(fs afero.Fs, want []*apiv2.IP) { +// // require.NoError(t, afero.WriteFile(fs, "/file.yaml", MustMarshalToMultiYAML(t, want), 0755)) +// // }, +// // // ClientMocks: &apitests.ClientMockFns{ +// // // Apiv1Mocks: &apitests.Apiv1MockFns{ +// // // IP: func(m *mock.Mock) { +// // // m.On("Allocate", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(v1.IpResponseToCreate(ip1())), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceAllocateResponse{ +// // // Ip: ip1(), +// // // }), nil) +// // // m.On("Allocate", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(v1.IpResponseToCreate(ip2())), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceAllocateResponse{ +// // // Ip: ip2(), +// // // }), nil) +// // // // FIXME: API does not return a conflict when already exists, so the update functionality does not work! +// // // }, +// // // }, +// // // }, +// // Want: []*apiv2.IP{ +// // ip1(), +// // ip2(), +// // }, +// // }, +// // { +// // Name: "update from file", +// // Cmd: func(want []*apiv2.IP) []string { +// // return appendFromFileCommonArgs("ip", "update") +// // }, +// // FsMocks: func(fs afero.Fs, want []*apiv2.IP) { +// // require.NoError(t, afero.WriteFile(fs, "/file.yaml", MustMarshalToMultiYAML(t, want), 0755)) +// // }, +// // // ClientMocks: &apitests.ClientMockFns{ +// // // Apiv1Mocks: &apitests.Apiv1MockFns{ +// // // IP: func(m *mock.Mock) { +// // // m.On("Update", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(&apiv2.IPServiceUpdateRequest{ +// // // Project: ip1().Project, +// // // Ip: ip1(), +// // // }), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceUpdateResponse{ +// // // Ip: ip1(), +// // // }), nil) +// // // }, +// // // }, +// // // }, +// // Want: []*apiv2.IP{ +// // ip1(), +// // }, +// // }, +// // { +// // Name: "create from file", +// // Cmd: func(want []*apiv2.IP) []string { +// // return appendFromFileCommonArgs("ip", "create") +// // }, +// // FsMocks: func(fs afero.Fs, want []*apiv2.IP) { +// // require.NoError(t, afero.WriteFile(fs, "/file.yaml", MustMarshalToMultiYAML(t, want), 0755)) +// // }, +// // ClientMocks: &apitests.ClientMockFns{ +// // Apiv1Mocks: &apitests.Apiv1MockFns{ +// // IP: func(m *mock.Mock) { +// // m.On("Allocate", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(v1.IpResponseToCreate(ip1())), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceAllocateResponse{ +// // Ip: ip1(), +// // }), nil) +// // }, +// // }, +// // }, +// // Want: []*apiv2.IP{ +// // ip1(), +// // }, +// // }, +// // { +// // Name: "delete from file", +// // Cmd: func(want []*apiv2.IP) []string { +// // return appendFromFileCommonArgs("ip", "delete") +// // }, +// // FsMocks: func(fs afero.Fs, want []*apiv2.IP) { +// // require.NoError(t, afero.WriteFile(fs, "/file.yaml", MustMarshalToMultiYAML(t, want), 0755)) +// // }, +// // ClientMocks: &apitests.ClientMockFns{ +// // Apiv1Mocks: &apitests.Apiv1MockFns{ +// // IP: func(m *mock.Mock) { +// // m.On("Delete", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(&apiv2.IPServiceDeleteRequest{ +// // Uuid: ip1().Uuid, +// // Project: ip1().Project, +// // }), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceDeleteResponse{ +// // Ip: ip1(), +// // }), nil) +// // }, +// // }, +// // }, +// // Want: []*apiv2.IP{ +// // ip1(), +// // }, +// // }, +// } +// for _, tt := range tests { +// tt.TestCmd(t) +// } +// } + +func Test_IPCmd_SingleResult(t *testing.T) { + ip1 := ip1() + + tests := []*Test[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse]{ + { + Name: "describe", + Cmd: func() []string { + return []string{"ip", "describe", "--project", ip1.Project, ip1.Ip} + }, + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1.Ip, + Project: ip1.Project, + }, + WantResponse: apiv2.IPServiceGetResponse{ + Ip: ip1, + }, + WantTable: new(` +IP PROJECT ID TYPE NAME ATTACHED SERVICE +1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a +`), + WantWideTable: new(` +IP PROJECT ID TYPE NAME DESCRIPTION LABELS +1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx +`), + Template: new("{{ .ip }} {{ .project }}"), + WantTemplate: new(` +1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 + `), + WantMarkdown: new(` +| IP | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | +|---------|--------------------------------------|--------------------------------------|--------|------|------------------| +| 1.1.1.1 | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | +`), + }, + // { + // Name: "delete", + // Cmd: func(want *apiv2.IP) []string { + // return []string{"ip", "rm", "--project", want.Project, want.Uuid} + // }, + // ClientMocks: &apitests.ClientMockFns{ + // Apiv1Mocks: &apitests.Apiv1MockFns{ + // IP: func(m *mock.Mock) { + // m.On("Delete", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(&apiv2.IPServiceDeleteRequest{ + // Project: ip1().Project, + // Uuid: ip1().Uuid, + // }), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceDeleteResponse{ + // Ip: ip1(), + // }), nil) + // }, + // }, + // }, + // Want: ip1(), + // }, + // { + // Name: "create", + // Cmd: func(want *apiv2.IP) []string { + // args := []string{"ip", "create", "--project", want.Project, "--description", want.Description, "--name", want.Name, "--tags", strings.Join(want.Tags, ",")} + // if want.Type == apiv2.IPType_IP_TYPE_STATIC { + // args = append(args, "--static") + // } + // AssertExhaustiveArgs(t, args, commonExcludedFileArgs()...) + // return args + // }, + // ClientMocks: &apitests.ClientMockFns{ + // Apiv1Mocks: &apitests.Apiv1MockFns{ + // IP: func(m *mock.Mock) { + // m.On("Allocate", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(v1.IpResponseToCreate(ip1())), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceAllocateResponse{ + // Ip: ip1(), + // }), nil) + // }, + // }, + // }, + // Want: ip1(), + // }, + // { + // Name: "update", + // Cmd: func(want *apiv2.IP) []string { + // args := []string{"ip", "update", want.Uuid, "--project", want.Project, "--description", want.Description, "--name", want.Name, "--tags", strings.Join(want.Tags, ",")} + // if want.Type == apiv2.IPType_IP_TYPE_STATIC { + // args = append(args, "--static") + // } + // AssertExhaustiveArgs(t, args, commonExcludedFileArgs()...) + // return args + // }, + // ClientMocks: &apitests.ClientMockFns{ + // Apiv1Mocks: &apitests.Apiv1MockFns{ + // IP: func(m *mock.Mock) { + // m.On("Get", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(&apiv2.IPServiceGetRequest{ + // Uuid: ip1().Uuid, + // Project: ip1().Project, + // }), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceGetResponse{ + // Ip: ip1(), + // }), nil) + + // m.On("Update", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(v1.IpResponseToUpdate(ip1())), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceUpdateResponse{ + // Ip: ip1(), + // }), nil) + // }, + // }, + // }, + // Want: ip1(), + // }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/go.mod b/go.mod index 84ce82e..2e474d8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/metal-stack/cli go 1.26 require ( + buf.build/go/protoyaml v0.6.0 + connectrpc.com/connect v1.19.1 github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.18.0 github.com/google/go-cmp v0.7.0 @@ -20,7 +22,9 @@ require ( require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 // indirect - connectrpc.com/connect v1.19.1 // indirect + buf.build/go/protovalidate v1.1.3 // indirect + cel.dev/expr v0.25.1 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/displaywidth v0.6.2 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect @@ -34,6 +38,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/google/cel-go v0.27.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.4 // indirect @@ -57,9 +62,12 @@ require ( go.mongodb.org/mongo-driver v1.17.9 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/text v0.35.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apimachinery v0.35.1 // indirect diff --git a/go.sum b/go.sum index 9c15578..17f23b6 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,17 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE= +buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE= +buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w= +buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= -github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= -github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= @@ -39,6 +47,8 @@ github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo= +github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -81,6 +91,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -110,13 +122,19 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI= +google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 65a04963349a88450b3bc46c84429fdac2d3ea6f Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Wed, 18 Mar 2026 15:29:20 +0000 Subject: [PATCH 02/34] Add full output format coverage and tests for project, tenant, token, image - Enhance test framework with proto yaml/json validators using WantObject field - Clean up commented-out dead code in common_test.go and ip_test.go - Enable IP list test (Test_IPCmd_MultiResult) - Add IP describe yaml/json coverage via WantObject - Add project describe + list tests with all 6 output formats - Add tenant describe test with yaml, json, template formats - Add token describe test with yaml, json, template formats - Add image list test with table, wide, template, markdown formats Co-Authored-By: Claude Opus 4.6 --- cmd/common_test.go | 117 ++++++++------------ cmd/image_test.go | 73 +++++++++++++ cmd/ip_test.go | 261 ++++++++------------------------------------ cmd/project_test.go | 116 ++++++++++++++++++++ cmd/tenant_test.go | 50 +++++++++ cmd/token_test.go | 53 +++++++++ 6 files changed, 379 insertions(+), 291 deletions(-) create mode 100644 cmd/image_test.go create mode 100644 cmd/project_test.go create mode 100644 cmd/tenant_test.go create mode 100644 cmd/token_test.go diff --git a/cmd/common_test.go b/cmd/common_test.go index fd6a536..b1bfb06 100644 --- a/cmd/common_test.go +++ b/cmd/common_test.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/runtime/protoimpl" ) @@ -40,12 +41,13 @@ type Test[Request, Response any] struct { WantErr error WantRequest Request - WantResponse Response // for client return and json and yaml - WantTable *string // for table printer - WantWideTable *string // for wide table printer - Template *string // for template printer - WantTemplate *string // for template printer - WantMarkdown *string // for markdown printer + WantResponse Response // for client return and json and yaml + WantObject proto.Message // domain object for yaml/json structural comparison + WantTable *string // for table printer + WantWideTable *string // for wide table printer + Template *string // for template printer + WantTemplate *string // for template printer + WantMarkdown *string // for markdown printer } func (c *Test[Request, Response]) TestCmd(t *testing.T) { @@ -99,9 +101,9 @@ func (c *Test[Request, Response]) newCmdConfig(t *testing.T) (any, *bytes.Buffer require.NoError(t, err) fs := afero.NewMemMapFs() - // if c.FsMocks != nil { - // c.FsMocks(fs, c.Want) - // } + if c.FsMocks != nil { + c.FsMocks(fs) + } var in io.Reader if c.MockStdin != nil { @@ -145,35 +147,15 @@ func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { }) } -// func MustMarshal(t *testing.T, d any) []byte { -// b, err := protoyaml.Marshal() -// require.NoError(t, err) -// return b -// } - -// func MustMarshalToMultiYAML[R any](t *testing.T, data []R) []byte { -// var parts []string -// for _, elem := range data { -// parts = append(parts, string(MustMarshal(t, elem))) -// } -// return []byte(strings.Join(parts, "\n---\n")) -// } - -// func MustJsonDeepCopy[O any](t *testing.T, object O) O { -// raw, err := protoyaml.Marshal(&object) -// require.NoError(t, err) -// var copy O -// err = json.Unmarshal(raw, ©) -// require.NoError(t, err) -// return copy -// } - func outputFormats[Request, Response any](c *Test[Request, Response]) []outputFormat[Response] { var formats []outputFormat[Response] - // if !pointer.IsZero(c.Want) { - // formats = append(formats, &jsonOutputFormat[Request, Response]{want: c.Want}, &yamlOutputFormat[Request, Response]{want: c.Want}) - // } + if c.WantObject != nil { + formats = append(formats, + &protoYAMLOutputFormat[Response]{want: c.WantObject}, + &protoJSONOutputFormat[Response]{want: c.WantObject}, + ) + } if c.WantTable != nil { formats = append(formats, &tableOutputFormat[Response]{table: *c.WantTable}) @@ -199,56 +181,43 @@ type outputFormat[R any] interface { Validate(t *testing.T, output []byte) } -// type jsonOutputFormat[R any] struct { -// want R -// } - -// func (o *jsonOutputFormat[R]) Args() []string { -// return []string{"-o", "jsonraw"} -// } - -// func (o *jsonOutputFormat[R]) Validate(t *testing.T, output []byte) { -// var got R - -// err := json.Unmarshal(output, &got) -// require.NoError(t, err, string(output)) - -// if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { -// t.Errorf("diff (+got -want):\n %s", diff) -// } -// } +type protoYAMLOutputFormat[R any] struct { + want proto.Message +} -// type yamlOutputFormat[R any] struct { -// want R -// } +func (o *protoYAMLOutputFormat[R]) Args() []string { + return []string{"-o", "yaml"} +} -// func (o *yamlOutputFormat[R]) Args() []string { -// return []string{"-o", "yamlraw"} -// } +func (o *protoYAMLOutputFormat[R]) Validate(t *testing.T, output []byte) { + t.Logf("got following yaml output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", string(output)) -// func (o *yamlOutputFormat[R]) Validate(t *testing.T, output []byte) { -// var got R + got := proto.Clone(o.want) + proto.Reset(got) -// err := protoyaml.Unmarshal(output, &got) -// require.NoError(t, err) + err := protoyaml.Unmarshal(output, got) + require.NoError(t, err) -// if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { -// t.Errorf("diff (+got -want):\n %s", diff) -// } -// } + if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } +} -type yamlOutputFormat[R proto.Message] struct { - want R +type protoJSONOutputFormat[R any] struct { + want proto.Message } -func (o *yamlOutputFormat[R]) Args() []string { - return []string{"-o", "yaml"} +func (o *protoJSONOutputFormat[R]) Args() []string { + return []string{"-o", "json"} } -func (o *yamlOutputFormat[R]) Validate(t *testing.T, output []byte) { - var got R +func (o *protoJSONOutputFormat[R]) Validate(t *testing.T, output []byte) { + t.Logf("got following json output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", string(output)) - err := protoyaml.Unmarshal(output, got) + got := proto.Clone(o.want) + proto.Reset(got) + + err := protojson.Unmarshal(output, got) require.NoError(t, err) if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { diff --git a/cmd/image_test.go b/cmd/image_test.go new file mode 100644 index 0000000..e871680 --- /dev/null +++ b/cmd/image_test.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "testing" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/pointer" +) + +var ( + image1 = func() *apiv2.Image { + return &apiv2.Image{ + Id: "ubuntu-24.04", + Name: pointer.Pointer("Ubuntu 24.04"), + Description: pointer.Pointer("Ubuntu 24.04 LTS"), + Features: []apiv2.ImageFeature{apiv2.ImageFeature_IMAGE_FEATURE_MACHINE}, + Classification: apiv2.ImageClassification_IMAGE_CLASSIFICATION_SUPPORTED, + } + } + image2 = func() *apiv2.Image { + return &apiv2.Image{ + Id: "firewall-3.0", + Name: pointer.Pointer("Firewall 3.0"), + Description: pointer.Pointer("Metal Firewall"), + Features: []apiv2.ImageFeature{apiv2.ImageFeature_IMAGE_FEATURE_FIREWALL}, + Classification: apiv2.ImageClassification_IMAGE_CLASSIFICATION_PREVIEW, + } + } +) + +func Test_ImageCmd_List(t *testing.T) { + tests := []*Test[apiv2.ImageServiceListRequest, apiv2.ImageServiceListResponse]{ + { + Name: "list", + Cmd: func() []string { + return []string{"image", "list"} + }, + WantRequest: apiv2.ImageServiceListRequest{ + Query: &apiv2.ImageQuery{}, + }, + WantResponse: apiv2.ImageServiceListResponse{ + Images: []*apiv2.Image{ + image1(), + image2(), + }, + }, + WantTable: new(` +ID NAME DESCRIPTION FEATURES EXPIRATION STATUS +ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported +firewall-3.0 Firewall 3.0 Metal Firewall firewall preview +`), + WantWideTable: new(` +ID NAME DESCRIPTION FEATURES EXPIRATION STATUS +ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported +firewall-3.0 Firewall 3.0 Metal Firewall firewall preview +`), + Template: new("{{ .id }} {{ .name }}"), + WantTemplate: new(` +ubuntu-24.04 Ubuntu 24.04 +firewall-3.0 Firewall 3.0 + `), + WantMarkdown: new(` +| ID | NAME | DESCRIPTION | FEATURES | EXPIRATION | STATUS | +|--------------|--------------|------------------|----------|------------|-----------| +| ubuntu-24.04 | Ubuntu 24.04 | Ubuntu 24.04 LTS | machine | | supported | +| firewall-3.0 | Firewall 3.0 | Metal Firewall | firewall | | preview | +`), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/cmd/ip_test.go b/cmd/ip_test.go index 91ba069..e6dc1fe 100644 --- a/cmd/ip_test.go +++ b/cmd/ip_test.go @@ -44,155 +44,49 @@ var ( } ) -// func Test_IPCmd_MultiResult(t *testing.T) { -// tests := []*Test[[]*apiv2.IP]{ -// { -// Name: "list", -// Cmd: func(want []*apiv2.IP) []string { -// return []string{"ip", "list", "--project", "a"} -// }, -// // ClientMocks: &apitests.ClientMockFns{ -// // Apiv1Mocks: &apitests.Apiv1MockFns{ -// // IP: func(m *mock.Mock) { -// // m.On("List", mock.Anything, connect.NewRequest(&apiv2.IPServiceListRequest{ -// // Project: "a", -// // })).Return(&connect.Response[apiv2.IPServiceListResponse]{ -// // Msg: &apiv2.IPServiceListResponse{ -// // Ips: []*apiv2.IP{ -// // ip2(), -// // ip1(), -// // }, -// // }, -// // }, nil) -// // }, -// // }, -// // }, -// Want: []*apiv2.IP{ -// ip1(), -// ip2(), -// }, -// WantTable: new(` -// IP PROJECT ID TYPE NAME ATTACHED SERVICE -// 1.1.1.1 a 2e0144a2-09ef-42b7-b629-4263295db6e8 static a ingress-nginx -// 4.3.2.1 b 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b -// `), -// WantWideTable: new(` -// IP PROJECT ID TYPE NAME DESCRIPTION LABELS -// 1.1.1.1 a 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx -// 4.3.2.1 b 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b b description a=b -// `), -// Template: new("{{ .ip }} {{ .project }}"), -// WantTemplate: new(` -// 1.1.1.1 a -// 4.3.2.1 b -// `), -// WantMarkdown: new(` -// | IP | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | -// |---------|---------|--------------------------------------|-----------|------|------------------| -// | 1.1.1.1 | a | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | ingress-nginx | -// | 4.3.2.1 | b | 9cef40ec-29c6-4dfa-aee8-47ee1f49223d | ephemeral | b | | -// `), -// }, -// // { -// // Name: "apply", -// // Cmd: func(want []*apiv2.IP) []string { -// // return appendFromFileCommonArgs("ip", "apply") -// // }, -// // FsMocks: func(fs afero.Fs, want []*apiv2.IP) { -// // require.NoError(t, afero.WriteFile(fs, "/file.yaml", MustMarshalToMultiYAML(t, want), 0755)) -// // }, -// // // ClientMocks: &apitests.ClientMockFns{ -// // // Apiv1Mocks: &apitests.Apiv1MockFns{ -// // // IP: func(m *mock.Mock) { -// // // m.On("Allocate", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(v1.IpResponseToCreate(ip1())), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceAllocateResponse{ -// // // Ip: ip1(), -// // // }), nil) -// // // m.On("Allocate", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(v1.IpResponseToCreate(ip2())), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceAllocateResponse{ -// // // Ip: ip2(), -// // // }), nil) -// // // // FIXME: API does not return a conflict when already exists, so the update functionality does not work! -// // // }, -// // // }, -// // // }, -// // Want: []*apiv2.IP{ -// // ip1(), -// // ip2(), -// // }, -// // }, -// // { -// // Name: "update from file", -// // Cmd: func(want []*apiv2.IP) []string { -// // return appendFromFileCommonArgs("ip", "update") -// // }, -// // FsMocks: func(fs afero.Fs, want []*apiv2.IP) { -// // require.NoError(t, afero.WriteFile(fs, "/file.yaml", MustMarshalToMultiYAML(t, want), 0755)) -// // }, -// // // ClientMocks: &apitests.ClientMockFns{ -// // // Apiv1Mocks: &apitests.Apiv1MockFns{ -// // // IP: func(m *mock.Mock) { -// // // m.On("Update", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(&apiv2.IPServiceUpdateRequest{ -// // // Project: ip1().Project, -// // // Ip: ip1(), -// // // }), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceUpdateResponse{ -// // // Ip: ip1(), -// // // }), nil) -// // // }, -// // // }, -// // // }, -// // Want: []*apiv2.IP{ -// // ip1(), -// // }, -// // }, -// // { -// // Name: "create from file", -// // Cmd: func(want []*apiv2.IP) []string { -// // return appendFromFileCommonArgs("ip", "create") -// // }, -// // FsMocks: func(fs afero.Fs, want []*apiv2.IP) { -// // require.NoError(t, afero.WriteFile(fs, "/file.yaml", MustMarshalToMultiYAML(t, want), 0755)) -// // }, -// // ClientMocks: &apitests.ClientMockFns{ -// // Apiv1Mocks: &apitests.Apiv1MockFns{ -// // IP: func(m *mock.Mock) { -// // m.On("Allocate", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(v1.IpResponseToCreate(ip1())), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceAllocateResponse{ -// // Ip: ip1(), -// // }), nil) -// // }, -// // }, -// // }, -// // Want: []*apiv2.IP{ -// // ip1(), -// // }, -// // }, -// // { -// // Name: "delete from file", -// // Cmd: func(want []*apiv2.IP) []string { -// // return appendFromFileCommonArgs("ip", "delete") -// // }, -// // FsMocks: func(fs afero.Fs, want []*apiv2.IP) { -// // require.NoError(t, afero.WriteFile(fs, "/file.yaml", MustMarshalToMultiYAML(t, want), 0755)) -// // }, -// // ClientMocks: &apitests.ClientMockFns{ -// // Apiv1Mocks: &apitests.Apiv1MockFns{ -// // IP: func(m *mock.Mock) { -// // m.On("Delete", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(&apiv2.IPServiceDeleteRequest{ -// // Uuid: ip1().Uuid, -// // Project: ip1().Project, -// // }), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceDeleteResponse{ -// // Ip: ip1(), -// // }), nil) -// // }, -// // }, -// // }, -// // Want: []*apiv2.IP{ -// // ip1(), -// // }, -// // }, -// } -// for _, tt := range tests { -// tt.TestCmd(t) -// } -// } +func Test_IPCmd_MultiResult(t *testing.T) { + tests := []*Test[apiv2.IPServiceListRequest, apiv2.IPServiceListResponse]{ + { + Name: "list", + Cmd: func() []string { + return []string{"ip", "list", "--project", "a"} + }, + WantRequest: apiv2.IPServiceListRequest{ + Project: "a", + }, + WantResponse: apiv2.IPServiceListResponse{ + Ips: []*apiv2.IP{ + ip1(), + ip2(), + }, + }, + WantTable: new(` +IP PROJECT ID TYPE NAME ATTACHED SERVICE +4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b +1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a +`), + WantWideTable: new(` +IP PROJECT ID TYPE NAME DESCRIPTION LABELS +4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b b description a=b +1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx +`), + Template: new("{{ .ip }} {{ .project }}"), + WantTemplate: new(` +4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 +1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 + `), + WantMarkdown: new(` +| IP | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | +|---------|--------------------------------------|--------------------------------------|-----------|------|------------------| +| 4.3.2.1 | 46bdfc45-9c8d-4268-b359-b40e3079d384 | 9cef40ec-29c6-4dfa-aee8-47ee1f49223d | ephemeral | b | | +| 1.1.1.1 | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | +`), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} func Test_IPCmd_SingleResult(t *testing.T) { ip1 := ip1() @@ -210,6 +104,7 @@ func Test_IPCmd_SingleResult(t *testing.T) { WantResponse: apiv2.IPServiceGetResponse{ Ip: ip1, }, + WantObject: ip1, WantTable: new(` IP PROJECT ID TYPE NAME ATTACHED SERVICE 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a @@ -228,74 +123,6 @@ IP PROJECT ID | 1.1.1.1 | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | `), }, - // { - // Name: "delete", - // Cmd: func(want *apiv2.IP) []string { - // return []string{"ip", "rm", "--project", want.Project, want.Uuid} - // }, - // ClientMocks: &apitests.ClientMockFns{ - // Apiv1Mocks: &apitests.Apiv1MockFns{ - // IP: func(m *mock.Mock) { - // m.On("Delete", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(&apiv2.IPServiceDeleteRequest{ - // Project: ip1().Project, - // Uuid: ip1().Uuid, - // }), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceDeleteResponse{ - // Ip: ip1(), - // }), nil) - // }, - // }, - // }, - // Want: ip1(), - // }, - // { - // Name: "create", - // Cmd: func(want *apiv2.IP) []string { - // args := []string{"ip", "create", "--project", want.Project, "--description", want.Description, "--name", want.Name, "--tags", strings.Join(want.Tags, ",")} - // if want.Type == apiv2.IPType_IP_TYPE_STATIC { - // args = append(args, "--static") - // } - // AssertExhaustiveArgs(t, args, commonExcludedFileArgs()...) - // return args - // }, - // ClientMocks: &apitests.ClientMockFns{ - // Apiv1Mocks: &apitests.Apiv1MockFns{ - // IP: func(m *mock.Mock) { - // m.On("Allocate", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(v1.IpResponseToCreate(ip1())), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceAllocateResponse{ - // Ip: ip1(), - // }), nil) - // }, - // }, - // }, - // Want: ip1(), - // }, - // { - // Name: "update", - // Cmd: func(want *apiv2.IP) []string { - // args := []string{"ip", "update", want.Uuid, "--project", want.Project, "--description", want.Description, "--name", want.Name, "--tags", strings.Join(want.Tags, ",")} - // if want.Type == apiv2.IPType_IP_TYPE_STATIC { - // args = append(args, "--static") - // } - // AssertExhaustiveArgs(t, args, commonExcludedFileArgs()...) - // return args - // }, - // ClientMocks: &apitests.ClientMockFns{ - // Apiv1Mocks: &apitests.Apiv1MockFns{ - // IP: func(m *mock.Mock) { - // m.On("Get", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(&apiv2.IPServiceGetRequest{ - // Uuid: ip1().Uuid, - // Project: ip1().Project, - // }), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceGetResponse{ - // Ip: ip1(), - // }), nil) - - // m.On("Update", mock.Anything, testcommon.MatchByCmpDiff(t, connect.NewRequest(v1.IpResponseToUpdate(ip1())), cmpopts.IgnoreTypes(protoimpl.MessageState{}))).Return(connect.NewResponse(&apiv2.IPServiceUpdateResponse{ - // Ip: ip1(), - // }), nil) - // }, - // }, - // }, - // Want: ip1(), - // }, } for _, tt := range tests { tt.TestCmd(t) diff --git a/cmd/project_test.go b/cmd/project_test.go new file mode 100644 index 0000000..82a03e1 --- /dev/null +++ b/cmd/project_test.go @@ -0,0 +1,116 @@ +package cmd + +import ( + "testing" + "time" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + project1 = func() *apiv2.Project { + return &apiv2.Project{ + Uuid: "0d81bca7-73f6-4da3-8397-4a8c52a0c583", + Name: "project-a", + Description: "first project", + Tenant: "metal-stack", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2025, 6, 1, 10, 0, 0, 0, time.UTC)), + }, + } + } + project2 = func() *apiv2.Project { + return &apiv2.Project{ + Uuid: "f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c", + Name: "project-b", + Description: "second project", + Tenant: "metal-stack", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2025, 7, 15, 14, 30, 0, 0, time.UTC)), + }, + } + } +) + +func Test_ProjectCmd_SingleResult(t *testing.T) { + p1 := project1() + + tests := []*Test[apiv2.ProjectServiceGetRequest, apiv2.ProjectServiceGetResponse]{ + { + Name: "describe", + Cmd: func() []string { + return []string{"project", "describe", p1.Uuid} + }, + WantRequest: apiv2.ProjectServiceGetRequest{ + Project: p1.Uuid, + }, + WantResponse: apiv2.ProjectServiceGetResponse{ + Project: p1, + }, + WantObject: p1, + WantTable: new(` +ID TENANT NAME DESCRIPTION CREATION DATE +0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC +`), + WantWideTable: new(` +ID TENANT NAME DESCRIPTION CREATION DATE +0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC +`), + Template: new("{{ .uuid }} {{ .name }}"), + WantTemplate: new(` +0d81bca7-73f6-4da3-8397-4a8c52a0c583 project-a + `), + WantMarkdown: new(` +| ID | TENANT | NAME | DESCRIPTION | CREATION DATE | +|--------------------------------------|-------------|-----------|---------------|-------------------------| +| 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | +`), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_ProjectCmd_MultiResult(t *testing.T) { + tests := []*Test[apiv2.ProjectServiceListRequest, apiv2.ProjectServiceListResponse]{ + { + Name: "list", + Cmd: func() []string { + return []string{"project", "list"} + }, + WantRequest: apiv2.ProjectServiceListRequest{}, + WantResponse: apiv2.ProjectServiceListResponse{ + Projects: []*apiv2.Project{ + project1(), + project2(), + }, + }, + WantTable: new(` +ID TENANT NAME DESCRIPTION CREATION DATE +0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC +f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC +`), + WantWideTable: new(` +ID TENANT NAME DESCRIPTION CREATION DATE +0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC +f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC +`), + Template: new("{{ .uuid }} {{ .name }}"), + WantTemplate: new(` +0d81bca7-73f6-4da3-8397-4a8c52a0c583 project-a +f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c project-b + `), + WantMarkdown: new(` +| ID | TENANT | NAME | DESCRIPTION | CREATION DATE | +|--------------------------------------|-------------|-----------|----------------|-------------------------| +| 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | +| f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | metal-stack | project-b | second project | 2025-07-15 14:30:00 UTC | +`), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/cmd/tenant_test.go b/cmd/tenant_test.go new file mode 100644 index 0000000..5562fd3 --- /dev/null +++ b/cmd/tenant_test.go @@ -0,0 +1,50 @@ +package cmd + +import ( + "testing" + "time" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + tenant1 = func() *apiv2.Tenant { + return &apiv2.Tenant{ + Login: "metal-stack", + Name: "Metal Stack", + Email: "info@metal-stack.io", + Description: "a tenant", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } +) + +func Test_TenantCmd_SingleResult(t *testing.T) { + tn := tenant1() + + tests := []*Test[apiv2.TenantServiceGetRequest, apiv2.TenantServiceGetResponse]{ + { + Name: "describe", + Cmd: func() []string { + return []string{"tenant", "describe", tn.Login} + }, + WantRequest: apiv2.TenantServiceGetRequest{ + Login: tn.Login, + }, + WantResponse: apiv2.TenantServiceGetResponse{ + Tenant: tn, + }, + WantObject: tn, + Template: new("{{ .login }} {{ .name }}"), + WantTemplate: new(` +metal-stack Metal Stack + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/cmd/token_test.go b/cmd/token_test.go new file mode 100644 index 0000000..2411019 --- /dev/null +++ b/cmd/token_test.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "testing" + "time" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + token1 = func() *apiv2.Token { + return &apiv2.Token{ + Uuid: "a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90", + User: "admin@metal-stack.io", + Description: "ci token", + TokenType: apiv2.TokenType_TOKEN_TYPE_API, + Expires: timestamppb.New(time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC)), + IssuedAt: timestamppb.New(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + Permissions: nil, + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } +) + +func Test_TokenCmd_SingleResult(t *testing.T) { + tk := token1() + + tests := []*Test[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse]{ + { + Name: "describe", + Cmd: func() []string { + return []string{"token", "describe", tk.Uuid} + }, + WantRequest: apiv2.TokenServiceGetRequest{ + Uuid: tk.Uuid, + }, + WantResponse: apiv2.TokenServiceGetResponse{ + Token: tk, + }, + WantObject: tk, + Template: new("{{ .uuid }} {{ .description }}"), + WantTemplate: new(` +a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From 3f588be194b05799d0b4f7fef9c50e0ee13c2243 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 18 Mar 2026 17:29:46 +0100 Subject: [PATCH 03/34] Move into e2e package. --- cmd/root.go | 4 ++-- {cmd => pkg/tests/e2e/api}/image_test.go | 5 ++-- {cmd => pkg/tests/e2e/api}/ip_test.go | 11 +++++---- {cmd => pkg/tests/e2e/api}/project_test.go | 11 +++++---- {cmd => pkg/tests/e2e/api}/tenant_test.go | 7 +++--- {cmd => pkg/tests/e2e/api}/token_test.go | 7 +++--- cmd/common_test.go => pkg/tests/e2e/common.go | 23 ++++++++++--------- 7 files changed, 37 insertions(+), 31 deletions(-) rename {cmd => pkg/tests/e2e/api}/image_test.go (93%) rename {cmd => pkg/tests/e2e/api}/ip_test.go (93%) rename {cmd => pkg/tests/e2e/api}/project_test.go (91%) rename {cmd => pkg/tests/e2e/api}/tenant_test.go (82%) rename {cmd => pkg/tests/e2e/api}/token_test.go (85%) rename cmd/common_test.go => pkg/tests/e2e/common.go (93%) diff --git a/cmd/root.go b/cmd/root.go index f561d97..fc27a8f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -28,7 +28,7 @@ func Execute() { Completion: &completion.Completion{}, } - cmd := newRootCmd(cfg) + cmd := NewRootCmd(cfg) err := cmd.Execute() if err != nil { @@ -40,7 +40,7 @@ func Execute() { } } -func newRootCmd(c *config.Config) *cobra.Command { +func NewRootCmd(c *config.Config) *cobra.Command { rootCmd := &cobra.Command{ Use: config.BinaryName, Aliases: []string{"m"}, diff --git a/cmd/image_test.go b/pkg/tests/e2e/api/image_test.go similarity index 93% rename from cmd/image_test.go rename to pkg/tests/e2e/api/image_test.go index e871680..fdae41d 100644 --- a/cmd/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -1,9 +1,10 @@ -package cmd +package api_e2e import ( "testing" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" "github.com/metal-stack/metal-lib/pkg/pointer" ) @@ -29,7 +30,7 @@ var ( ) func Test_ImageCmd_List(t *testing.T) { - tests := []*Test[apiv2.ImageServiceListRequest, apiv2.ImageServiceListResponse]{ + tests := []*e2e.Test[apiv2.ImageServiceListRequest, apiv2.ImageServiceListResponse]{ { Name: "list", Cmd: func() []string { diff --git a/cmd/ip_test.go b/pkg/tests/e2e/api/ip_test.go similarity index 93% rename from cmd/ip_test.go rename to pkg/tests/e2e/api/ip_test.go index e6dc1fe..12db878 100644 --- a/cmd/ip_test.go +++ b/pkg/tests/e2e/api/ip_test.go @@ -1,9 +1,10 @@ -package cmd +package api_e2e import ( "testing" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" "github.com/metal-stack/metal-lib/pkg/tag" ) @@ -44,8 +45,8 @@ var ( } ) -func Test_IPCmd_MultiResult(t *testing.T) { - tests := []*Test[apiv2.IPServiceListRequest, apiv2.IPServiceListResponse]{ +func Test_IPCmd_List(t *testing.T) { + tests := []*e2e.Test[apiv2.IPServiceListRequest, apiv2.IPServiceListResponse]{ { Name: "list", Cmd: func() []string { @@ -88,10 +89,10 @@ IP PROJECT ID } } -func Test_IPCmd_SingleResult(t *testing.T) { +func Test_IPCmd_Describe(t *testing.T) { ip1 := ip1() - tests := []*Test[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse]{ + tests := []*e2e.Test[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse]{ { Name: "describe", Cmd: func() []string { diff --git a/cmd/project_test.go b/pkg/tests/e2e/api/project_test.go similarity index 91% rename from cmd/project_test.go rename to pkg/tests/e2e/api/project_test.go index 82a03e1..a107b69 100644 --- a/cmd/project_test.go +++ b/pkg/tests/e2e/api/project_test.go @@ -1,10 +1,11 @@ -package cmd +package api_e2e import ( "testing" "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -33,10 +34,10 @@ var ( } ) -func Test_ProjectCmd_SingleResult(t *testing.T) { +func Test_ProjectCmd_Describe(t *testing.T) { p1 := project1() - tests := []*Test[apiv2.ProjectServiceGetRequest, apiv2.ProjectServiceGetResponse]{ + tests := []*e2e.Test[apiv2.ProjectServiceGetRequest, apiv2.ProjectServiceGetResponse]{ { Name: "describe", Cmd: func() []string { @@ -73,8 +74,8 @@ ID TENANT NAME DESCRIPTION CRE } } -func Test_ProjectCmd_MultiResult(t *testing.T) { - tests := []*Test[apiv2.ProjectServiceListRequest, apiv2.ProjectServiceListResponse]{ +func Test_ProjectCmd_List(t *testing.T) { + tests := []*e2e.Test[apiv2.ProjectServiceListRequest, apiv2.ProjectServiceListResponse]{ { Name: "list", Cmd: func() []string { diff --git a/cmd/tenant_test.go b/pkg/tests/e2e/api/tenant_test.go similarity index 82% rename from cmd/tenant_test.go rename to pkg/tests/e2e/api/tenant_test.go index 5562fd3..fd0da46 100644 --- a/cmd/tenant_test.go +++ b/pkg/tests/e2e/api/tenant_test.go @@ -1,10 +1,11 @@ -package cmd +package api_e2e import ( "testing" "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -22,10 +23,10 @@ var ( } ) -func Test_TenantCmd_SingleResult(t *testing.T) { +func Test_TenantCmd_Describe(t *testing.T) { tn := tenant1() - tests := []*Test[apiv2.TenantServiceGetRequest, apiv2.TenantServiceGetResponse]{ + tests := []*e2e.Test[apiv2.TenantServiceGetRequest, apiv2.TenantServiceGetResponse]{ { Name: "describe", Cmd: func() []string { diff --git a/cmd/token_test.go b/pkg/tests/e2e/api/token_test.go similarity index 85% rename from cmd/token_test.go rename to pkg/tests/e2e/api/token_test.go index 2411019..aa2603c 100644 --- a/cmd/token_test.go +++ b/pkg/tests/e2e/api/token_test.go @@ -1,10 +1,11 @@ -package cmd +package api_e2e import ( "testing" "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -25,10 +26,10 @@ var ( } ) -func Test_TokenCmd_SingleResult(t *testing.T) { +func Test_TokenCmd_Describe(t *testing.T) { tk := token1() - tests := []*Test[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse]{ + tests := []*e2e.Test[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse]{ { Name: "describe", Cmd: func() []string { diff --git a/cmd/common_test.go b/pkg/tests/e2e/common.go similarity index 93% rename from cmd/common_test.go rename to pkg/tests/e2e/common.go index b1bfb06..60604e0 100644 --- a/cmd/common_test.go +++ b/pkg/tests/e2e/common.go @@ -1,4 +1,4 @@ -package cmd +package e2e import ( "bytes" @@ -17,6 +17,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" client "github.com/metal-stack/api/go/client" + "github.com/metal-stack/cli/cmd" "github.com/metal-stack/cli/cmd/completion" "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/testcommon" @@ -41,13 +42,13 @@ type Test[Request, Response any] struct { WantErr error WantRequest Request - WantResponse Response // for client return and json and yaml - WantObject proto.Message // domain object for yaml/json structural comparison - WantTable *string // for table printer - WantWideTable *string // for wide table printer - Template *string // for template printer - WantTemplate *string // for template printer - WantMarkdown *string // for markdown printer + WantResponse Response // for client return and json and yaml + WantObject proto.Message // domain object for yaml/json structural comparison + WantTable *string // for table printer + WantWideTable *string // for wide table printer + Template *string // for template printer + WantTemplate *string // for template printer + WantMarkdown *string // for markdown printer } func (c *Test[Request, Response]) TestCmd(t *testing.T) { @@ -57,7 +58,7 @@ func (c *Test[Request, Response]) TestCmd(t *testing.T) { if c.WantErr != nil { _, _, conf := c.newCmdConfig(t) - cmd := newRootCmd(conf) + cmd := cmd.NewRootCmd(conf) os.Args = append([]string{config.BinaryName}, c.Cmd()...) err := cmd.Execute() @@ -70,7 +71,7 @@ func (c *Test[Request, Response]) TestCmd(t *testing.T) { t.Run(fmt.Sprintf("%v", format.Args()), func(t *testing.T) { _, out, conf := c.newCmdConfig(t) - cmd := newRootCmd(conf) + cmd := cmd.NewRootCmd(conf) os.Args = append([]string{config.BinaryName}, c.Cmd()...) os.Args = append(os.Args, format.Args()...) @@ -135,7 +136,7 @@ func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { return fmt.Errorf("not exhaustive: does not contain %q", prefix) } - root := newRootCmd(&config.Config{}) + root := cmd.NewRootCmd(&config.Config{}) cmd, args, err := root.Find(args) require.NoError(t, err) From f66177d9cd3568034fe78291efe47743d84976e0 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 18 Mar 2026 17:51:22 +0100 Subject: [PATCH 04/34] Also add raw outputs with same approach. --- pkg/tests/e2e/api/image_test.go | 2 +- pkg/tests/e2e/api/ip_test.go | 7 +-- pkg/tests/e2e/api/project_test.go | 7 +-- pkg/tests/e2e/api/tenant_test.go | 7 +-- pkg/tests/e2e/api/token_test.go | 7 +-- pkg/tests/e2e/common.go | 87 ++++++++++++++++++++++++------- 6 files changed, 84 insertions(+), 33 deletions(-) diff --git a/pkg/tests/e2e/api/image_test.go b/pkg/tests/e2e/api/image_test.go index fdae41d..11b6969 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -30,7 +30,7 @@ var ( ) func Test_ImageCmd_List(t *testing.T) { - tests := []*e2e.Test[apiv2.ImageServiceListRequest, apiv2.ImageServiceListResponse]{ + tests := []*e2e.Test[apiv2.ImageServiceListRequest, apiv2.ImageServiceListResponse, *apiv2.Image]{ { Name: "list", Cmd: func() []string { diff --git a/pkg/tests/e2e/api/ip_test.go b/pkg/tests/e2e/api/ip_test.go index 12db878..decb230 100644 --- a/pkg/tests/e2e/api/ip_test.go +++ b/pkg/tests/e2e/api/ip_test.go @@ -46,7 +46,7 @@ var ( ) func Test_IPCmd_List(t *testing.T) { - tests := []*e2e.Test[apiv2.IPServiceListRequest, apiv2.IPServiceListResponse]{ + tests := []*e2e.Test[apiv2.IPServiceListRequest, apiv2.IPServiceListResponse, apiv2.IP]{ { Name: "list", Cmd: func() []string { @@ -92,7 +92,7 @@ IP PROJECT ID func Test_IPCmd_Describe(t *testing.T) { ip1 := ip1() - tests := []*e2e.Test[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse]{ + tests := []*e2e.Test[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse, *apiv2.IP]{ { Name: "describe", Cmd: func() []string { @@ -105,7 +105,8 @@ func Test_IPCmd_Describe(t *testing.T) { WantResponse: apiv2.IPServiceGetResponse{ Ip: ip1, }, - WantObject: ip1, + WantObject: ip1, + WantProtoObject: ip1, WantTable: new(` IP PROJECT ID TYPE NAME ATTACHED SERVICE 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a diff --git a/pkg/tests/e2e/api/project_test.go b/pkg/tests/e2e/api/project_test.go index a107b69..9c266d2 100644 --- a/pkg/tests/e2e/api/project_test.go +++ b/pkg/tests/e2e/api/project_test.go @@ -37,7 +37,7 @@ var ( func Test_ProjectCmd_Describe(t *testing.T) { p1 := project1() - tests := []*e2e.Test[apiv2.ProjectServiceGetRequest, apiv2.ProjectServiceGetResponse]{ + tests := []*e2e.Test[apiv2.ProjectServiceGetRequest, apiv2.ProjectServiceGetResponse, *apiv2.Project]{ { Name: "describe", Cmd: func() []string { @@ -49,7 +49,8 @@ func Test_ProjectCmd_Describe(t *testing.T) { WantResponse: apiv2.ProjectServiceGetResponse{ Project: p1, }, - WantObject: p1, + WantObject: p1, + WantProtoObject: p1, WantTable: new(` ID TENANT NAME DESCRIPTION CREATION DATE 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC @@ -75,7 +76,7 @@ ID TENANT NAME DESCRIPTION CRE } func Test_ProjectCmd_List(t *testing.T) { - tests := []*e2e.Test[apiv2.ProjectServiceListRequest, apiv2.ProjectServiceListResponse]{ + tests := []*e2e.Test[apiv2.ProjectServiceListRequest, apiv2.ProjectServiceListResponse, apiv2.Project]{ { Name: "list", Cmd: func() []string { diff --git a/pkg/tests/e2e/api/tenant_test.go b/pkg/tests/e2e/api/tenant_test.go index fd0da46..3591e73 100644 --- a/pkg/tests/e2e/api/tenant_test.go +++ b/pkg/tests/e2e/api/tenant_test.go @@ -26,7 +26,7 @@ var ( func Test_TenantCmd_Describe(t *testing.T) { tn := tenant1() - tests := []*e2e.Test[apiv2.TenantServiceGetRequest, apiv2.TenantServiceGetResponse]{ + tests := []*e2e.Test[apiv2.TenantServiceGetRequest, apiv2.TenantServiceGetResponse, *apiv2.Tenant]{ { Name: "describe", Cmd: func() []string { @@ -38,8 +38,9 @@ func Test_TenantCmd_Describe(t *testing.T) { WantResponse: apiv2.TenantServiceGetResponse{ Tenant: tn, }, - WantObject: tn, - Template: new("{{ .login }} {{ .name }}"), + WantObject: tn, + WantProtoObject: tn, + Template: new("{{ .login }} {{ .name }}"), WantTemplate: new(` metal-stack Metal Stack `), diff --git a/pkg/tests/e2e/api/token_test.go b/pkg/tests/e2e/api/token_test.go index aa2603c..bb99982 100644 --- a/pkg/tests/e2e/api/token_test.go +++ b/pkg/tests/e2e/api/token_test.go @@ -29,7 +29,7 @@ var ( func Test_TokenCmd_Describe(t *testing.T) { tk := token1() - tests := []*e2e.Test[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse]{ + tests := []*e2e.Test[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse, *apiv2.Token]{ { Name: "describe", Cmd: func() []string { @@ -41,8 +41,9 @@ func Test_TokenCmd_Describe(t *testing.T) { WantResponse: apiv2.TokenServiceGetResponse{ Token: tk, }, - WantObject: tk, - Template: new("{{ .uuid }} {{ .description }}"), + WantObject: tk, + WantProtoObject: tk, + Template: new("{{ .uuid }} {{ .description }}"), WantTemplate: new(` a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token `), diff --git a/pkg/tests/e2e/common.go b/pkg/tests/e2e/common.go index 60604e0..9bd73e6 100644 --- a/pkg/tests/e2e/common.go +++ b/pkg/tests/e2e/common.go @@ -3,6 +3,7 @@ package e2e import ( "bytes" "context" + "encoding/json" "fmt" "io" "log/slog" @@ -28,9 +29,10 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/runtime/protoimpl" + "sigs.k8s.io/yaml" ) -type Test[Request, Response any] struct { +type Test[Request, Response, Object any] struct { Name string Cmd func() []string @@ -38,20 +40,19 @@ type Test[Request, Response any] struct { FsMocks func(fs afero.Fs) MockStdin *bytes.Buffer - // DisableMockClient bool // can switch off mock client creation - - WantErr error - WantRequest Request - WantResponse Response // for client return and json and yaml - WantObject proto.Message // domain object for yaml/json structural comparison - WantTable *string // for table printer - WantWideTable *string // for wide table printer - Template *string // for template printer - WantTemplate *string // for template printer - WantMarkdown *string // for markdown printer + WantErr error + WantRequest Request // for client expectation + WantResponse Response // for client return + WantObject Object // domain object for rawyaml/rawjson structural comparison + WantProtoObject proto.Message // domain object for yaml/json structural comparison + WantTable *string // for table printer + WantWideTable *string // for wide table printer + Template *string // for template printer + WantTemplate *string // for template printer + WantMarkdown *string // for markdown printer } -func (c *Test[Request, Response]) TestCmd(t *testing.T) { +func (c *Test[Request, Response, Object]) TestCmd(t *testing.T) { require.NotEmpty(t, c.Name, "test name must not be empty") require.NotEmpty(t, c.Cmd, "cmd must not be empty") @@ -83,7 +84,7 @@ func (c *Test[Request, Response]) TestCmd(t *testing.T) { } } -func (c *Test[Request, Response]) newCmdConfig(t *testing.T) (any, *bytes.Buffer, *config.Config) { +func (c *Test[Request, Response, Object]) newCmdConfig(t *testing.T) (any, *bytes.Buffer, *config.Config) { interceptors := []connect.Interceptor{ &testClientInterceptor[Request, Response]{ t: t, @@ -148,13 +149,15 @@ func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { }) } -func outputFormats[Request, Response any](c *Test[Request, Response]) []outputFormat[Response] { +func outputFormats[Request, Response, Object any](c *Test[Request, Response, Object]) []outputFormat[Response] { var formats []outputFormat[Response] - if c.WantObject != nil { + if c.WantProtoObject != nil { formats = append(formats, - &protoYAMLOutputFormat[Response]{want: c.WantObject}, - &protoJSONOutputFormat[Response]{want: c.WantObject}, + &protoYAMLOutputFormat[Response]{want: c.WantProtoObject}, + &rawYamlOutputFormat[Object]{want: c.WantObject}, + &protoJSONOutputFormat[Response]{want: c.WantProtoObject}, + &rawJsonOutputFormat[Object]{want: c.WantObject}, ) } @@ -182,6 +185,48 @@ type outputFormat[R any] interface { Validate(t *testing.T, output []byte) } +type rawYamlOutputFormat[R any] struct { + want R +} + +func (o *rawYamlOutputFormat[R]) Args() []string { + return []string{"-o", "yamlraw"} +} + +func (o *rawYamlOutputFormat[R]) Validate(t *testing.T, output []byte) { + t.Logf("got following yamlraw output:\n\n%s\n", string(output)) + + var got R + + err := yaml.Unmarshal(output, &got) + require.NoError(t, err) + + if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } +} + +type rawJsonOutputFormat[R any] struct { + want R +} + +func (o *rawJsonOutputFormat[R]) Args() []string { + return []string{"-o", "jsonraw"} +} + +func (o *rawJsonOutputFormat[R]) Validate(t *testing.T, output []byte) { + t.Logf("got following jsonraw output:\n\n%s\n", string(output)) + + var got R + + err := json.Unmarshal(output, &got) + require.NoError(t, err) + + if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } +} + type protoYAMLOutputFormat[R any] struct { want proto.Message } @@ -191,7 +236,7 @@ func (o *protoYAMLOutputFormat[R]) Args() []string { } func (o *protoYAMLOutputFormat[R]) Validate(t *testing.T, output []byte) { - t.Logf("got following yaml output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", string(output)) + t.Logf("got following yaml output:\n\n%s\n", string(output)) got := proto.Clone(o.want) proto.Reset(got) @@ -213,7 +258,7 @@ func (o *protoJSONOutputFormat[R]) Args() []string { } func (o *protoJSONOutputFormat[R]) Validate(t *testing.T, output []byte) { - t.Logf("got following json output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", string(output)) + t.Logf("got following json output:\n\n%s\n", string(output)) got := proto.Clone(o.want) proto.Reset(got) @@ -263,6 +308,8 @@ func (o *templateOutputFormat[R]) Validate(t *testing.T, output []byte) { t.Logf("got following template output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", string(output)) assert.Equal(t, strings.TrimSpace(o.templateOutput), strings.TrimSpace(string(output))) + + // somehow this diff does not look nice anymore. :( // if diff := cmp.Diff(strings.TrimSpace(o.templateOutput), strings.TrimSpace(string(output))); diff != "" { // t.Errorf("diff (+got -want):\n %s", diff) // } From ed89dae180bef7a48e4726a69f9b3ab400c3a471 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 08:54:23 +0100 Subject: [PATCH 05/34] Refactor again. --- go.mod | 1 + go.sum | 2 + pkg/tests/e2e/api/image_test.go | 24 +-- pkg/tests/e2e/api/ip_test.go | 46 ++--- pkg/tests/e2e/api/project_test.go | 40 ++-- pkg/tests/e2e/api/tenant_test.go | 20 +- pkg/tests/e2e/api/token_test.go | 20 +- pkg/tests/e2e/test_client.go | 68 +++++++ .../e2e/{common.go => test_framework.go} | 181 ++++++------------ pkg/tests/e2e/test_interceptor.go | 39 ++++ 10 files changed, 244 insertions(+), 197 deletions(-) create mode 100644 pkg/tests/e2e/test_client.go rename pkg/tests/e2e/{common.go => test_framework.go} (59%) create mode 100644 pkg/tests/e2e/test_interceptor.go diff --git a/go.mod b/go.mod index 2e474d8..27207ca 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.26 require ( buf.build/go/protoyaml v0.6.0 connectrpc.com/connect v1.19.1 + connectrpc.com/validate v0.6.0 github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.18.0 github.com/google/go-cmp v0.7.0 diff --git a/go.sum b/go.sum index 17f23b6..689d4ac 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +connectrpc.com/validate v0.6.0 h1:DcrgDKt2ZScrUs/d/mh9itD2yeEa0UbBBa+i0mwzx+4= +connectrpc.com/validate v0.6.0/go.mod h1:ihrpI+8gVbLH1fvVWJL1I3j0CfWnF8P/90LsmluRiZs= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= diff --git a/pkg/tests/e2e/api/image_test.go b/pkg/tests/e2e/api/image_test.go index 11b6969..fdada87 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -30,21 +30,21 @@ var ( ) func Test_ImageCmd_List(t *testing.T) { - tests := []*e2e.Test[apiv2.ImageServiceListRequest, apiv2.ImageServiceListResponse, *apiv2.Image]{ + tests := []*e2e.Test[apiv2.ImageServiceListResponse, *apiv2.Image]{ { Name: "list", - Cmd: func() []string { - return []string{"image", "list"} - }, - WantRequest: apiv2.ImageServiceListRequest{ - Query: &apiv2.ImageQuery{}, - }, - WantResponse: apiv2.ImageServiceListResponse{ - Images: []*apiv2.Image{ - image1(), - image2(), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ImageServiceListRequest, apiv2.ImageServiceListResponse]{ + WantRequest: apiv2.ImageServiceListRequest{ + Query: &apiv2.ImageQuery{}, }, - }, + WantResponse: apiv2.ImageServiceListResponse{ + Images: []*apiv2.Image{ + image1(), + image2(), + }, + }, + }), + CmdArgs: []string{"image", "list"}, WantTable: new(` ID NAME DESCRIPTION FEATURES EXPIRATION STATUS ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported diff --git a/pkg/tests/e2e/api/ip_test.go b/pkg/tests/e2e/api/ip_test.go index decb230..fc014d9 100644 --- a/pkg/tests/e2e/api/ip_test.go +++ b/pkg/tests/e2e/api/ip_test.go @@ -46,21 +46,21 @@ var ( ) func Test_IPCmd_List(t *testing.T) { - tests := []*e2e.Test[apiv2.IPServiceListRequest, apiv2.IPServiceListResponse, apiv2.IP]{ + tests := []*e2e.Test[apiv2.IPServiceListResponse, apiv2.IP]{ { Name: "list", - Cmd: func() []string { - return []string{"ip", "list", "--project", "a"} - }, - WantRequest: apiv2.IPServiceListRequest{ - Project: "a", - }, - WantResponse: apiv2.IPServiceListResponse{ - Ips: []*apiv2.IP{ - ip1(), - ip2(), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.IPServiceListRequest, apiv2.IPServiceListResponse]{ + WantRequest: apiv2.IPServiceListRequest{ + Project: ip1().Project, }, - }, + WantResponse: apiv2.IPServiceListResponse{ + Ips: []*apiv2.IP{ + ip1(), + ip2(), + }, + }, + }), + CmdArgs: []string{"ip", "list", "--project", ip1().Project}, WantTable: new(` IP PROJECT ID TYPE NAME ATTACHED SERVICE 4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b @@ -92,19 +92,19 @@ IP PROJECT ID func Test_IPCmd_Describe(t *testing.T) { ip1 := ip1() - tests := []*e2e.Test[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse, *apiv2.IP]{ + tests := []*e2e.Test[apiv2.IPServiceGetResponse, *apiv2.IP]{ { Name: "describe", - Cmd: func() []string { - return []string{"ip", "describe", "--project", ip1.Project, ip1.Ip} - }, - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1.Ip, - Project: ip1.Project, - }, - WantResponse: apiv2.IPServiceGetResponse{ - Ip: ip1, - }, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse]{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1.Ip, + Project: ip1.Project, + }, + WantResponse: apiv2.IPServiceGetResponse{ + Ip: ip1, + }, + }), + CmdArgs: []string{"ip", "describe", "--project", ip1.Project, ip1.Ip}, WantObject: ip1, WantProtoObject: ip1, WantTable: new(` diff --git a/pkg/tests/e2e/api/project_test.go b/pkg/tests/e2e/api/project_test.go index 9c266d2..043c0fb 100644 --- a/pkg/tests/e2e/api/project_test.go +++ b/pkg/tests/e2e/api/project_test.go @@ -37,18 +37,18 @@ var ( func Test_ProjectCmd_Describe(t *testing.T) { p1 := project1() - tests := []*e2e.Test[apiv2.ProjectServiceGetRequest, apiv2.ProjectServiceGetResponse, *apiv2.Project]{ + tests := []*e2e.Test[apiv2.ProjectServiceGetResponse, *apiv2.Project]{ { Name: "describe", - Cmd: func() []string { - return []string{"project", "describe", p1.Uuid} - }, - WantRequest: apiv2.ProjectServiceGetRequest{ - Project: p1.Uuid, - }, - WantResponse: apiv2.ProjectServiceGetResponse{ - Project: p1, - }, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ProjectServiceGetRequest, apiv2.ProjectServiceGetResponse]{ + WantRequest: apiv2.ProjectServiceGetRequest{ + Project: p1.Uuid, + }, + WantResponse: apiv2.ProjectServiceGetResponse{ + Project: p1, + }, + }), + CmdArgs: []string{"project", "describe", p1.Uuid}, WantObject: p1, WantProtoObject: p1, WantTable: new(` @@ -76,19 +76,19 @@ ID TENANT NAME DESCRIPTION CRE } func Test_ProjectCmd_List(t *testing.T) { - tests := []*e2e.Test[apiv2.ProjectServiceListRequest, apiv2.ProjectServiceListResponse, apiv2.Project]{ + tests := []*e2e.Test[apiv2.ProjectServiceListResponse, apiv2.Project]{ { Name: "list", - Cmd: func() []string { - return []string{"project", "list"} - }, - WantRequest: apiv2.ProjectServiceListRequest{}, - WantResponse: apiv2.ProjectServiceListResponse{ - Projects: []*apiv2.Project{ - project1(), - project2(), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ProjectServiceListRequest, apiv2.ProjectServiceListResponse]{ + WantRequest: apiv2.ProjectServiceListRequest{}, + WantResponse: apiv2.ProjectServiceListResponse{ + Projects: []*apiv2.Project{ + project1(), + project2(), + }, }, - }, + }), + CmdArgs: []string{"project", "list"}, WantTable: new(` ID TENANT NAME DESCRIPTION CREATION DATE 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC diff --git a/pkg/tests/e2e/api/tenant_test.go b/pkg/tests/e2e/api/tenant_test.go index 3591e73..0e564b5 100644 --- a/pkg/tests/e2e/api/tenant_test.go +++ b/pkg/tests/e2e/api/tenant_test.go @@ -26,18 +26,18 @@ var ( func Test_TenantCmd_Describe(t *testing.T) { tn := tenant1() - tests := []*e2e.Test[apiv2.TenantServiceGetRequest, apiv2.TenantServiceGetResponse, *apiv2.Tenant]{ + tests := []*e2e.Test[apiv2.TenantServiceGetResponse, *apiv2.Tenant]{ { Name: "describe", - Cmd: func() []string { - return []string{"tenant", "describe", tn.Login} - }, - WantRequest: apiv2.TenantServiceGetRequest{ - Login: tn.Login, - }, - WantResponse: apiv2.TenantServiceGetResponse{ - Tenant: tn, - }, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.TenantServiceGetRequest, apiv2.TenantServiceGetResponse]{ + WantRequest: apiv2.TenantServiceGetRequest{ + Login: tn.Login, + }, + WantResponse: apiv2.TenantServiceGetResponse{ + Tenant: tn, + }, + }), + CmdArgs: []string{"tenant", "describe", tn.Login}, WantObject: tn, WantProtoObject: tn, Template: new("{{ .login }} {{ .name }}"), diff --git a/pkg/tests/e2e/api/token_test.go b/pkg/tests/e2e/api/token_test.go index bb99982..026b453 100644 --- a/pkg/tests/e2e/api/token_test.go +++ b/pkg/tests/e2e/api/token_test.go @@ -29,18 +29,18 @@ var ( func Test_TokenCmd_Describe(t *testing.T) { tk := token1() - tests := []*e2e.Test[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse, *apiv2.Token]{ + tests := []*e2e.Test[apiv2.TokenServiceGetResponse, *apiv2.Token]{ { Name: "describe", - Cmd: func() []string { - return []string{"token", "describe", tk.Uuid} - }, - WantRequest: apiv2.TokenServiceGetRequest{ - Uuid: tk.Uuid, - }, - WantResponse: apiv2.TokenServiceGetResponse{ - Token: tk, - }, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse]{ + WantRequest: apiv2.TokenServiceGetRequest{ + Uuid: tk.Uuid, + }, + WantResponse: apiv2.TokenServiceGetResponse{ + Token: tk, + }, + }), + CmdArgs: []string{"token", "describe", tk.Uuid}, WantObject: tk, WantProtoObject: tk, Template: new("{{ .uuid }} {{ .description }}"), diff --git a/pkg/tests/e2e/test_client.go b/pkg/tests/e2e/test_client.go new file mode 100644 index 0000000..670bc3f --- /dev/null +++ b/pkg/tests/e2e/test_client.go @@ -0,0 +1,68 @@ +package e2e + +import ( + "bytes" + "io" + "log/slog" + "testing" + + "connectrpc.com/connect" + "connectrpc.com/validate" + client "github.com/metal-stack/api/go/client" + "github.com/metal-stack/cli/cmd" + "github.com/metal-stack/cli/cmd/completion" + "github.com/metal-stack/cli/cmd/config" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +type TestClientConfig[Request, Response any] struct { + WantRequest Request // for client expectation + WantResponse Response // for client return + + FsMocks func(fs afero.Fs) + MockStdin *bytes.Buffer +} + +func NewRootCmd[Request, Response any](t *testing.T, c *TestClientConfig[Request, Response]) NewRootCmdFunc { + interceptors := []connect.Interceptor{ + validate.NewInterceptor(), + &testClientInterceptor[Request, Response]{ + t: t, + response: c.WantResponse, + request: c.WantRequest, + }, + } + + cl, err := client.New(&client.DialConfig{ + BaseURL: "http://this-is-just-for-testing", + Interceptors: interceptors, + UserAgent: "cli-test", + Log: slog.Default(), + }) + require.NoError(t, err) + + fs := afero.NewMemMapFs() + if c.FsMocks != nil { + c.FsMocks(fs) + } + + var in io.Reader + if c.MockStdin != nil { + in = bytes.NewReader(c.MockStdin.Bytes()) + } + + return func() (*cobra.Command, *bytes.Buffer) { + var out bytes.Buffer + + return cmd.NewRootCmd(&config.Config{ + Fs: fs, + Out: &out, + In: in, + PromptOut: io.Discard, + Completion: &completion.Completion{}, + Client: cl, + }), &out + } +} diff --git a/pkg/tests/e2e/common.go b/pkg/tests/e2e/test_framework.go similarity index 59% rename from pkg/tests/e2e/common.go rename to pkg/tests/e2e/test_framework.go index 9bd73e6..765efda 100644 --- a/pkg/tests/e2e/common.go +++ b/pkg/tests/e2e/test_framework.go @@ -2,11 +2,8 @@ package e2e import ( "bytes" - "context" "encoding/json" "fmt" - "io" - "log/slog" "os" "strings" "testing" @@ -14,15 +11,12 @@ import ( "slices" "buf.build/go/protoyaml" - "connectrpc.com/connect" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - client "github.com/metal-stack/api/go/client" "github.com/metal-stack/cli/cmd" - "github.com/metal-stack/cli/cmd/completion" "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/testcommon" - "github.com/spf13/afero" + "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -32,99 +26,67 @@ import ( "sigs.k8s.io/yaml" ) -type Test[Request, Response, Object any] struct { +// NewRootCmdFunc returns the root command for the cli and an output buffer which returns the output after command execution +type NewRootCmdFunc func() (rootCmd *cobra.Command, out *bytes.Buffer) + +type Test[Response, Object any] struct { Name string - Cmd func() []string - Client client.Client - FsMocks func(fs afero.Fs) - MockStdin *bytes.Buffer + NewRootCmd NewRootCmdFunc + CmdArgs []string + Out *bytes.Buffer - WantErr error - WantRequest Request // for client expectation - WantResponse Response // for client return - WantObject Object // domain object for rawyaml/rawjson structural comparison - WantProtoObject proto.Message // domain object for yaml/json structural comparison + // output format tests + WantObject Object // object for rawyaml/rawjson structural comparison + WantProtoObject proto.Message // object for yaml/json structural comparison WantTable *string // for table printer WantWideTable *string // for wide table printer - Template *string // for template printer - WantTemplate *string // for template printer WantMarkdown *string // for markdown printer + WantTemplate *string // for template printer + Template *string // for template printer + + WantErr error } -func (c *Test[Request, Response, Object]) TestCmd(t *testing.T) { +func (c *Test[Response, Object]) TestCmd(t *testing.T) { require.NotEmpty(t, c.Name, "test name must not be empty") - require.NotEmpty(t, c.Cmd, "cmd must not be empty") + require.NotEmpty(t, c.CmdArgs, "cmd must not be empty") - if c.WantErr != nil { - _, _, conf := c.newCmdConfig(t) + rootCmd, out := c.NewRootCmd() - cmd := cmd.NewRootCmd(conf) - os.Args = append([]string{config.BinaryName}, c.Cmd()...) + if c.WantErr != nil { + os.Args = append([]string{rootCmd.Use}, c.CmdArgs...) - err := cmd.Execute() + err := rootCmd.Execute() if diff := cmp.Diff(c.WantErr, err, testcommon.IgnoreUnexported(), testcommon.ErrorStringComparer()); diff != "" { t.Errorf("error diff (+got -want):\n %s", diff) } } - for _, format := range outputFormats(c) { - t.Run(fmt.Sprintf("%v", format.Args()), func(t *testing.T) { - _, out, conf := c.newCmdConfig(t) + formats := outputFormats(c) + + if len(formats) == 0 { + t.Errorf("at least one want section for output formats must be specified, otherwise no command is getting executed") + return + } + + for _, format := range formats { + succeeded := t.Run(fmt.Sprintf("%v", format.Args()), func(t *testing.T) { + out.Reset() - cmd := cmd.NewRootCmd(conf) - os.Args = append([]string{config.BinaryName}, c.Cmd()...) + os.Args = append([]string{rootCmd.Use}, c.CmdArgs...) os.Args = append(os.Args, format.Args()...) - err := cmd.Execute() + err := rootCmd.Execute() require.NoError(t, err) format.Validate(t, out.Bytes()) }) - } -} - -func (c *Test[Request, Response, Object]) newCmdConfig(t *testing.T) (any, *bytes.Buffer, *config.Config) { - interceptors := []connect.Interceptor{ - &testClientInterceptor[Request, Response]{ - t: t, - response: c.WantResponse, - request: c.WantRequest, - }, - // validate.NewInterceptor(), - } - - cl, err := client.New(&client.DialConfig{ - BaseURL: "http://this-is-just-for-testing", - Interceptors: interceptors, - UserAgent: "cli-test", - Log: slog.Default(), - }) - require.NoError(t, err) - - fs := afero.NewMemMapFs() - if c.FsMocks != nil { - c.FsMocks(fs) - } - var in io.Reader - if c.MockStdin != nil { - in = bytes.NewReader(c.MockStdin.Bytes()) - } - - var ( - out bytes.Buffer - config = &config.Config{ - Fs: fs, - Out: &out, - In: in, - PromptOut: io.Discard, - Completion: &completion.Completion{}, - Client: cl, + if !succeeded { + t.FailNow() } - ) - - return nil, &out, config + } } func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { @@ -149,8 +111,8 @@ func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { }) } -func outputFormats[Request, Response, Object any](c *Test[Request, Response, Object]) []outputFormat[Response] { - var formats []outputFormat[Response] +func outputFormats[Response, Object any](c *Test[Response, Object]) []outputFormat { + var formats []outputFormat if c.WantProtoObject != nil { formats = append(formats, @@ -162,25 +124,25 @@ func outputFormats[Request, Response, Object any](c *Test[Request, Response, Obj } if c.WantTable != nil { - formats = append(formats, &tableOutputFormat[Response]{table: *c.WantTable}) + formats = append(formats, &tableOutputFormat{table: *c.WantTable}) } if c.WantWideTable != nil { - formats = append(formats, &wideTableOutputFormat[Response]{table: *c.WantWideTable}) + formats = append(formats, &wideTableOutputFormat{table: *c.WantWideTable}) } if c.Template != nil && c.WantTemplate != nil { - formats = append(formats, &templateOutputFormat[Response]{template: *c.Template, templateOutput: *c.WantTemplate}) + formats = append(formats, &templateOutputFormat{template: *c.Template, templateOutput: *c.WantTemplate}) } if c.WantMarkdown != nil { - formats = append(formats, &markdownOutputFormat[Response]{table: *c.WantMarkdown}) + formats = append(formats, &markdownOutputFormat{table: *c.WantMarkdown}) } return formats } -type outputFormat[R any] interface { +type outputFormat interface { Args() []string Validate(t *testing.T, output []byte) } @@ -271,59 +233,56 @@ func (o *protoJSONOutputFormat[R]) Validate(t *testing.T, output []byte) { } } -type tableOutputFormat[R any] struct { +type tableOutputFormat struct { table string } -func (o *tableOutputFormat[R]) Args() []string { +func (o *tableOutputFormat) Args() []string { return []string{"-o", "table"} } -func (o *tableOutputFormat[R]) Validate(t *testing.T, output []byte) { +func (o *tableOutputFormat) Validate(t *testing.T, output []byte) { validateTableRows(t, o.table, string(output)) } -type wideTableOutputFormat[R any] struct { +type wideTableOutputFormat struct { table string } -func (o *wideTableOutputFormat[R]) Args() []string { +func (o *wideTableOutputFormat) Args() []string { return []string{"-o", "wide"} } -func (o *wideTableOutputFormat[R]) Validate(t *testing.T, output []byte) { +func (o *wideTableOutputFormat) Validate(t *testing.T, output []byte) { validateTableRows(t, o.table, string(output)) } -type templateOutputFormat[R any] struct { +type templateOutputFormat struct { template string templateOutput string } -func (o *templateOutputFormat[R]) Args() []string { +func (o *templateOutputFormat) Args() []string { return []string{"-o", "template", "--template", o.template} } -func (o *templateOutputFormat[R]) Validate(t *testing.T, output []byte) { +func (o *templateOutputFormat) Validate(t *testing.T, output []byte) { t.Logf("got following template output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", string(output)) - assert.Equal(t, strings.TrimSpace(o.templateOutput), strings.TrimSpace(string(output))) - - // somehow this diff does not look nice anymore. :( - // if diff := cmp.Diff(strings.TrimSpace(o.templateOutput), strings.TrimSpace(string(output))); diff != "" { - // t.Errorf("diff (+got -want):\n %s", diff) - // } + if diff := cmp.Diff(strings.TrimSpace(o.templateOutput), strings.TrimSpace(string(output))); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } } -type markdownOutputFormat[R any] struct { +type markdownOutputFormat struct { table string } -func (o *markdownOutputFormat[R]) Args() []string { +func (o *markdownOutputFormat) Args() []string { return []string{"-o", "markdown"} } -func (o *markdownOutputFormat[R]) Validate(t *testing.T, output []byte) { +func (o *markdownOutputFormat) Validate(t *testing.T, output []byte) { validateTableRows(t, o.table, string(output)) } @@ -346,6 +305,7 @@ func validateTableRows(t *testing.T, want, got string) { t.Logf("got following table output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", trimmedGot) + // somehow this diff does not look nice anymore. :( // t.Log(cmp.Diff(trimmedWant, trimmedGot)) require.Equal(t, len(wantRows), len(gotRows), "tables have different lengths") @@ -361,26 +321,3 @@ func validateTableRows(t *testing.T, want, got string) { } } } - -type testClientInterceptor[Request, Response any] struct { - t *testing.T - request Request - response Response -} - -func (t *testClientInterceptor[Request, Response]) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { - return func(ctx context.Context, ar connect.AnyRequest) (connect.AnyResponse, error) { - assert.Equal(t.t, &t.request, ar.Any()) - return connect.NewResponse(&t.response), nil - } -} - -func (t *testClientInterceptor[Request, Response]) WrapStreamingClient(connect.StreamingClientFunc) connect.StreamingClientFunc { - t.t.Errorf("streaming not supported") - return nil -} - -func (t *testClientInterceptor[Request, Response]) WrapStreamingHandler(connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { - t.t.Errorf("streaming not supported") - return nil -} diff --git a/pkg/tests/e2e/test_interceptor.go b/pkg/tests/e2e/test_interceptor.go new file mode 100644 index 0000000..9cc4da1 --- /dev/null +++ b/pkg/tests/e2e/test_interceptor.go @@ -0,0 +1,39 @@ +package e2e + +import ( + "context" + "testing" + + "connectrpc.com/connect" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/metal-stack/metal-lib/pkg/testcommon" + "google.golang.org/protobuf/runtime/protoimpl" +) + +type testClientInterceptor[Request, Response any] struct { + t *testing.T + request Request + response Response +} + +func (t *testClientInterceptor[Request, Response]) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, ar connect.AnyRequest) (connect.AnyResponse, error) { + if diff := cmp.Diff(&t.request, ar.Any(), testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + t.t.Errorf("request diff (+got -want):\n %s", diff) + t.t.FailNow() + } + + return connect.NewResponse(&t.response), nil + } +} + +func (t *testClientInterceptor[Request, Response]) WrapStreamingClient(connect.StreamingClientFunc) connect.StreamingClientFunc { + t.t.Errorf("streaming not supported") + return nil +} + +func (t *testClientInterceptor[Request, Response]) WrapStreamingHandler(connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + t.t.Errorf("streaming not supported") + return nil +} From 48383c06608b6ed13abb8b20c63a9351a642749f Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 09:19:25 +0100 Subject: [PATCH 06/34] Fix. --- pkg/tests/e2e/api/image_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/tests/e2e/api/image_test.go b/pkg/tests/e2e/api/image_test.go index fdada87..e4bd1ee 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -5,15 +5,14 @@ import ( apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" - "github.com/metal-stack/metal-lib/pkg/pointer" ) var ( image1 = func() *apiv2.Image { return &apiv2.Image{ Id: "ubuntu-24.04", - Name: pointer.Pointer("Ubuntu 24.04"), - Description: pointer.Pointer("Ubuntu 24.04 LTS"), + Name: new("Ubuntu 24.04"), + Description: new("Ubuntu 24.04 LTS"), Features: []apiv2.ImageFeature{apiv2.ImageFeature_IMAGE_FEATURE_MACHINE}, Classification: apiv2.ImageClassification_IMAGE_CLASSIFICATION_SUPPORTED, } @@ -21,8 +20,8 @@ var ( image2 = func() *apiv2.Image { return &apiv2.Image{ Id: "firewall-3.0", - Name: pointer.Pointer("Firewall 3.0"), - Description: pointer.Pointer("Metal Firewall"), + Name: new("Firewall 3.0"), + Description: new("Metal Firewall"), Features: []apiv2.ImageFeature{apiv2.ImageFeature_IMAGE_FEATURE_FIREWALL}, Classification: apiv2.ImageClassification_IMAGE_CLASSIFICATION_PREVIEW, } From ae57709fcfaaedab98f096acc37b75389c6878d8 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 10:43:09 +0100 Subject: [PATCH 07/34] Fixes and run in time bubble. --- cmd/api/v2/image.go | 5 -- cmd/api/v2/ip.go | 8 +- pkg/tests/e2e/api/image_test.go | 67 +++++++++++++---- pkg/tests/e2e/api/ip_test.go | 117 +++++++++++++++++++++--------- pkg/tests/e2e/api/project_test.go | 56 +++++++------- pkg/tests/e2e/api/tenant_test.go | 21 +++++- pkg/tests/e2e/api/token_test.go | 25 +++++-- pkg/tests/e2e/test_client.go | 6 +- pkg/tests/e2e/test_framework.go | 73 ++++++++++++++++--- pkg/tests/e2e/test_interceptor.go | 3 +- 10 files changed, 270 insertions(+), 111 deletions(-) diff --git a/cmd/api/v2/image.go b/cmd/api/v2/image.go index 1275fb2..4cac62e 100644 --- a/cmd/api/v2/image.go +++ b/cmd/api/v2/image.go @@ -33,11 +33,6 @@ func newImageCmd(c *config.Config) *cobra.Command { DescribePrinter: func() printers.Printer { return c.DescribePrinter }, ListPrinter: func() printers.Printer { return c.ListPrinter }, OnlyCmds: genericcli.OnlyCmds(genericcli.DescribeCmd, genericcli.ListCmd), - DescribeCmdMutateFn: func(cmd *cobra.Command) { - cmd.RunE = func(cmd *cobra.Command, args []string) error { - return gcli.DescribeAndPrint("", w.c.DescribePrinter) - } - }, ListCmdMutateFn: func(cmd *cobra.Command) { cmd.Flags().StringP("id", "", "", "image id to filter for") cmd.Flags().StringP("os", "", "", "image os to filter for") diff --git a/cmd/api/v2/ip.go b/cmd/api/v2/ip.go index 3e372ac..dc50684 100644 --- a/cmd/api/v2/ip.go +++ b/cmd/api/v2/ip.go @@ -9,6 +9,7 @@ import ( "github.com/metal-stack/cli/pkg/helpers" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -69,9 +70,9 @@ func newIPCmd(c *config.Config) *cobra.Command { CreateRequestFromCLI: func() (*apiv2.IPServiceCreateRequest, error) { return &apiv2.IPServiceCreateRequest{ Project: c.GetProject(), - Name: new(viper.GetString("name")), - Description: new(viper.GetString("description")), Network: viper.GetString("network"), + Name: pointer.PointerOrNil(viper.GetString("name")), + Description: pointer.PointerOrNil(viper.GetString("description")), // Labels: viper.GetStringSlice("tags"), // FIXME implement Type: new(ipStaticToType(viper.GetBool("static"))), AddressFamily: addressFamilyToType(viper.GetString("addressfamily")), @@ -204,7 +205,9 @@ func (c *ip) Convert(r *apiv2.IP) (string, *apiv2.IPServiceCreateRequest, *apiv2 func IpResponseToCreate(r *apiv2.IP) *apiv2.IPServiceCreateRequest { return &apiv2.IPServiceCreateRequest{ + Ip: &r.Ip, Project: r.Project, + Network: r.Network, Name: &r.Name, Description: &r.Description, Labels: r.Meta.Labels, @@ -213,7 +216,6 @@ func IpResponseToCreate(r *apiv2.IP) *apiv2.IPServiceCreateRequest { } func (c *ip) IpResponseToUpdate(desired *apiv2.IP) (*apiv2.IPServiceUpdateRequest, error) { - ctx, cancel := c.c.NewRequestContext() defer cancel() diff --git a/pkg/tests/e2e/api/image_test.go b/pkg/tests/e2e/api/image_test.go index e4bd1ee..6d33f94 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -31,7 +31,8 @@ var ( func Test_ImageCmd_List(t *testing.T) { tests := []*e2e.Test[apiv2.ImageServiceListResponse, *apiv2.Image]{ { - Name: "list", + Name: "list", + CmdArgs: []string{"image", "list"}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ImageServiceListRequest, apiv2.ImageServiceListResponse]{ WantRequest: apiv2.ImageServiceListRequest{ Query: &apiv2.ImageQuery{}, @@ -43,28 +44,64 @@ func Test_ImageCmd_List(t *testing.T) { }, }, }), - CmdArgs: []string{"image", "list"}, WantTable: new(` -ID NAME DESCRIPTION FEATURES EXPIRATION STATUS -ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported -firewall-3.0 Firewall 3.0 Metal Firewall firewall preview -`), + ID NAME DESCRIPTION FEATURES EXPIRATION STATUS + ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported + firewall-3.0 Firewall 3.0 Metal Firewall firewall preview + `), WantWideTable: new(` -ID NAME DESCRIPTION FEATURES EXPIRATION STATUS -ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported -firewall-3.0 Firewall 3.0 Metal Firewall firewall preview -`), + ID NAME DESCRIPTION FEATURES EXPIRATION STATUS + ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported + firewall-3.0 Firewall 3.0 Metal Firewall firewall preview + `), Template: new("{{ .id }} {{ .name }}"), WantTemplate: new(` ubuntu-24.04 Ubuntu 24.04 firewall-3.0 Firewall 3.0 `), WantMarkdown: new(` -| ID | NAME | DESCRIPTION | FEATURES | EXPIRATION | STATUS | -|--------------|--------------|------------------|----------|------------|-----------| -| ubuntu-24.04 | Ubuntu 24.04 | Ubuntu 24.04 LTS | machine | | supported | -| firewall-3.0 | Firewall 3.0 | Metal Firewall | firewall | | preview | -`), + | ID | NAME | DESCRIPTION | FEATURES | EXPIRATION | STATUS | + |--------------|--------------|------------------|----------|------------|-----------| + | ubuntu-24.04 | Ubuntu 24.04 | Ubuntu 24.04 LTS | machine | | supported | + | firewall-3.0 | Firewall 3.0 | Metal Firewall | firewall | | preview | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_ImageCmd_Describe(t *testing.T) { + tests := []*e2e.Test[apiv2.ImageServiceGetResponse, *apiv2.Image]{ + { + Name: "list", + CmdArgs: []string{"image", "describe", image1().Id}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ImageServiceGetRequest, apiv2.ImageServiceGetResponse]{ + WantRequest: apiv2.ImageServiceGetRequest{ + Id: image1().Id, + }, + WantResponse: apiv2.ImageServiceGetResponse{ + Image: image1(), + }, + }), + WantTable: new(` + ID NAME DESCRIPTION FEATURES EXPIRATION STATUS + ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported + `), + WantWideTable: new(` + ID NAME DESCRIPTION FEATURES EXPIRATION STATUS + ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported + `), + Template: new("{{ .id }} {{ .name }}"), + WantTemplate: new(` + ubuntu-24.04 Ubuntu 24.04 + `), + WantMarkdown: new(` + | ID | NAME | DESCRIPTION | FEATURES | EXPIRATION | STATUS | + |--------------|--------------|------------------|----------|------------|-----------| + | ubuntu-24.04 | Ubuntu 24.04 | Ubuntu 24.04 LTS | machine | | supported | + `), }, } for _, tt := range tests { diff --git a/pkg/tests/e2e/api/ip_test.go b/pkg/tests/e2e/api/ip_test.go index fc014d9..7874cad 100644 --- a/pkg/tests/e2e/api/ip_test.go +++ b/pkg/tests/e2e/api/ip_test.go @@ -12,6 +12,7 @@ var ( ip1 = func() *apiv2.IP { return &apiv2.IP{ Uuid: "2e0144a2-09ef-42b7-b629-4263295db6e8", + Network: "internet", Ip: "1.1.1.1", Name: "a", Description: "a description", @@ -29,6 +30,7 @@ var ( ip2 = func() *apiv2.IP { return &apiv2.IP{ Uuid: "9cef40ec-29c6-4dfa-aee8-47ee1f49223d", + Network: "internet", Ip: "4.3.2.1", Name: "b", Description: "b description", @@ -48,7 +50,8 @@ var ( func Test_IPCmd_List(t *testing.T) { tests := []*e2e.Test[apiv2.IPServiceListResponse, apiv2.IP]{ { - Name: "list", + Name: "list", + CmdArgs: []string{"ip", "list", "--project", ip1().Project}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.IPServiceListRequest, apiv2.IPServiceListResponse]{ WantRequest: apiv2.IPServiceListRequest{ Project: ip1().Project, @@ -60,28 +63,27 @@ func Test_IPCmd_List(t *testing.T) { }, }, }), - CmdArgs: []string{"ip", "list", "--project", ip1().Project}, WantTable: new(` -IP PROJECT ID TYPE NAME ATTACHED SERVICE -4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b -1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a -`), + IP PROJECT ID TYPE NAME ATTACHED SERVICE + 4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + `), WantWideTable: new(` -IP PROJECT ID TYPE NAME DESCRIPTION LABELS -4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b b description a=b -1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx -`), + IP PROJECT ID TYPE NAME DESCRIPTION LABELS + 4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b b description a=b + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx + `), Template: new("{{ .ip }} {{ .project }}"), WantTemplate: new(` 4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 `), WantMarkdown: new(` -| IP | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | -|---------|--------------------------------------|--------------------------------------|-----------|------|------------------| -| 4.3.2.1 | 46bdfc45-9c8d-4268-b359-b40e3079d384 | 9cef40ec-29c6-4dfa-aee8-47ee1f49223d | ephemeral | b | | -| 1.1.1.1 | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | -`), + | IP | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | + |---------|--------------------------------------|--------------------------------------|-----------|------|------------------| + | 4.3.2.1 | 46bdfc45-9c8d-4268-b359-b40e3079d384 | 9cef40ec-29c6-4dfa-aee8-47ee1f49223d | ephemeral | b | | + | 1.1.1.1 | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | + `), }, } for _, tt := range tests { @@ -90,41 +92,86 @@ IP PROJECT ID } func Test_IPCmd_Describe(t *testing.T) { - ip1 := ip1() - tests := []*e2e.Test[apiv2.IPServiceGetResponse, *apiv2.IP]{ { - Name: "describe", + Name: "describe", + CmdArgs: []string{"ip", "describe", "--project", ip1().Project, ip1().Ip}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse]{ WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1.Ip, - Project: ip1.Project, + Ip: ip1().Ip, + Project: ip1().Project, }, WantResponse: apiv2.IPServiceGetResponse{ - Ip: ip1, + Ip: ip1(), }, }), - CmdArgs: []string{"ip", "describe", "--project", ip1.Project, ip1.Ip}, - WantObject: ip1, - WantProtoObject: ip1, + WantObject: ip1(), + WantProtoObject: ip1(), WantTable: new(` -IP PROJECT ID TYPE NAME ATTACHED SERVICE -1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a -`), + IP PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + `), WantWideTable: new(` -IP PROJECT ID TYPE NAME DESCRIPTION LABELS -1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx -`), + IP PROJECT ID TYPE NAME DESCRIPTION LABELS + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a a description cluster.metal-stack.io/id/namespace/service=/default/ingress-nginx + `), Template: new("{{ .ip }} {{ .project }}"), WantTemplate: new(` -1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 `), WantMarkdown: new(` -| IP | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | -|---------|--------------------------------------|--------------------------------------|--------|------|------------------| -| 1.1.1.1 | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | -`), + | IP | PROJECT | ID | TYPE | NAME | ATTACHED SERVICE | + |---------|--------------------------------------|--------------------------------------|--------|------|------------------| + | 1.1.1.1 | ce19a655-7933-4745-8f3e-9592b4a90488 | 2e0144a2-09ef-42b7-b629-4263295db6e8 | static | a | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_IPCmd_Create(t *testing.T) { + tests := []*e2e.Test[apiv2.IPServiceGetResponse, *apiv2.IP]{ + { + Name: "create", + CmdArgs: []string{"ip", "create", "--project", ip1().Project, "--network", ip1().Network, "--static=true"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.IPServiceCreateRequest, apiv2.IPServiceCreateResponse]{ + WantRequest: apiv2.IPServiceCreateRequest{ + Project: ip1().Project, + Network: ip1().Network, + Type: &ip1().Type, + }, + WantResponse: apiv2.IPServiceCreateResponse{ + Ip: ip1(), + }, + }), + WantObject: ip1(), }, + // FIXME: + // { + // Name: "create from file", + // CmdArgs: append([]string{"ip", "create"}, e2e.AppendFromFileCommonArgs()...), + // NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.IPServiceCreateRequest, apiv2.IPServiceCreateResponse]{ + // WantRequest: apiv2.IPServiceCreateRequest{ + // Ip: &ip1().Ip, + // Project: ip1().Project, + // Network: ip1().Network, + // Name: &ip1().Name, + // Description: &ip1().Description, + // Labels: ip1().Meta.Labels, + // Type: &ip1().Type, + // AddressFamily: nil, + // }, + // WantResponse: apiv2.IPServiceCreateResponse{ + // Ip: ip1(), + // }, + // FsMocks: func(fs *afero.Afero) { + // require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + // }, + // }), + // WantObject: ip1(), + // }, } for _, tt := range tests { tt.TestCmd(t) diff --git a/pkg/tests/e2e/api/project_test.go b/pkg/tests/e2e/api/project_test.go index 043c0fb..d7f309f 100644 --- a/pkg/tests/e2e/api/project_test.go +++ b/pkg/tests/e2e/api/project_test.go @@ -39,7 +39,8 @@ func Test_ProjectCmd_Describe(t *testing.T) { tests := []*e2e.Test[apiv2.ProjectServiceGetResponse, *apiv2.Project]{ { - Name: "describe", + Name: "describe", + CmdArgs: []string{"project", "describe", p1.Uuid}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ProjectServiceGetRequest, apiv2.ProjectServiceGetResponse]{ WantRequest: apiv2.ProjectServiceGetRequest{ Project: p1.Uuid, @@ -48,26 +49,25 @@ func Test_ProjectCmd_Describe(t *testing.T) { Project: p1, }, }), - CmdArgs: []string{"project", "describe", p1.Uuid}, WantObject: p1, WantProtoObject: p1, WantTable: new(` -ID TENANT NAME DESCRIPTION CREATION DATE -0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC -`), + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + `), WantWideTable: new(` -ID TENANT NAME DESCRIPTION CREATION DATE -0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC -`), + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + `), Template: new("{{ .uuid }} {{ .name }}"), WantTemplate: new(` -0d81bca7-73f6-4da3-8397-4a8c52a0c583 project-a + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 project-a `), WantMarkdown: new(` -| ID | TENANT | NAME | DESCRIPTION | CREATION DATE | -|--------------------------------------|-------------|-----------|---------------|-------------------------| -| 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | -`), + | ID | TENANT | NAME | DESCRIPTION | CREATION DATE | + |--------------------------------------|-------------|-----------|---------------|-------------------------| + | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | + `), }, } for _, tt := range tests { @@ -78,7 +78,8 @@ ID TENANT NAME DESCRIPTION CRE func Test_ProjectCmd_List(t *testing.T) { tests := []*e2e.Test[apiv2.ProjectServiceListResponse, apiv2.Project]{ { - Name: "list", + Name: "list", + CmdArgs: []string{"project", "list"}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ProjectServiceListRequest, apiv2.ProjectServiceListResponse]{ WantRequest: apiv2.ProjectServiceListRequest{}, WantResponse: apiv2.ProjectServiceListResponse{ @@ -88,28 +89,27 @@ func Test_ProjectCmd_List(t *testing.T) { }, }, }), - CmdArgs: []string{"project", "list"}, WantTable: new(` -ID TENANT NAME DESCRIPTION CREATION DATE -0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC -f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC -`), + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + `), WantWideTable: new(` -ID TENANT NAME DESCRIPTION CREATION DATE -0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC -f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC -`), + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + `), Template: new("{{ .uuid }} {{ .name }}"), WantTemplate: new(` 0d81bca7-73f6-4da3-8397-4a8c52a0c583 project-a f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c project-b `), WantMarkdown: new(` -| ID | TENANT | NAME | DESCRIPTION | CREATION DATE | -|--------------------------------------|-------------|-----------|----------------|-------------------------| -| 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | -| f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | metal-stack | project-b | second project | 2025-07-15 14:30:00 UTC | -`), + | ID | TENANT | NAME | DESCRIPTION | CREATION DATE | + |--------------------------------------|-------------|-----------|----------------|-------------------------| + | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | + | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | metal-stack | project-b | second project | 2025-07-15 14:30:00 UTC | + `), }, } for _, tt := range tests { diff --git a/pkg/tests/e2e/api/tenant_test.go b/pkg/tests/e2e/api/tenant_test.go index 0e564b5..9f872e3 100644 --- a/pkg/tests/e2e/api/tenant_test.go +++ b/pkg/tests/e2e/api/tenant_test.go @@ -17,7 +17,7 @@ var ( Email: "info@metal-stack.io", Description: "a tenant", Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), }, } } @@ -28,7 +28,8 @@ func Test_TenantCmd_Describe(t *testing.T) { tests := []*e2e.Test[apiv2.TenantServiceGetResponse, *apiv2.Tenant]{ { - Name: "describe", + Name: "describe", + CmdArgs: []string{"tenant", "describe", tn.Login}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.TenantServiceGetRequest, apiv2.TenantServiceGetResponse]{ WantRequest: apiv2.TenantServiceGetRequest{ Login: tn.Login, @@ -37,12 +38,24 @@ func Test_TenantCmd_Describe(t *testing.T) { Tenant: tn, }, }), - CmdArgs: []string{"tenant", "describe", tn.Login}, + WantTable: new(` + ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS + metal-stack Metal Stack info@metal-stack.io now - + `), + WantWideTable: new(` + ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS + metal-stack Metal Stack info@metal-stack.io now - + `), + WantMarkdown: new(` + | ID | NAME | EMAIL | REGISTERED | COUPONS | TERMS AND CONDITIONS | + |-------------|-------------|---------------------|------------|---------|----------------------| + | metal-stack | Metal Stack | info@metal-stack.io | now | - | | + `), WantObject: tn, WantProtoObject: tn, Template: new("{{ .login }} {{ .name }}"), WantTemplate: new(` -metal-stack Metal Stack + metal-stack Metal Stack `), }, } diff --git a/pkg/tests/e2e/api/token_test.go b/pkg/tests/e2e/api/token_test.go index 026b453..42f6f3a 100644 --- a/pkg/tests/e2e/api/token_test.go +++ b/pkg/tests/e2e/api/token_test.go @@ -16,11 +16,11 @@ var ( User: "admin@metal-stack.io", Description: "ci token", TokenType: apiv2.TokenType_TOKEN_TYPE_API, - Expires: timestamppb.New(time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC)), - IssuedAt: timestamppb.New(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + Expires: timestamppb.New(time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC)), + IssuedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), Permissions: nil, Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), }, } } @@ -31,7 +31,8 @@ func Test_TokenCmd_Describe(t *testing.T) { tests := []*e2e.Test[apiv2.TokenServiceGetResponse, *apiv2.Token]{ { - Name: "describe", + Name: "describe", + CmdArgs: []string{"token", "describe", tk.Uuid}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse]{ WantRequest: apiv2.TokenServiceGetRequest{ Uuid: tk.Uuid, @@ -40,12 +41,24 @@ func Test_TokenCmd_Describe(t *testing.T) { Token: tk, }, }), - CmdArgs: []string{"token", "describe", tk.Uuid}, + WantTable: new(` + TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES + TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) + `), + WantWideTable: new(` + TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES + TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) + `), + WantMarkdown: new(` + | TYPE | ID | ADMIN | USER | DESCRIPTION | ROLES | PERMS | EXPIRES | + |----------------|--------------------------------------|-------|----------------------|-------------|-------|-------|---------------------------------| + | TOKEN_TYPE_API | a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 | | admin@metal-stack.io | ci token | 0 | 0 | 2000-01-02 00:00:00 UTC (in 1d) | + `), WantObject: tk, WantProtoObject: tk, Template: new("{{ .uuid }} {{ .description }}"), WantTemplate: new(` -a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token + a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token `), }, } diff --git a/pkg/tests/e2e/test_client.go b/pkg/tests/e2e/test_client.go index 670bc3f..2af881b 100644 --- a/pkg/tests/e2e/test_client.go +++ b/pkg/tests/e2e/test_client.go @@ -21,7 +21,7 @@ type TestClientConfig[Request, Response any] struct { WantRequest Request // for client expectation WantResponse Response // for client return - FsMocks func(fs afero.Fs) + FsMocks func(fs *afero.Afero) MockStdin *bytes.Buffer } @@ -45,7 +45,9 @@ func NewRootCmd[Request, Response any](t *testing.T, c *TestClientConfig[Request fs := afero.NewMemMapFs() if c.FsMocks != nil { - c.FsMocks(fs) + c.FsMocks(&afero.Afero{ + Fs: fs, + }) } var in io.Reader diff --git a/pkg/tests/e2e/test_framework.go b/pkg/tests/e2e/test_framework.go index 765efda..8ce4717 100644 --- a/pkg/tests/e2e/test_framework.go +++ b/pkg/tests/e2e/test_framework.go @@ -7,6 +7,7 @@ import ( "os" "strings" "testing" + "testing/synctest" "slices" @@ -15,6 +16,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/metal-stack/cli/cmd" "github.com/metal-stack/cli/cmd/config" + "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -23,9 +25,14 @@ import ( "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/runtime/protoimpl" + "google.golang.org/protobuf/testing/protocmp" "sigs.k8s.io/yaml" ) +const ( + InputFilePath = "/file.yaml" +) + // NewRootCmdFunc returns the root command for the cli and an output buffer which returns the output after command execution type NewRootCmdFunc func() (rootCmd *cobra.Command, out *bytes.Buffer) @@ -36,9 +43,12 @@ type Test[Response, Object any] struct { CmdArgs []string Out *bytes.Buffer + AssertExhaustiveArgs bool + AssertExhaustiveExcludes []string + // output format tests - WantObject Object // object for rawyaml/rawjson structural comparison - WantProtoObject proto.Message // object for yaml/json structural comparison + WantObject Object // for rawyaml / rawjson printer + WantProtoObject proto.Message // for yaml / json printer WantTable *string // for table printer WantWideTable *string // for wide table printer WantMarkdown *string // for markdown printer @@ -57,10 +67,16 @@ func (c *Test[Response, Object]) TestCmd(t *testing.T) { if c.WantErr != nil { os.Args = append([]string{rootCmd.Use}, c.CmdArgs...) - err := rootCmd.Execute() - if diff := cmp.Diff(c.WantErr, err, testcommon.IgnoreUnexported(), testcommon.ErrorStringComparer()); diff != "" { - t.Errorf("error diff (+got -want):\n %s", diff) + if c.AssertExhaustiveArgs { + assertExhaustiveArgs(t, os.Args, c.AssertExhaustiveExcludes...) } + + synctest.Test(t, func(t *testing.T) { + err := rootCmd.Execute() + if diff := cmp.Diff(c.WantErr, err, testcommon.IgnoreUnexported(), testcommon.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n %s", diff) + } + }) } formats := outputFormats(c) @@ -77,8 +93,14 @@ func (c *Test[Response, Object]) TestCmd(t *testing.T) { os.Args = append([]string{rootCmd.Use}, c.CmdArgs...) os.Args = append(os.Args, format.Args()...) - err := rootCmd.Execute() - require.NoError(t, err) + if c.AssertExhaustiveArgs { + assertExhaustiveArgs(t, os.Args, c.AssertExhaustiveExcludes...) + } + + synctest.Test(t, func(t *testing.T) { + err := rootCmd.Execute() + require.NoError(t, err) + }) format.Validate(t, out.Bytes()) }) @@ -89,7 +111,7 @@ func (c *Test[Response, Object]) TestCmd(t *testing.T) { } } -func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { +func assertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { assertContainsPrefix := func(ss []string, prefix string) error { for _, s := range ss { if strings.HasPrefix(s, prefix) { @@ -114,12 +136,17 @@ func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { func outputFormats[Response, Object any](c *Test[Response, Object]) []outputFormat { var formats []outputFormat + if !pointer.IsZero(c.WantObject) { + formats = append(formats, + &rawYamlOutputFormat[Object]{want: c.WantObject}, + &rawJsonOutputFormat[Object]{want: c.WantObject}, + ) + } + if c.WantProtoObject != nil { formats = append(formats, &protoYAMLOutputFormat[Response]{want: c.WantProtoObject}, - &rawYamlOutputFormat[Object]{want: c.WantObject}, &protoJSONOutputFormat[Response]{want: c.WantProtoObject}, - &rawJsonOutputFormat[Object]{want: c.WantObject}, ) } @@ -206,7 +233,7 @@ func (o *protoYAMLOutputFormat[R]) Validate(t *testing.T, output []byte) { err := protoyaml.Unmarshal(output, got) require.NoError(t, err) - if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + if diff := cmp.Diff(o.want, got, protocmp.Transform(), testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { t.Errorf("diff (+got -want):\n %s", diff) } } @@ -228,7 +255,7 @@ func (o *protoJSONOutputFormat[R]) Validate(t *testing.T, output []byte) { err := protojson.Unmarshal(output, got) require.NoError(t, err) - if diff := cmp.Diff(o.want, got, testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + if diff := cmp.Diff(o.want, got, protocmp.Transform(), testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { t.Errorf("diff (+got -want):\n %s", diff) } } @@ -321,3 +348,25 @@ func validateTableRows(t *testing.T, want, got string) { } } } + +func CommonExcludedFileArgs() []string { + return []string{"file", "bulk-output", "skip-security-prompts", "timestamps"} +} + +func AppendFromFileCommonArgs(args ...string) []string { + return append(args, []string{"-f", InputFilePath, "--skip-security-prompts", "--bulk-output"}...) +} + +func MustMarshal(t *testing.T, d any) []byte { + b, err := json.MarshalIndent(d, "", " ") + require.NoError(t, err) + return b +} + +func MustMarshalToMultiYAML[R any](t *testing.T, data []R) []byte { + var parts []string + for _, elem := range data { + parts = append(parts, string(MustMarshal(t, elem))) + } + return []byte(strings.Join(parts, "\n---\n")) +} diff --git a/pkg/tests/e2e/test_interceptor.go b/pkg/tests/e2e/test_interceptor.go index 9cc4da1..098f8ef 100644 --- a/pkg/tests/e2e/test_interceptor.go +++ b/pkg/tests/e2e/test_interceptor.go @@ -9,6 +9,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/metal-stack/metal-lib/pkg/testcommon" "google.golang.org/protobuf/runtime/protoimpl" + "google.golang.org/protobuf/testing/protocmp" ) type testClientInterceptor[Request, Response any] struct { @@ -19,7 +20,7 @@ type testClientInterceptor[Request, Response any] struct { func (t *testClientInterceptor[Request, Response]) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, ar connect.AnyRequest) (connect.AnyResponse, error) { - if diff := cmp.Diff(&t.request, ar.Any(), testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + if diff := cmp.Diff(&t.request, ar.Any(), protocmp.Transform(), testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { t.t.Errorf("request diff (+got -want):\n %s", diff) t.t.FailNow() } From ef7eb2bd00603dd22e2132be09ee7ece26dc00a6 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 10:53:12 +0100 Subject: [PATCH 08/34] Make exhaustive work again. --- pkg/tests/e2e/api/image_test.go | 2 +- pkg/tests/e2e/test_framework.go | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/tests/e2e/api/image_test.go b/pkg/tests/e2e/api/image_test.go index 6d33f94..9ed01ec 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -75,7 +75,7 @@ firewall-3.0 Firewall 3.0 func Test_ImageCmd_Describe(t *testing.T) { tests := []*e2e.Test[apiv2.ImageServiceGetResponse, *apiv2.Image]{ { - Name: "list", + Name: "describe", CmdArgs: []string{"image", "describe", image1().Id}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ImageServiceGetRequest, apiv2.ImageServiceGetResponse]{ WantRequest: apiv2.ImageServiceGetRequest{ diff --git a/pkg/tests/e2e/test_framework.go b/pkg/tests/e2e/test_framework.go index 8ce4717..c851878 100644 --- a/pkg/tests/e2e/test_framework.go +++ b/pkg/tests/e2e/test_framework.go @@ -14,8 +14,6 @@ import ( "buf.build/go/protoyaml" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/metal-stack/cli/cmd" - "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/spf13/cobra" @@ -68,7 +66,7 @@ func (c *Test[Response, Object]) TestCmd(t *testing.T) { os.Args = append([]string{rootCmd.Use}, c.CmdArgs...) if c.AssertExhaustiveArgs { - assertExhaustiveArgs(t, os.Args, c.AssertExhaustiveExcludes...) + c.assertExhaustiveArgs(t) } synctest.Test(t, func(t *testing.T) { @@ -94,7 +92,7 @@ func (c *Test[Response, Object]) TestCmd(t *testing.T) { os.Args = append(os.Args, format.Args()...) if c.AssertExhaustiveArgs { - assertExhaustiveArgs(t, os.Args, c.AssertExhaustiveExcludes...) + c.assertExhaustiveArgs(t) } synctest.Test(t, func(t *testing.T) { @@ -111,7 +109,7 @@ func (c *Test[Response, Object]) TestCmd(t *testing.T) { } } -func assertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { +func (c *Test[Response, Object]) assertExhaustiveArgs(t *testing.T) { assertContainsPrefix := func(ss []string, prefix string) error { for _, s := range ss { if strings.HasPrefix(s, prefix) { @@ -121,15 +119,17 @@ func assertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { return fmt.Errorf("not exhaustive: does not contain %q", prefix) } - root := cmd.NewRootCmd(&config.Config{}) - cmd, args, err := root.Find(args) + rootCmd, _ := c.NewRootCmd() + + cmd, args, err := rootCmd.Find(c.CmdArgs) require.NoError(t, err) cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { - if slices.Contains(exclude, f.Name) { + if slices.Contains(c.AssertExhaustiveExcludes, f.Name) { return } - require.NoError(t, assertContainsPrefix(args, "--"+f.Name), "please ensure you all available args are used in order to increase coverage or exclude them explicitly") + + require.NoError(t, assertContainsPrefix(args, "--"+f.Name), "ensure all available args are used in order to increase coverage") }) } From f92b62dfcf621a8f8ff447d032157041e3d4d824 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 11:23:40 +0100 Subject: [PATCH 09/34] Test for interceptor. --- pkg/tests/e2e/test_client.go | 14 ++++---- pkg/tests/e2e/test_interceptor_test.go | 49 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 pkg/tests/e2e/test_interceptor_test.go diff --git a/pkg/tests/e2e/test_client.go b/pkg/tests/e2e/test_client.go index 2af881b..6042d04 100644 --- a/pkg/tests/e2e/test_client.go +++ b/pkg/tests/e2e/test_client.go @@ -59,12 +59,14 @@ func NewRootCmd[Request, Response any](t *testing.T, c *TestClientConfig[Request var out bytes.Buffer return cmd.NewRootCmd(&config.Config{ - Fs: fs, - Out: &out, - In: in, - PromptOut: io.Discard, - Completion: &completion.Completion{}, - Client: cl, + Fs: fs, + Out: &out, + In: in, + PromptOut: io.Discard, + Completion: &completion.Completion{ + Client: cl, + }, + Client: cl, }), &out } } diff --git a/pkg/tests/e2e/test_interceptor_test.go b/pkg/tests/e2e/test_interceptor_test.go new file mode 100644 index 0000000..991d480 --- /dev/null +++ b/pkg/tests/e2e/test_interceptor_test.go @@ -0,0 +1,49 @@ +package e2e + +import ( + "log/slog" + "testing" + + "connectrpc.com/connect" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + client "github.com/metal-stack/api/go/client" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/testcommon" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/runtime/protoimpl" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestInterceptor(t *testing.T) { + cl, err := client.New(&client.DialConfig{ + BaseURL: "http://this-is-just-for-testing", + Interceptors: []connect.Interceptor{ + &testClientInterceptor[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse]{ + t: t, + response: apiv2.IPServiceGetResponse{ + Ip: &apiv2.IP{Ip: "1.2.3.4"}, + }, + request: apiv2.IPServiceGetRequest{ + Ip: "1.2.3.4", + }, + }, + }, + UserAgent: "cli-test", + Log: slog.Default(), + }) + require.NoError(t, err) + + resp, err := cl.Apiv2().IP().Get(t.Context(), &apiv2.IPServiceGetRequest{ + Ip: "1.2.3.4", + }) + require.NoError(t, err) + + if diff := cmp.Diff(&apiv2.IPServiceGetResponse{ + Ip: &apiv2.IP{ + Ip: "1.2.3.4", + }, + }, resp, protocmp.Transform(), testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + t.Errorf("diff = %s", diff) + } +} From 85c3b2413b1dca5b7bf3448b1235393028e96c00 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 12:21:43 +0100 Subject: [PATCH 10/34] Support multiple client calls. --- pkg/tests/e2e/api/image_test.go | 31 ++++--- pkg/tests/e2e/api/ip_test.go | 118 +++++++++++++++---------- pkg/tests/e2e/api/project_test.go | 33 +++---- pkg/tests/e2e/api/tenant_test.go | 19 ++-- pkg/tests/e2e/api/token_test.go | 80 ++++++++--------- pkg/tests/e2e/test_client.go | 59 ++++++------- pkg/tests/e2e/test_framework.go | 16 ++-- pkg/tests/e2e/test_interceptor.go | 32 +++++-- pkg/tests/e2e/test_interceptor_test.go | 18 ++-- 9 files changed, 228 insertions(+), 178 deletions(-) diff --git a/pkg/tests/e2e/api/image_test.go b/pkg/tests/e2e/api/image_test.go index 9ed01ec..dc45ea2 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -3,6 +3,7 @@ package api_e2e import ( "testing" + "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" ) @@ -33,17 +34,21 @@ func Test_ImageCmd_List(t *testing.T) { { Name: "list", CmdArgs: []string{"image", "list"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ImageServiceListRequest, apiv2.ImageServiceListResponse]{ - WantRequest: apiv2.ImageServiceListRequest{ - Query: &apiv2.ImageQuery{}, - }, - WantResponse: apiv2.ImageServiceListResponse{ - Images: []*apiv2.Image{ - image1(), - image2(), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, + e2e.ClientCall{ + WantRequest: apiv2.ImageServiceListRequest{ + Query: &apiv2.ImageQuery{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ImageServiceListResponse{ + Images: []*apiv2.Image{ + image1(), + image2(), + }, + }) }, }, - }), + ), WantTable: new(` ID NAME DESCRIPTION FEATURES EXPIRATION STATUS ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported @@ -77,12 +82,14 @@ func Test_ImageCmd_Describe(t *testing.T) { { Name: "describe", CmdArgs: []string{"image", "describe", image1().Id}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ImageServiceGetRequest, apiv2.ImageServiceGetResponse]{ + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ WantRequest: apiv2.ImageServiceGetRequest{ Id: image1().Id, }, - WantResponse: apiv2.ImageServiceGetResponse{ - Image: image1(), + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ImageServiceGetResponse{ + Image: image1(), + }) }, }), WantTable: new(` diff --git a/pkg/tests/e2e/api/ip_test.go b/pkg/tests/e2e/api/ip_test.go index 7874cad..52b2ef0 100644 --- a/pkg/tests/e2e/api/ip_test.go +++ b/pkg/tests/e2e/api/ip_test.go @@ -3,9 +3,12 @@ package api_e2e import ( "testing" + "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" "github.com/metal-stack/metal-lib/pkg/tag" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" ) var ( @@ -52,17 +55,21 @@ func Test_IPCmd_List(t *testing.T) { { Name: "list", CmdArgs: []string{"ip", "list", "--project", ip1().Project}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.IPServiceListRequest, apiv2.IPServiceListResponse]{ - WantRequest: apiv2.IPServiceListRequest{ - Project: ip1().Project, - }, - WantResponse: apiv2.IPServiceListResponse{ - Ips: []*apiv2.IP{ - ip1(), - ip2(), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceListRequest{ + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceListResponse{ + Ips: []*apiv2.IP{ + ip1(), + ip2(), + }, + }) }, }, - }), + ), WantTable: new(` IP PROJECT ID TYPE NAME ATTACHED SERVICE 4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b @@ -96,15 +103,18 @@ func Test_IPCmd_Describe(t *testing.T) { { Name: "describe", CmdArgs: []string{"ip", "describe", "--project", ip1().Project, ip1().Ip}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse]{ - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: apiv2.IPServiceGetResponse{ - Ip: ip1(), - }, - }), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }), WantObject: ip1(), WantProtoObject: ip1(), WantTable: new(` @@ -136,42 +146,60 @@ func Test_IPCmd_Create(t *testing.T) { { Name: "create", CmdArgs: []string{"ip", "create", "--project", ip1().Project, "--network", ip1().Network, "--static=true"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.IPServiceCreateRequest, apiv2.IPServiceCreateResponse]{ + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ WantRequest: apiv2.IPServiceCreateRequest{ Project: ip1().Project, Network: ip1().Network, Type: &ip1().Type, }, - WantResponse: apiv2.IPServiceCreateResponse{ - Ip: ip1(), + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceCreateResponse{ + Ip: ip1(), + }) }, }), WantObject: ip1(), }, - // FIXME: - // { - // Name: "create from file", - // CmdArgs: append([]string{"ip", "create"}, e2e.AppendFromFileCommonArgs()...), - // NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.IPServiceCreateRequest, apiv2.IPServiceCreateResponse]{ - // WantRequest: apiv2.IPServiceCreateRequest{ - // Ip: &ip1().Ip, - // Project: ip1().Project, - // Network: ip1().Network, - // Name: &ip1().Name, - // Description: &ip1().Description, - // Labels: ip1().Meta.Labels, - // Type: &ip1().Type, - // AddressFamily: nil, - // }, - // WantResponse: apiv2.IPServiceCreateResponse{ - // Ip: ip1(), - // }, - // FsMocks: func(fs *afero.Afero) { - // require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) - // }, - // }), - // WantObject: ip1(), - // }, + { + Name: "create from file", + CmdArgs: append([]string{"ip", "create"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + }, + }, e2e.ClientCall{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, e2e.ClientCall{ + WantRequest: apiv2.IPServiceCreateRequest{ + Ip: &ip1().Ip, + Project: ip1().Project, + Network: ip1().Network, + Name: &ip1().Name, + Description: &ip1().Description, + Labels: ip1().Meta.Labels, + Type: &ip1().Type, + AddressFamily: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceCreateResponse{ + Ip: ip1(), + }) + }, + }, + ), + WantTable: new(` + IP PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + `), + }, } for _, tt := range tests { tt.TestCmd(t) diff --git a/pkg/tests/e2e/api/project_test.go b/pkg/tests/e2e/api/project_test.go index d7f309f..dc68ae1 100644 --- a/pkg/tests/e2e/api/project_test.go +++ b/pkg/tests/e2e/api/project_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" "google.golang.org/protobuf/types/known/timestamppb" @@ -35,22 +36,22 @@ var ( ) func Test_ProjectCmd_Describe(t *testing.T) { - p1 := project1() - tests := []*e2e.Test[apiv2.ProjectServiceGetResponse, *apiv2.Project]{ { Name: "describe", - CmdArgs: []string{"project", "describe", p1.Uuid}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ProjectServiceGetRequest, apiv2.ProjectServiceGetResponse]{ + CmdArgs: []string{"project", "describe", project1().Uuid}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ WantRequest: apiv2.ProjectServiceGetRequest{ - Project: p1.Uuid, + Project: project1().Uuid, }, - WantResponse: apiv2.ProjectServiceGetResponse{ - Project: p1, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceGetResponse{ + Project: project1(), + }) }, }), - WantObject: p1, - WantProtoObject: p1, + WantObject: project1(), + WantProtoObject: project1(), WantTable: new(` ID TENANT NAME DESCRIPTION CREATION DATE 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC @@ -80,13 +81,15 @@ func Test_ProjectCmd_List(t *testing.T) { { Name: "list", CmdArgs: []string{"project", "list"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.ProjectServiceListRequest, apiv2.ProjectServiceListResponse]{ + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ WantRequest: apiv2.ProjectServiceListRequest{}, - WantResponse: apiv2.ProjectServiceListResponse{ - Projects: []*apiv2.Project{ - project1(), - project2(), - }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceListResponse{ + Projects: []*apiv2.Project{ + project1(), + project2(), + }, + }) }, }), WantTable: new(` diff --git a/pkg/tests/e2e/api/tenant_test.go b/pkg/tests/e2e/api/tenant_test.go index 9f872e3..b64c555 100644 --- a/pkg/tests/e2e/api/tenant_test.go +++ b/pkg/tests/e2e/api/tenant_test.go @@ -4,6 +4,7 @@ import ( "testing" "time" + "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" "google.golang.org/protobuf/types/known/timestamppb" @@ -24,18 +25,18 @@ var ( ) func Test_TenantCmd_Describe(t *testing.T) { - tn := tenant1() - tests := []*e2e.Test[apiv2.TenantServiceGetResponse, *apiv2.Tenant]{ { Name: "describe", - CmdArgs: []string{"tenant", "describe", tn.Login}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.TenantServiceGetRequest, apiv2.TenantServiceGetResponse]{ + CmdArgs: []string{"tenant", "describe", tenant1().Login}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ WantRequest: apiv2.TenantServiceGetRequest{ - Login: tn.Login, + Login: tenant1().Login, }, - WantResponse: apiv2.TenantServiceGetResponse{ - Tenant: tn, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.TenantServiceGetResponse{ + Tenant: tenant1(), + }) }, }), WantTable: new(` @@ -51,8 +52,8 @@ func Test_TenantCmd_Describe(t *testing.T) { |-------------|-------------|---------------------|------------|---------|----------------------| | metal-stack | Metal Stack | info@metal-stack.io | now | - | | `), - WantObject: tn, - WantProtoObject: tn, + WantObject: tenant1(), + WantProtoObject: tenant1(), Template: new("{{ .login }} {{ .name }}"), WantTemplate: new(` metal-stack Metal Stack diff --git a/pkg/tests/e2e/api/token_test.go b/pkg/tests/e2e/api/token_test.go index 42f6f3a..bb7b0bf 100644 --- a/pkg/tests/e2e/api/token_test.go +++ b/pkg/tests/e2e/api/token_test.go @@ -1,11 +1,9 @@ package api_e2e import ( - "testing" "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -26,43 +24,43 @@ var ( } ) -func Test_TokenCmd_Describe(t *testing.T) { - tk := token1() +// func Test_TokenCmd_Describe(t *testing.T) { +// tk := token1() - tests := []*e2e.Test[apiv2.TokenServiceGetResponse, *apiv2.Token]{ - { - Name: "describe", - CmdArgs: []string{"token", "describe", tk.Uuid}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse]{ - WantRequest: apiv2.TokenServiceGetRequest{ - Uuid: tk.Uuid, - }, - WantResponse: apiv2.TokenServiceGetResponse{ - Token: tk, - }, - }), - WantTable: new(` - TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES - TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) - `), - WantWideTable: new(` - TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES - TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) - `), - WantMarkdown: new(` - | TYPE | ID | ADMIN | USER | DESCRIPTION | ROLES | PERMS | EXPIRES | - |----------------|--------------------------------------|-------|----------------------|-------------|-------|-------|---------------------------------| - | TOKEN_TYPE_API | a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 | | admin@metal-stack.io | ci token | 0 | 0 | 2000-01-02 00:00:00 UTC (in 1d) | - `), - WantObject: tk, - WantProtoObject: tk, - Template: new("{{ .uuid }} {{ .description }}"), - WantTemplate: new(` - a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token - `), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} +// tests := []*e2e.Test[apiv2.TokenServiceGetResponse, *apiv2.Token]{ +// { +// Name: "describe", +// CmdArgs: []string{"token", "describe", tk.Uuid}, +// NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse]{ +// WantRequest: apiv2.TokenServiceGetRequest{ +// Uuid: tk.Uuid, +// }, +// WantResponse: apiv2.TokenServiceGetResponse{ +// Token: tk, +// }, +// }), +// WantTable: new(` +// TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES +// TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) +// `), +// WantWideTable: new(` +// TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES +// TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) +// `), +// WantMarkdown: new(` +// | TYPE | ID | ADMIN | USER | DESCRIPTION | ROLES | PERMS | EXPIRES | +// |----------------|--------------------------------------|-------|----------------------|-------------|-------|-------|---------------------------------| +// | TOKEN_TYPE_API | a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 | | admin@metal-stack.io | ci token | 0 | 0 | 2000-01-02 00:00:00 UTC (in 1d) | +// `), +// WantObject: tk, +// WantProtoObject: tk, +// Template: new("{{ .uuid }} {{ .description }}"), +// WantTemplate: new(` +// a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token +// `), +// }, +// } +// for _, tt := range tests { +// tt.TestCmd(t) +// } +// } diff --git a/pkg/tests/e2e/test_client.go b/pkg/tests/e2e/test_client.go index 6042d04..b03e6b2 100644 --- a/pkg/tests/e2e/test_client.go +++ b/pkg/tests/e2e/test_client.go @@ -17,45 +17,42 @@ import ( "github.com/stretchr/testify/require" ) -type TestClientConfig[Request, Response any] struct { - WantRequest Request // for client expectation - WantResponse Response // for client return - +type TestClientConfig struct { FsMocks func(fs *afero.Afero) MockStdin *bytes.Buffer } -func NewRootCmd[Request, Response any](t *testing.T, c *TestClientConfig[Request, Response]) NewRootCmdFunc { - interceptors := []connect.Interceptor{ - validate.NewInterceptor(), - &testClientInterceptor[Request, Response]{ - t: t, - response: c.WantResponse, - request: c.WantRequest, - }, - } - - cl, err := client.New(&client.DialConfig{ - BaseURL: "http://this-is-just-for-testing", - Interceptors: interceptors, - UserAgent: "cli-test", - Log: slog.Default(), - }) - require.NoError(t, err) +func NewRootCmd(t *testing.T, c *TestClientConfig, calls ...ClientCall) NewRootCmdFunc { + return func() (*cobra.Command, *bytes.Buffer) { + interceptors := []connect.Interceptor{ + &testClientInterceptor{ + t: t, + calls: calls, + count: 0, + }, + validate.NewInterceptor(), + } - fs := afero.NewMemMapFs() - if c.FsMocks != nil { - c.FsMocks(&afero.Afero{ - Fs: fs, + cl, err := client.New(&client.DialConfig{ + BaseURL: "http://this-is-just-for-testing", + Interceptors: interceptors, + UserAgent: "cli-test", + Log: slog.Default(), }) - } + require.NoError(t, err) - var in io.Reader - if c.MockStdin != nil { - in = bytes.NewReader(c.MockStdin.Bytes()) - } + fs := afero.NewMemMapFs() + if c.FsMocks != nil { + c.FsMocks(&afero.Afero{ + Fs: fs, + }) + } + + var in io.Reader + if c.MockStdin != nil { + in = bytes.NewReader(c.MockStdin.Bytes()) + } - return func() (*cobra.Command, *bytes.Buffer) { var out bytes.Buffer return cmd.NewRootCmd(&config.Config{ diff --git a/pkg/tests/e2e/test_framework.go b/pkg/tests/e2e/test_framework.go index c851878..6e456a4 100644 --- a/pkg/tests/e2e/test_framework.go +++ b/pkg/tests/e2e/test_framework.go @@ -60,14 +60,14 @@ func (c *Test[Response, Object]) TestCmd(t *testing.T) { require.NotEmpty(t, c.Name, "test name must not be empty") require.NotEmpty(t, c.CmdArgs, "cmd must not be empty") - rootCmd, out := c.NewRootCmd() + if c.AssertExhaustiveArgs { + c.assertExhaustiveArgs(t) + } if c.WantErr != nil { - os.Args = append([]string{rootCmd.Use}, c.CmdArgs...) + rootCmd, _ := c.NewRootCmd() - if c.AssertExhaustiveArgs { - c.assertExhaustiveArgs(t) - } + os.Args = append([]string{rootCmd.Use}, c.CmdArgs...) synctest.Test(t, func(t *testing.T) { err := rootCmd.Execute() @@ -86,15 +86,11 @@ func (c *Test[Response, Object]) TestCmd(t *testing.T) { for _, format := range formats { succeeded := t.Run(fmt.Sprintf("%v", format.Args()), func(t *testing.T) { - out.Reset() + rootCmd, out := c.NewRootCmd() os.Args = append([]string{rootCmd.Use}, c.CmdArgs...) os.Args = append(os.Args, format.Args()...) - if c.AssertExhaustiveArgs { - c.assertExhaustiveArgs(t) - } - synctest.Test(t, func(t *testing.T) { err := rootCmd.Execute() require.NoError(t, err) diff --git a/pkg/tests/e2e/test_interceptor.go b/pkg/tests/e2e/test_interceptor.go index 098f8ef..fdf0b50 100644 --- a/pkg/tests/e2e/test_interceptor.go +++ b/pkg/tests/e2e/test_interceptor.go @@ -12,29 +12,43 @@ import ( "google.golang.org/protobuf/testing/protocmp" ) -type testClientInterceptor[Request, Response any] struct { - t *testing.T - request Request - response Response +type testClientInterceptor struct { + t *testing.T + calls []ClientCall + count int } -func (t *testClientInterceptor[Request, Response]) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { +type ClientCall struct { + WantRequest any + WantResponse func() connect.AnyResponse +} + +func (t *testClientInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, ar connect.AnyRequest) (connect.AnyResponse, error) { - if diff := cmp.Diff(&t.request, ar.Any(), protocmp.Transform(), testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { + defer func() { t.count++ }() + + if t.count >= len(t.calls) { + t.t.Errorf("received an unexpected client call: %v", ar.Any()) + t.t.FailNow() + } + + call := t.calls[t.count] + + if diff := cmp.Diff(call.WantRequest, ar.Any(), protocmp.Transform(), testcommon.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); diff != "" { t.t.Errorf("request diff (+got -want):\n %s", diff) t.t.FailNow() } - return connect.NewResponse(&t.response), nil + return call.WantResponse(), nil } } -func (t *testClientInterceptor[Request, Response]) WrapStreamingClient(connect.StreamingClientFunc) connect.StreamingClientFunc { +func (t *testClientInterceptor) WrapStreamingClient(connect.StreamingClientFunc) connect.StreamingClientFunc { t.t.Errorf("streaming not supported") return nil } -func (t *testClientInterceptor[Request, Response]) WrapStreamingHandler(connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { +func (t *testClientInterceptor) WrapStreamingHandler(connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { t.t.Errorf("streaming not supported") return nil } diff --git a/pkg/tests/e2e/test_interceptor_test.go b/pkg/tests/e2e/test_interceptor_test.go index 991d480..3305c01 100644 --- a/pkg/tests/e2e/test_interceptor_test.go +++ b/pkg/tests/e2e/test_interceptor_test.go @@ -19,13 +19,19 @@ func TestInterceptor(t *testing.T) { cl, err := client.New(&client.DialConfig{ BaseURL: "http://this-is-just-for-testing", Interceptors: []connect.Interceptor{ - &testClientInterceptor[apiv2.IPServiceGetRequest, apiv2.IPServiceGetResponse]{ + &testClientInterceptor{ t: t, - response: apiv2.IPServiceGetResponse{ - Ip: &apiv2.IP{Ip: "1.2.3.4"}, - }, - request: apiv2.IPServiceGetRequest{ - Ip: "1.2.3.4", + calls: []ClientCall{ + { + WantRequest: &apiv2.IPServiceGetRequest{ + Ip: "1.2.3.4", + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: &apiv2.IP{Ip: "1.2.3.4"}, + }) + }, + }, }, }, }, From 33c0931961fa936b075dac660721fef4e79605df Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 12:57:27 +0100 Subject: [PATCH 11/34] More tests and fixes. --- cmd/api/v2/ip.go | 7 +- pkg/tests/e2e/api/ip_test.go | 330 +++++++++++++++++++++++++++--- pkg/tests/e2e/api/token_test.go | 82 ++++---- pkg/tests/e2e/test_framework.go | 8 +- pkg/tests/e2e/test_interceptor.go | 7 +- 5 files changed, 365 insertions(+), 69 deletions(-) diff --git a/cmd/api/v2/ip.go b/cmd/api/v2/ip.go index dc50684..4cab305 100644 --- a/cmd/api/v2/ip.go +++ b/cmd/api/v2/ip.go @@ -1,8 +1,10 @@ package v2 import ( + "errors" "fmt" + "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/cli/cmd/sorters" @@ -126,6 +128,9 @@ func (c *ip) Create(rq *apiv2.IPServiceCreateRequest) (*apiv2.IP, error) { resp, err := c.c.Client.Apiv2().IP().Create(ctx, rq) if err != nil { + if connectErr, ok := errors.AsType[*connect.Error](err); ok && connectErr.Code() == connect.CodeAlreadyExists { + return nil, genericcli.AlreadyExistsError() + } return nil, err } @@ -200,7 +205,7 @@ func (c *ip) Update(rq *apiv2.IPServiceUpdateRequest) (*apiv2.IP, error) { func (c *ip) Convert(r *apiv2.IP) (string, *apiv2.IPServiceCreateRequest, *apiv2.IPServiceUpdateRequest, error) { responseToUpdate, err := c.IpResponseToUpdate(r) - return helpers.EncodeProject(r.Uuid, r.Project), IpResponseToCreate(r), responseToUpdate, err + return helpers.EncodeProject(r.Ip, r.Project), IpResponseToCreate(r), responseToUpdate, err } func IpResponseToCreate(r *apiv2.IP) *apiv2.IPServiceCreateRequest { diff --git a/pkg/tests/e2e/api/ip_test.go b/pkg/tests/e2e/api/ip_test.go index 52b2ef0..fe38f9e 100644 --- a/pkg/tests/e2e/api/ip_test.go +++ b/pkg/tests/e2e/api/ip_test.go @@ -1,6 +1,7 @@ package api_e2e import ( + "fmt" "testing" "connectrpc.com/connect" @@ -163,41 +164,324 @@ func Test_IPCmd_Create(t *testing.T) { { Name: "create from file", CmdArgs: append([]string{"ip", "create"}, e2e.AppendFromFileCommonArgs()...), - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{ - FsMocks: func(fs *afero.Afero) { - require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + NewRootCmd: e2e.NewRootCmd(t, + &e2e.TestClientConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceCreateRequest{ + Ip: &ip1().Ip, + Project: ip1().Project, + Network: ip1().Network, + Name: &ip1().Name, + Description: &ip1().Description, + Labels: ip1().Meta.Labels, + Type: &ip1().Type, + AddressFamily: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceCreateResponse{ + Ip: ip1(), + }) + }, }, - }, e2e.ClientCall{ - WantRequest: apiv2.IPServiceGetRequest{ + ), + WantTable: new(` + IP PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_IPCmd_Delete(t *testing.T) { + tests := []*e2e.Test[apiv2.IPServiceDeleteResponse, *apiv2.IP]{ + { + Name: "delete", + CmdArgs: []string{"ip", "delete", "--project", ip1().Project, ip1().Ip}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ + WantRequest: apiv2.IPServiceDeleteRequest{ Ip: ip1().Ip, Project: ip1().Project, }, WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ + return connect.NewResponse(&apiv2.IPServiceDeleteResponse{ Ip: ip1(), }) }, - }, e2e.ClientCall{ - WantRequest: apiv2.IPServiceCreateRequest{ - Ip: &ip1().Ip, - Project: ip1().Project, - Network: ip1().Network, - Name: &ip1().Name, - Description: &ip1().Description, - Labels: ip1().Meta.Labels, - Type: &ip1().Type, - AddressFamily: nil, + }), + WantObject: ip1(), + }, + { + Name: "delete from file", + CmdArgs: append([]string{"ip", "delete"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2e.NewRootCmd(t, + &e2e.TestClientConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + }, }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceCreateResponse{ - Ip: ip1(), - }) + e2e.ClientCall{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceDeleteRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceDeleteResponse{ + Ip: ip1(), + }) + }, }, - }, ), WantTable: new(` - IP PROJECT ID TYPE NAME ATTACHED SERVICE - 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + IP PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_IPCmd_Update(t *testing.T) { + tests := []*e2e.Test[apiv2.IPServiceDeleteResponse, *apiv2.IP]{ + { + Name: "update", + CmdArgs: []string{"ip", "update", "--project", ip1().Project, ip1().Ip, "--name", "foo"}, + NewRootCmd: e2e.NewRootCmd(t, + &e2e.TestClientConfig{}, + // TODO: the client gets the IP two times? + e2e.ClientCall{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceUpdateRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + Name: new("foo"), + + // TODO: these fields do not need to be sent? + Description: &ip1().Description, + Labels: &apiv2.UpdateLabels{ + Update: &apiv2.Labels{}, + }, + Type: &ip1().Type, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ + Ip: ip1(), + }) + }, + }, + ), + WantObject: ip1(), + }, + { + Name: "update from file", + CmdArgs: append([]string{"ip", "update"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2e.NewRootCmd(t, + &e2e.TestClientConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceUpdateRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + Description: &ip1().Description, + Labels: &apiv2.UpdateLabels{ + Update: &apiv2.Labels{}, + }, + Name: &ip1().Name, + Type: &ip1().Type, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ + Ip: ip1(), + }) + }, + }, + ), + WantTable: new(` + IP PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_IPCmd_Apply(t *testing.T) { + tests := []*e2e.Test[apiv2.IPServiceDeleteResponse, *apiv2.IP]{ + { + Name: "apply", + CmdArgs: append([]string{"ip", "apply"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2e.NewRootCmd(t, + &e2e.TestClientConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceCreateRequest{ + Ip: &ip1().Ip, + Project: ip1().Project, + Network: ip1().Network, + Name: &ip1().Name, + Description: &ip1().Description, + Labels: ip1().Meta.Labels, + Type: &ip1().Type, + AddressFamily: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceCreateResponse{ + Ip: ip1(), + }) + }, + }, + ), + WantTable: new(` + IP PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a + `), + }, + { + Name: "apply already exists", + CmdArgs: append([]string{"ip", "apply"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2e.NewRootCmd(t, + &e2e.TestClientConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceCreateRequest{ + Ip: &ip1().Ip, + Project: ip1().Project, + Network: ip1().Network, + Name: &ip1().Name, + Description: &ip1().Description, + Labels: ip1().Meta.Labels, + Type: &ip1().Type, + AddressFamily: nil, + }, + WantError: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("already exists")), + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + e2e.ClientCall{ + WantRequest: apiv2.IPServiceUpdateRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + Description: &ip1().Description, + Labels: &apiv2.UpdateLabels{ + Update: &apiv2.Labels{}, + }, + Name: &ip1().Name, + Type: &ip1().Type, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ + Ip: ip1(), + }) + }, + }, + ), + WantTable: new(` + IP PROJECT ID TYPE NAME ATTACHED SERVICE + 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a `), }, } diff --git a/pkg/tests/e2e/api/token_test.go b/pkg/tests/e2e/api/token_test.go index bb7b0bf..a7e34f1 100644 --- a/pkg/tests/e2e/api/token_test.go +++ b/pkg/tests/e2e/api/token_test.go @@ -1,9 +1,12 @@ package api_e2e import ( + "testing" "time" + "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -24,43 +27,42 @@ var ( } ) -// func Test_TokenCmd_Describe(t *testing.T) { -// tk := token1() - -// tests := []*e2e.Test[apiv2.TokenServiceGetResponse, *apiv2.Token]{ -// { -// Name: "describe", -// CmdArgs: []string{"token", "describe", tk.Uuid}, -// NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig[apiv2.TokenServiceGetRequest, apiv2.TokenServiceGetResponse]{ -// WantRequest: apiv2.TokenServiceGetRequest{ -// Uuid: tk.Uuid, -// }, -// WantResponse: apiv2.TokenServiceGetResponse{ -// Token: tk, -// }, -// }), -// WantTable: new(` -// TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES -// TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) -// `), -// WantWideTable: new(` -// TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES -// TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) -// `), -// WantMarkdown: new(` -// | TYPE | ID | ADMIN | USER | DESCRIPTION | ROLES | PERMS | EXPIRES | -// |----------------|--------------------------------------|-------|----------------------|-------------|-------|-------|---------------------------------| -// | TOKEN_TYPE_API | a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 | | admin@metal-stack.io | ci token | 0 | 0 | 2000-01-02 00:00:00 UTC (in 1d) | -// `), -// WantObject: tk, -// WantProtoObject: tk, -// Template: new("{{ .uuid }} {{ .description }}"), -// WantTemplate: new(` -// a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token -// `), -// }, -// } -// for _, tt := range tests { -// tt.TestCmd(t) -// } -// } +func Test_TokenCmd_Describe(t *testing.T) { + tests := []*e2e.Test[apiv2.TokenServiceGetResponse, *apiv2.Token]{ + { + Name: "describe", + CmdArgs: []string{"token", "describe", token1().Uuid}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ + WantRequest: apiv2.TokenServiceGetRequest{ + Uuid: token1().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.TokenServiceGetResponse{ + Token: token1(), + }) + }}), + WantTable: new(` + TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES + TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) + `), + WantWideTable: new(` + TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES + TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) + `), + WantMarkdown: new(` + | TYPE | ID | ADMIN | USER | DESCRIPTION | ROLES | PERMS | EXPIRES | + |----------------|--------------------------------------|-------|----------------------|-------------|-------|-------|---------------------------------| + | TOKEN_TYPE_API | a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 | | admin@metal-stack.io | ci token | 0 | 0 | 2000-01-02 00:00:00 UTC (in 1d) | + `), + WantObject: token1(), + WantProtoObject: token1(), + Template: new("{{ .uuid }} {{ .description }}"), + WantTemplate: new(` + a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/pkg/tests/e2e/test_framework.go b/pkg/tests/e2e/test_framework.go index 6e456a4..3f7939c 100644 --- a/pkg/tests/e2e/test_framework.go +++ b/pkg/tests/e2e/test_framework.go @@ -34,7 +34,7 @@ const ( // NewRootCmdFunc returns the root command for the cli and an output buffer which returns the output after command execution type NewRootCmdFunc func() (rootCmd *cobra.Command, out *bytes.Buffer) -type Test[Response, Object any] struct { +type Test[Response, RawObject any] struct { Name string NewRootCmd NewRootCmdFunc @@ -45,7 +45,7 @@ type Test[Response, Object any] struct { AssertExhaustiveExcludes []string // output format tests - WantObject Object // for rawyaml / rawjson printer + WantObject RawObject // for rawyaml / rawjson printer WantProtoObject proto.Message // for yaml / json printer WantTable *string // for table printer WantWideTable *string // for wide table printer @@ -56,7 +56,7 @@ type Test[Response, Object any] struct { WantErr error } -func (c *Test[Response, Object]) TestCmd(t *testing.T) { +func (c *Test[Response, RawObject]) TestCmd(t *testing.T) { require.NotEmpty(t, c.Name, "test name must not be empty") require.NotEmpty(t, c.CmdArgs, "cmd must not be empty") @@ -105,7 +105,7 @@ func (c *Test[Response, Object]) TestCmd(t *testing.T) { } } -func (c *Test[Response, Object]) assertExhaustiveArgs(t *testing.T) { +func (c *Test[Response, RawObject]) assertExhaustiveArgs(t *testing.T) { assertContainsPrefix := func(ss []string, prefix string) error { for _, s := range ss { if strings.HasPrefix(s, prefix) { diff --git a/pkg/tests/e2e/test_interceptor.go b/pkg/tests/e2e/test_interceptor.go index fdf0b50..0dee0fb 100644 --- a/pkg/tests/e2e/test_interceptor.go +++ b/pkg/tests/e2e/test_interceptor.go @@ -21,6 +21,7 @@ type testClientInterceptor struct { type ClientCall struct { WantRequest any WantResponse func() connect.AnyResponse + WantError *connect.Error } func (t *testClientInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { @@ -28,7 +29,7 @@ func (t *testClientInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryF defer func() { t.count++ }() if t.count >= len(t.calls) { - t.t.Errorf("received an unexpected client call: %v", ar.Any()) + t.t.Errorf("received an unexpected client call of type %T: %v", ar.Any(), ar.Any()) t.t.FailNow() } @@ -39,6 +40,10 @@ func (t *testClientInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryF t.t.FailNow() } + if call.WantError != nil { + return nil, call.WantError + } + return call.WantResponse(), nil } } From 53a4c910ac11d6b99a03f018390867370e4a73b4 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 13:30:09 +0100 Subject: [PATCH 12/34] Move things around. --- pkg/tests/e2e/api/image_test.go | 47 +-- pkg/tests/e2e/api/ip_test.go | 505 ++++++++++++++++-------------- pkg/tests/e2e/api/project_test.go | 40 ++- pkg/tests/e2e/api/tenant_test.go | 20 +- pkg/tests/e2e/api/token_test.go | 21 +- pkg/tests/e2e/test_client.go | 11 +- 6 files changed, 347 insertions(+), 297 deletions(-) diff --git a/pkg/tests/e2e/api/image_test.go b/pkg/tests/e2e/api/image_test.go index dc45ea2..707f8fe 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -34,20 +34,23 @@ func Test_ImageCmd_List(t *testing.T) { { Name: "list", CmdArgs: []string{"image", "list"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, - e2e.ClientCall{ - WantRequest: apiv2.ImageServiceListRequest{ - Query: &apiv2.ImageQuery{}, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.ImageServiceListResponse{ - Images: []*apiv2.Image{ - image1(), - image2(), - }, - }) + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ImageServiceListRequest{ + Query: &apiv2.ImageQuery{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ImageServiceListResponse{ + Images: []*apiv2.Image{ + image1(), + image2(), + }, + }) + }, }, }, + }, ), WantTable: new(` ID NAME DESCRIPTION FEATURES EXPIRATION STATUS @@ -82,14 +85,18 @@ func Test_ImageCmd_Describe(t *testing.T) { { Name: "describe", CmdArgs: []string{"image", "describe", image1().Id}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ - WantRequest: apiv2.ImageServiceGetRequest{ - Id: image1().Id, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.ImageServiceGetResponse{ - Image: image1(), - }) + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ImageServiceGetRequest{ + Id: image1().Id, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ImageServiceGetResponse{ + Image: image1(), + }) + }, + }, }, }), WantTable: new(` diff --git a/pkg/tests/e2e/api/ip_test.go b/pkg/tests/e2e/api/ip_test.go index fe38f9e..7588fe6 100644 --- a/pkg/tests/e2e/api/ip_test.go +++ b/pkg/tests/e2e/api/ip_test.go @@ -56,21 +56,23 @@ func Test_IPCmd_List(t *testing.T) { { Name: "list", CmdArgs: []string{"ip", "list", "--project", ip1().Project}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceListRequest{ - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceListResponse{ - Ips: []*apiv2.IP{ - ip1(), - ip2(), - }, - }) + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.IPServiceListRequest{ + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceListResponse{ + Ips: []*apiv2.IP{ + ip1(), + ip2(), + }, + }) + }, }, }, - ), + }), WantTable: new(` IP PROJECT ID TYPE NAME ATTACHED SERVICE 4.3.2.1 46bdfc45-9c8d-4268-b359-b40e3079d384 9cef40ec-29c6-4dfa-aee8-47ee1f49223d ephemeral b @@ -104,18 +106,21 @@ func Test_IPCmd_Describe(t *testing.T) { { Name: "describe", CmdArgs: []string{"ip", "describe", "--project", ip1().Project, ip1().Ip}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), - }) + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, }, - }), + }, + }), WantObject: ip1(), WantProtoObject: ip1(), WantTable: new(` @@ -147,16 +152,20 @@ func Test_IPCmd_Create(t *testing.T) { { Name: "create", CmdArgs: []string{"ip", "create", "--project", ip1().Project, "--network", ip1().Network, "--static=true"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ - WantRequest: apiv2.IPServiceCreateRequest{ - Project: ip1().Project, - Network: ip1().Network, - Type: &ip1().Type, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceCreateResponse{ - Ip: ip1(), - }) + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.IPServiceCreateRequest{ + Project: ip1().Project, + Network: ip1().Network, + Type: &ip1().Type, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceCreateResponse{ + Ip: ip1(), + }) + }, + }, }, }), WantObject: ip1(), @@ -165,40 +174,41 @@ func Test_IPCmd_Create(t *testing.T) { Name: "create from file", CmdArgs: append([]string{"ip", "create"}, e2e.AppendFromFileCommonArgs()...), NewRootCmd: e2e.NewRootCmd(t, - &e2e.TestClientConfig{ + &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), - }) - }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceCreateRequest{ - Ip: &ip1().Ip, - Project: ip1().Project, - Network: ip1().Network, - Name: &ip1().Name, - Description: &ip1().Description, - Labels: ip1().Meta.Labels, - Type: &ip1().Type, - AddressFamily: nil, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceCreateResponse{ - Ip: ip1(), - }) + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + { + WantRequest: apiv2.IPServiceCreateRequest{ + Ip: &ip1().Ip, + Project: ip1().Project, + Network: ip1().Network, + Name: &ip1().Name, + Description: &ip1().Description, + Labels: ip1().Meta.Labels, + Type: &ip1().Type, + AddressFamily: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceCreateResponse{ + Ip: ip1(), + }) + }, + }, }, - }, - ), + }), WantTable: new(` IP PROJECT ID TYPE NAME ATTACHED SERVICE 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a @@ -215,15 +225,19 @@ func Test_IPCmd_Delete(t *testing.T) { { Name: "delete", CmdArgs: []string{"ip", "delete", "--project", ip1().Project, ip1().Ip}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ - WantRequest: apiv2.IPServiceDeleteRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceDeleteResponse{ - Ip: ip1(), - }) + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.IPServiceDeleteRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceDeleteResponse{ + Ip: ip1(), + }) + }, + }, }, }), WantObject: ip1(), @@ -232,31 +246,33 @@ func Test_IPCmd_Delete(t *testing.T) { Name: "delete from file", CmdArgs: append([]string{"ip", "delete"}, e2e.AppendFromFileCommonArgs()...), NewRootCmd: e2e.NewRootCmd(t, - &e2e.TestClientConfig{ + &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), - }) - }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceDeleteRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceDeleteResponse{ - Ip: ip1(), - }) + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + { + WantRequest: apiv2.IPServiceDeleteRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceDeleteResponse{ + Ip: ip1(), + }) + }, + }, }, }, ), @@ -277,47 +293,50 @@ func Test_IPCmd_Update(t *testing.T) { Name: "update", CmdArgs: []string{"ip", "update", "--project", ip1().Project, ip1().Ip, "--name", "foo"}, NewRootCmd: e2e.NewRootCmd(t, - &e2e.TestClientConfig{}, - // TODO: the client gets the IP two times? - e2e.ClientCall{ - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), - }) - }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), - }) - }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceUpdateRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - Name: new("foo"), + &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + // TODO: the client gets the IP two times? + { + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + { + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + { + WantRequest: apiv2.IPServiceUpdateRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + Name: new("foo"), - // TODO: these fields do not need to be sent? - Description: &ip1().Description, - Labels: &apiv2.UpdateLabels{ - Update: &apiv2.Labels{}, + // TODO: these fields do not need to be sent? + Description: &ip1().Description, + Labels: &apiv2.UpdateLabels{ + Update: &apiv2.Labels{}, + }, + Type: &ip1().Type, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ + Ip: ip1(), + }) + }, }, - Type: &ip1().Type, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ - Ip: ip1(), - }) }, }, ), @@ -327,37 +346,39 @@ func Test_IPCmd_Update(t *testing.T) { Name: "update from file", CmdArgs: append([]string{"ip", "update"}, e2e.AppendFromFileCommonArgs()...), NewRootCmd: e2e.NewRootCmd(t, - &e2e.TestClientConfig{ + &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), - }) - }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceUpdateRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - Description: &ip1().Description, - Labels: &apiv2.UpdateLabels{ - Update: &apiv2.Labels{}, + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + { + WantRequest: apiv2.IPServiceUpdateRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + Description: &ip1().Description, + Labels: &apiv2.UpdateLabels{ + Update: &apiv2.Labels{}, + }, + Name: &ip1().Name, + Type: &ip1().Type, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ + Ip: ip1(), + }) + }, }, - Name: &ip1().Name, - Type: &ip1().Type, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ - Ip: ip1(), - }) }, }, ), @@ -378,37 +399,39 @@ func Test_IPCmd_Apply(t *testing.T) { Name: "apply", CmdArgs: append([]string{"ip", "apply"}, e2e.AppendFromFileCommonArgs()...), NewRootCmd: e2e.NewRootCmd(t, - &e2e.TestClientConfig{ + &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), - }) - }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceCreateRequest{ - Ip: &ip1().Ip, - Project: ip1().Project, - Network: ip1().Network, - Name: &ip1().Name, - Description: &ip1().Description, - Labels: ip1().Meta.Labels, - Type: &ip1().Type, - AddressFamily: nil, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceCreateResponse{ - Ip: ip1(), - }) + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + { + WantRequest: apiv2.IPServiceCreateRequest{ + Ip: &ip1().Ip, + Project: ip1().Project, + Network: ip1().Network, + Name: &ip1().Name, + Description: &ip1().Description, + Labels: ip1().Meta.Labels, + Type: &ip1().Type, + AddressFamily: nil, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceCreateResponse{ + Ip: ip1(), + }) + }, + }, }, }, ), @@ -421,61 +444,63 @@ func Test_IPCmd_Apply(t *testing.T) { Name: "apply already exists", CmdArgs: append([]string{"ip", "apply"}, e2e.AppendFromFileCommonArgs()...), NewRootCmd: e2e.NewRootCmd(t, - &e2e.TestClientConfig{ + &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), - }) - }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceCreateRequest{ - Ip: &ip1().Ip, - Project: ip1().Project, - Network: ip1().Network, - Name: &ip1().Name, - Description: &ip1().Description, - Labels: ip1().Meta.Labels, - Type: &ip1().Type, - AddressFamily: nil, - }, - WantError: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("already exists")), - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), - }) - }, - }, - e2e.ClientCall{ - WantRequest: apiv2.IPServiceUpdateRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - Description: &ip1().Description, - Labels: &apiv2.UpdateLabels{ - Update: &apiv2.Labels{}, + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + { + WantRequest: apiv2.IPServiceCreateRequest{ + Ip: &ip1().Ip, + Project: ip1().Project, + Network: ip1().Network, + Name: &ip1().Name, + Description: &ip1().Description, + Labels: ip1().Meta.Labels, + Type: &ip1().Type, + AddressFamily: nil, + }, + WantError: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("already exists")), + }, + { + WantRequest: apiv2.IPServiceGetRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceGetResponse{ + Ip: ip1(), + }) + }, + }, + { + WantRequest: apiv2.IPServiceUpdateRequest{ + Ip: ip1().Ip, + Project: ip1().Project, + Description: &ip1().Description, + Labels: &apiv2.UpdateLabels{ + Update: &apiv2.Labels{}, + }, + Name: &ip1().Name, + Type: &ip1().Type, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ + Ip: ip1(), + }) + }, }, - Name: &ip1().Name, - Type: &ip1().Type, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ - Ip: ip1(), - }) }, }, ), diff --git a/pkg/tests/e2e/api/project_test.go b/pkg/tests/e2e/api/project_test.go index dc68ae1..221bb59 100644 --- a/pkg/tests/e2e/api/project_test.go +++ b/pkg/tests/e2e/api/project_test.go @@ -40,14 +40,18 @@ func Test_ProjectCmd_Describe(t *testing.T) { { Name: "describe", CmdArgs: []string{"project", "describe", project1().Uuid}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ - WantRequest: apiv2.ProjectServiceGetRequest{ - Project: project1().Uuid, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.ProjectServiceGetResponse{ - Project: project1(), - }) + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ProjectServiceGetRequest{ + Project: project1().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceGetResponse{ + Project: project1(), + }) + }, + }, }, }), WantObject: project1(), @@ -81,15 +85,19 @@ func Test_ProjectCmd_List(t *testing.T) { { Name: "list", CmdArgs: []string{"project", "list"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ - WantRequest: apiv2.ProjectServiceListRequest{}, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.ProjectServiceListResponse{ - Projects: []*apiv2.Project{ - project1(), - project2(), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ProjectServiceListRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceListResponse{ + Projects: []*apiv2.Project{ + project1(), + project2(), + }, + }) }, - }) + }, }, }), WantTable: new(` diff --git a/pkg/tests/e2e/api/tenant_test.go b/pkg/tests/e2e/api/tenant_test.go index b64c555..2ffc328 100644 --- a/pkg/tests/e2e/api/tenant_test.go +++ b/pkg/tests/e2e/api/tenant_test.go @@ -29,14 +29,18 @@ func Test_TenantCmd_Describe(t *testing.T) { { Name: "describe", CmdArgs: []string{"tenant", "describe", tenant1().Login}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ - WantRequest: apiv2.TenantServiceGetRequest{ - Login: tenant1().Login, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.TenantServiceGetResponse{ - Tenant: tenant1(), - }) + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.TenantServiceGetRequest{ + Login: tenant1().Login, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.TenantServiceGetResponse{ + Tenant: tenant1(), + }) + }, + }, }, }), WantTable: new(` diff --git a/pkg/tests/e2e/api/token_test.go b/pkg/tests/e2e/api/token_test.go index a7e34f1..4afe6af 100644 --- a/pkg/tests/e2e/api/token_test.go +++ b/pkg/tests/e2e/api/token_test.go @@ -32,15 +32,20 @@ func Test_TokenCmd_Describe(t *testing.T) { { Name: "describe", CmdArgs: []string{"token", "describe", token1().Uuid}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestClientConfig{}, e2e.ClientCall{ - WantRequest: apiv2.TokenServiceGetRequest{ - Uuid: token1().Uuid, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.TokenServiceGetRequest{ + Uuid: token1().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.TokenServiceGetResponse{ + Token: token1(), + }) + }, + }, }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&apiv2.TokenServiceGetResponse{ - Token: token1(), - }) - }}), + }), WantTable: new(` TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) diff --git a/pkg/tests/e2e/test_client.go b/pkg/tests/e2e/test_client.go index b03e6b2..ab8cf1d 100644 --- a/pkg/tests/e2e/test_client.go +++ b/pkg/tests/e2e/test_client.go @@ -17,17 +17,18 @@ import ( "github.com/stretchr/testify/require" ) -type TestClientConfig struct { - FsMocks func(fs *afero.Afero) - MockStdin *bytes.Buffer +type TestConfig struct { + FsMocks func(fs *afero.Afero) + MockStdin *bytes.Buffer + ClientCalls []ClientCall } -func NewRootCmd(t *testing.T, c *TestClientConfig, calls ...ClientCall) NewRootCmdFunc { +func NewRootCmd(t *testing.T, c *TestConfig) NewRootCmdFunc { return func() (*cobra.Command, *bytes.Buffer) { interceptors := []connect.Interceptor{ &testClientInterceptor{ t: t, - calls: calls, + calls: c.ClientCalls, count: 0, }, validate.NewInterceptor(), From 27401af2da4c5bd47cf68daa429217f54521f9be Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 12:55:23 +0000 Subject: [PATCH 13/34] Add yaml/json output format coverage to image describe test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/image_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tests/e2e/api/image_test.go b/pkg/tests/e2e/api/image_test.go index 707f8fe..890b374 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -99,6 +99,8 @@ func Test_ImageCmd_Describe(t *testing.T) { }, }, }), + WantObject: image1(), + WantProtoObject: image1(), WantTable: new(` ID NAME DESCRIPTION FEATURES EXPIRATION STATUS ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported From 896672e7daa255c8dd9617031013d89596cd7e85 Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 12:56:05 +0000 Subject: [PATCH 14/34] Add token list test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/token_test.go | 62 +++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/pkg/tests/e2e/api/token_test.go b/pkg/tests/e2e/api/token_test.go index 4afe6af..8139f78 100644 --- a/pkg/tests/e2e/api/token_test.go +++ b/pkg/tests/e2e/api/token_test.go @@ -25,6 +25,20 @@ var ( }, } } + token2 = func() *apiv2.Token { + return &apiv2.Token{ + Uuid: "b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01", + User: "dev@metal-stack.io", + Description: "dev token", + TokenType: apiv2.TokenType_TOKEN_TYPE_API, + Expires: timestamppb.New(time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC)), + IssuedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Permissions: nil, + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } ) func Test_TokenCmd_Describe(t *testing.T) { @@ -71,3 +85,51 @@ func Test_TokenCmd_Describe(t *testing.T) { tt.TestCmd(t) } } + +func Test_TokenCmd_List(t *testing.T) { + tests := []*e2e.Test[apiv2.TokenServiceListResponse, apiv2.Token]{ + { + Name: "list", + CmdArgs: []string{"token", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.TokenServiceListRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.TokenServiceListResponse{ + Tokens: []*apiv2.Token{ + token1(), + token2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES + TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) + TOKEN_TYPE_API b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev@metal-stack.io dev token 0 0 2000-01-03 00:00:00 UTC (in 2d) + `), + WantWideTable: new(` + TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES + TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) + TOKEN_TYPE_API b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev@metal-stack.io dev token 0 0 2000-01-03 00:00:00 UTC (in 2d) + `), + Template: new("{{ .uuid }} {{ .description }}"), + WantTemplate: new(` +a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token +b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev token + `), + WantMarkdown: new(` + | TYPE | ID | ADMIN | USER | DESCRIPTION | ROLES | PERMS | EXPIRES | + |----------------|--------------------------------------|-------|----------------------|-------------|-------|-------|---------------------------------| + | TOKEN_TYPE_API | a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 | | admin@metal-stack.io | ci token | 0 | 0 | 2000-01-02 00:00:00 UTC (in 1d) | + | TOKEN_TYPE_API | b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 | | dev@metal-stack.io | dev token | 0 | 0 | 2000-01-03 00:00:00 UTC (in 2d) | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From 983460a250691f7a4a678baf29fc9dfb3701e19d Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 12:56:57 +0000 Subject: [PATCH 15/34] Add tenant list test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/tenant_test.go | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/pkg/tests/e2e/api/tenant_test.go b/pkg/tests/e2e/api/tenant_test.go index 2ffc328..2a45d41 100644 --- a/pkg/tests/e2e/api/tenant_test.go +++ b/pkg/tests/e2e/api/tenant_test.go @@ -22,6 +22,17 @@ var ( }, } } + tenant2 = func() *apiv2.Tenant { + return &apiv2.Tenant{ + Login: "acme-corp", + Name: "ACME Corp", + Email: "admin@acme.io", + Description: "another tenant", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } ) func Test_TenantCmd_Describe(t *testing.T) { @@ -68,3 +79,51 @@ func Test_TenantCmd_Describe(t *testing.T) { tt.TestCmd(t) } } + +func Test_TenantCmd_List(t *testing.T) { + tests := []*e2e.Test[apiv2.TenantServiceListResponse, apiv2.Tenant]{ + { + Name: "list", + CmdArgs: []string{"tenant", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.TenantServiceListRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.TenantServiceListResponse{ + Tenants: []*apiv2.Tenant{ + tenant1(), + tenant2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS + metal-stack Metal Stack info@metal-stack.io now - + acme-corp ACME Corp admin@acme.io now - + `), + WantWideTable: new(` + ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS + metal-stack Metal Stack info@metal-stack.io now - + acme-corp ACME Corp admin@acme.io now - + `), + Template: new("{{ .login }} {{ .name }}"), + WantTemplate: new(` +metal-stack Metal Stack +acme-corp ACME Corp + `), + WantMarkdown: new(` + | ID | NAME | EMAIL | REGISTERED | COUPONS | TERMS AND CONDITIONS | + |-------------|-------------|---------------------|------------|---------|----------------------| + | metal-stack | Metal Stack | info@metal-stack.io | now | - | | + | acme-corp | ACME Corp | admin@acme.io | now | - | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From b05b65e02372d3b82ded0bf38b35750acea37649 Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:03:03 +0000 Subject: [PATCH 16/34] Add project create, delete, update tests with from-file variants. Project's Convert() doesn't make API calls (unlike IP which calls Get inside IpResponseToUpdate), so from-file operations go directly to the target operation without a preceding Get call. Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/project_test.go | 167 ++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/pkg/tests/e2e/api/project_test.go b/pkg/tests/e2e/api/project_test.go index 221bb59..e27ea67 100644 --- a/pkg/tests/e2e/api/project_test.go +++ b/pkg/tests/e2e/api/project_test.go @@ -7,6 +7,8 @@ import ( "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -80,6 +82,171 @@ func Test_ProjectCmd_Describe(t *testing.T) { } } +func Test_ProjectCmd_Create(t *testing.T) { + tests := []*e2e.Test[apiv2.ProjectServiceCreateResponse, *apiv2.Project]{ + { + Name: "create", + CmdArgs: []string{"project", "create", "--name", project1().Name, "--description", project1().Description, "--tenant", project1().Tenant}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ProjectServiceCreateRequest{ + Login: project1().Tenant, + Name: project1().Name, + Description: project1().Description, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceCreateResponse{ + Project: project1(), + }) + }, + }, + }, + }), + WantObject: project1(), + WantProtoObject: project1(), + }, + { + Name: "create from file", + CmdArgs: append([]string{"project", "create"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, project1()), 0755)) + }, + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ProjectServiceCreateRequest{ + Login: project1().Tenant, + Name: project1().Name, + Description: project1().Description, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceCreateResponse{ + Project: project1(), + }) + }, + }, + }, + }), + WantTable: new(` + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_ProjectCmd_Delete(t *testing.T) { + tests := []*e2e.Test[apiv2.ProjectServiceDeleteResponse, *apiv2.Project]{ + { + Name: "delete", + CmdArgs: []string{"project", "delete", project1().Uuid}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ProjectServiceDeleteRequest{ + Project: project1().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceDeleteResponse{ + Project: project1(), + }) + }, + }, + }, + }), + WantObject: project1(), + }, + { + Name: "delete from file", + CmdArgs: append([]string{"project", "delete"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, project1()), 0755)) + }, + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ProjectServiceDeleteRequest{ + Project: project1().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceDeleteResponse{ + Project: project1(), + }) + }, + }, + }, + }), + WantTable: new(` + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_ProjectCmd_Update(t *testing.T) { + tests := []*e2e.Test[apiv2.ProjectServiceUpdateResponse, *apiv2.Project]{ + { + Name: "update", + CmdArgs: []string{"project", "update", project1().Uuid, "--name", "new-name", "--description", "new-desc"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ProjectServiceUpdateRequest{ + Project: project1().Uuid, + Name: new("new-name"), + Description: new("new-desc"), + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceUpdateResponse{ + Project: project1(), + }) + }, + }, + }, + }), + WantObject: project1(), + }, + { + Name: "update from file", + CmdArgs: append([]string{"project", "update"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, project1()), 0755)) + }, + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ProjectServiceUpdateRequest{ + Project: project1().Uuid, + Name: new(project1().Name), + Description: new(project1().Description), + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceUpdateResponse{ + Project: project1(), + }) + }, + }, + }, + }), + WantTable: new(` + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + func Test_ProjectCmd_List(t *testing.T) { tests := []*e2e.Test[apiv2.ProjectServiceListResponse, apiv2.Project]{ { From a912333720e1c6b79ae42f03367d6dab930cdbdf Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:20:42 +0000 Subject: [PATCH 17/34] Add admin tenant list test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 pkg/tests/e2e/api/admin_test.go diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go new file mode 100644 index 0000000..33a1020 --- /dev/null +++ b/pkg/tests/e2e/api/admin_test.go @@ -0,0 +1,85 @@ +package api_e2e + +import ( + "testing" + "time" + + "connectrpc.com/connect" + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + adminTenant1 = func() *apiv2.Tenant { + return &apiv2.Tenant{ + Login: "metal-stack", + Name: "Metal Stack", + Email: "info@metal-stack.io", + Description: "a tenant", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } + adminTenant2 = func() *apiv2.Tenant { + return &apiv2.Tenant{ + Login: "acme-corp", + Name: "ACME Corp", + Email: "admin@acme.io", + Description: "another tenant", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } +) + +func Test_AdminTenantCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.TenantServiceListResponse, apiv2.Tenant]{ + { + Name: "list", + CmdArgs: []string{"admin", "tenant", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.TenantServiceListRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.TenantServiceListResponse{ + Tenants: []*apiv2.Tenant{ + adminTenant1(), + adminTenant2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS + metal-stack Metal Stack info@metal-stack.io now - + acme-corp ACME Corp admin@acme.io now - + `), + WantWideTable: new(` + ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS + metal-stack Metal Stack info@metal-stack.io now - + acme-corp ACME Corp admin@acme.io now - + `), + Template: new("{{ .login }} {{ .name }}"), + WantTemplate: new(` +metal-stack Metal Stack +acme-corp ACME Corp + `), + WantMarkdown: new(` + | ID | NAME | EMAIL | REGISTERED | COUPONS | TERMS AND CONDITIONS | + |-------------|-------------|---------------------|------------|---------|----------------------| + | metal-stack | Metal Stack | info@metal-stack.io | now | - | | + | acme-corp | ACME Corp | admin@acme.io | now | - | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From ea373a696955633916d59e7edf160ad2f00385b8 Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:21:15 +0000 Subject: [PATCH 18/34] Add admin tenant create test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go index 33a1020..dcd8206 100644 --- a/pkg/tests/e2e/api/admin_test.go +++ b/pkg/tests/e2e/api/admin_test.go @@ -36,6 +36,36 @@ var ( } ) +func Test_AdminTenantCmd_Create(t *testing.T) { + tests := []*e2e.Test[adminv2.TenantServiceCreateResponse, *apiv2.Tenant]{ + { + Name: "create", + CmdArgs: []string{"admin", "tenant", "create", "--name", adminTenant1().Name, "--description", adminTenant1().Description, "--email", adminTenant1().Email}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.TenantServiceCreateRequest{ + Name: adminTenant1().Name, + Description: new(adminTenant1().Description), + Email: new(adminTenant1().Email), + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.TenantServiceCreateResponse{ + Tenant: adminTenant1(), + }) + }, + }, + }, + }), + WantObject: adminTenant1(), + WantProtoObject: adminTenant1(), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + func Test_AdminTenantCmd_List(t *testing.T) { tests := []*e2e.Test[adminv2.TenantServiceListResponse, apiv2.Tenant]{ { From 1266b22248f11b3426644d2e5268944197277db6 Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:23:02 +0000 Subject: [PATCH 19/34] Add admin token list test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go index dcd8206..788365e 100644 --- a/pkg/tests/e2e/api/admin_test.go +++ b/pkg/tests/e2e/api/admin_test.go @@ -113,3 +113,51 @@ acme-corp ACME Corp tt.TestCmd(t) } } + +func Test_AdminTokenCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.TokenServiceListResponse, apiv2.Token]{ + { + Name: "list", + CmdArgs: []string{"admin", "token", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.TokenServiceListRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.TokenServiceListResponse{ + Tokens: []*apiv2.Token{ + token1(), + token2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES + TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) + TOKEN_TYPE_API b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev@metal-stack.io dev token 0 0 2000-01-03 00:00:00 UTC (in 2d) + `), + WantWideTable: new(` + TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES + TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) + TOKEN_TYPE_API b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev@metal-stack.io dev token 0 0 2000-01-03 00:00:00 UTC (in 2d) + `), + Template: new("{{ .uuid }} {{ .description }}"), + WantTemplate: new(` +a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token +b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev token + `), + WantMarkdown: new(` + | TYPE | ID | ADMIN | USER | DESCRIPTION | ROLES | PERMS | EXPIRES | + |----------------|--------------------------------------|-------|----------------------|-------------|-------|-------|---------------------------------| + | TOKEN_TYPE_API | a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 | | admin@metal-stack.io | ci token | 0 | 0 | 2000-01-02 00:00:00 UTC (in 1d) | + | TOKEN_TYPE_API | b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 | | dev@metal-stack.io | dev token | 0 | 0 | 2000-01-03 00:00:00 UTC (in 2d) | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From b7e248aae1bc731956d7151f716b393b6fa4952e Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:27:53 +0000 Subject: [PATCH 20/34] Add admin token delete test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go index 788365e..64c3cd8 100644 --- a/pkg/tests/e2e/api/admin_test.go +++ b/pkg/tests/e2e/api/admin_test.go @@ -161,3 +161,31 @@ b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev token tt.TestCmd(t) } } + +func Test_AdminTokenCmd_Delete(t *testing.T) { + tests := []*e2e.Test[adminv2.TokenServiceRevokeResponse, *apiv2.Token]{ + { + Name: "delete", + CmdArgs: []string{"admin", "token", "delete", token1().Uuid, "--user", "user-123"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.TokenServiceRevokeRequest{ + Uuid: token1().Uuid, + User: "user-123", + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.TokenServiceRevokeResponse{}) + }, + }, + }, + }), + WantObject: &apiv2.Token{ + Uuid: token1().Uuid, + }, + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From a151c8bd2ec655a16510f4ef20f825af449b44be Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:28:37 +0000 Subject: [PATCH 21/34] Add admin project list test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go index 64c3cd8..f464e9d 100644 --- a/pkg/tests/e2e/api/admin_test.go +++ b/pkg/tests/e2e/api/admin_test.go @@ -189,3 +189,51 @@ func Test_AdminTokenCmd_Delete(t *testing.T) { tt.TestCmd(t) } } + +func Test_AdminProjectCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.ProjectServiceListResponse, apiv2.Project]{ + { + Name: "list", + CmdArgs: []string{"admin", "project", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.ProjectServiceListRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ProjectServiceListResponse{ + Projects: []*apiv2.Project{ + project1(), + project2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + `), + WantWideTable: new(` + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + `), + Template: new("{{ .uuid }} {{ .name }}"), + WantTemplate: new(` +0d81bca7-73f6-4da3-8397-4a8c52a0c583 project-a +f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c project-b + `), + WantMarkdown: new(` + | ID | TENANT | NAME | DESCRIPTION | CREATION DATE | + |--------------------------------------|-------------|-----------|----------------|-------------------------| + | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | + | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | metal-stack | project-b | second project | 2025-07-15 14:30:00 UTC | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From 449071786e3aabc3c72e78d73f95f6a841a0a9e8 Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:31:15 +0000 Subject: [PATCH 22/34] Add admin component describe test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go index f464e9d..337444a 100644 --- a/pkg/tests/e2e/api/admin_test.go +++ b/pkg/tests/e2e/api/admin_test.go @@ -8,6 +8,7 @@ import ( adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -237,3 +238,68 @@ f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c project-b tt.TestCmd(t) } } + +var ( + component1 = func() *apiv2.Component { + return &apiv2.Component{ + Uuid: "c1a2b3d4-e5f6-7890-abcd-ef1234567890", + Type: apiv2.ComponentType_COMPONENT_TYPE_METAL_CORE, + Identifier: "metal-core-1", + StartedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ReportedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Interval: durationpb.New(10 * time.Second), + Version: &apiv2.Version{ + Version: "v1.0.0", + }, + Token: &apiv2.Token{ + Uuid: "t1a2b3d4-e5f6-7890-abcd-ef1234567890", + Expires: timestamppb.New(time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC)), + }, + } + } +) + +func Test_AdminComponentCmd_Describe(t *testing.T) { + tests := []*e2e.Test[adminv2.ComponentServiceGetResponse, *apiv2.Component]{ + { + Name: "describe", + CmdArgs: []string{"admin", "component", "describe", component1().Uuid}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.ComponentServiceGetRequest{ + Uuid: component1().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceGetResponse{ + Component: component1(), + }) + }, + }, + }, + }), + WantObject: component1(), + WantProtoObject: component1(), + WantTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d + `), + WantWideTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d + `), + Template: new("{{ .uuid }} {{ .identifier }}"), + WantTemplate: new(` + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core-1 + `), + WantMarkdown: new(` + | ID | TYPE | IDENTIFIER | STARTED | AGE | VERSION | TOKEN | TOKEN EXPIRES IN | + |--------------------------------------|------------|--------------|---------|-----|---------|--------------------------------------|------------------| + | c1a2b3d4-e5f6-7890-abcd-ef1234567890 | metal-core | metal-core-1 | 0s | 0s | v1.0.0 | t1a2b3d4-e5f6-7890-abcd-ef1234567890 | 1d | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From 4b95c4435fde662b0cf943180839a887cb430acf Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:32:30 +0000 Subject: [PATCH 23/34] Add admin component list test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 67 +++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go index 337444a..bc2db8d 100644 --- a/pkg/tests/e2e/api/admin_test.go +++ b/pkg/tests/e2e/api/admin_test.go @@ -257,6 +257,23 @@ var ( }, } } + component2 = func() *apiv2.Component { + return &apiv2.Component{ + Uuid: "d2b3c4e5-f6a7-8901-bcde-f12345678901", + Type: apiv2.ComponentType_COMPONENT_TYPE_PIXIECORE, + Identifier: "pixiecore-1", + StartedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ReportedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Interval: durationpb.New(10 * time.Second), + Version: &apiv2.Version{ + Version: "v2.0.0", + }, + Token: &apiv2.Token{ + Uuid: "t2b3c4e5-f6a7-8901-bcde-f12345678901", + Expires: timestamppb.New(time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC)), + }, + } + } ) func Test_AdminComponentCmd_Describe(t *testing.T) { @@ -303,3 +320,53 @@ func Test_AdminComponentCmd_Describe(t *testing.T) { tt.TestCmd(t) } } + +func Test_AdminComponentCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.ComponentServiceListResponse, apiv2.Component]{ + { + Name: "list", + CmdArgs: []string{"admin", "component", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.ComponentServiceListRequest{ + Query: &apiv2.ComponentQuery{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceListResponse{ + Components: []*apiv2.Component{ + component1(), + component2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d + d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore pixiecore-1 0s 0s v2.0.0 t2b3c4e5-f6a7-8901-bcde-f12345678901 2d + `), + WantWideTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d + d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore pixiecore-1 0s 0s v2.0.0 t2b3c4e5-f6a7-8901-bcde-f12345678901 2d + `), + Template: new("{{ .uuid }} {{ .identifier }}"), + WantTemplate: new(` +c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core-1 +d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore-1 + `), + WantMarkdown: new(` + | ID | TYPE | IDENTIFIER | STARTED | AGE | VERSION | TOKEN | TOKEN EXPIRES IN | + |--------------------------------------|------------|--------------|---------|-----|---------|--------------------------------------|------------------| + | c1a2b3d4-e5f6-7890-abcd-ef1234567890 | metal-core | metal-core-1 | 0s | 0s | v1.0.0 | t1a2b3d4-e5f6-7890-abcd-ef1234567890 | 1d | + | d2b3c4e5-f6a7-8901-bcde-f12345678901 | pixiecore | pixiecore-1 | 0s | 0s | v2.0.0 | t2b3c4e5-f6a7-8901-bcde-f12345678901 | 2d | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From f658e45642521ccecdddb61b540a69f5d69e0649 Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:33:07 +0000 Subject: [PATCH 24/34] Add admin component delete test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go index bc2db8d..1bfca3d 100644 --- a/pkg/tests/e2e/api/admin_test.go +++ b/pkg/tests/e2e/api/admin_test.go @@ -370,3 +370,30 @@ d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore-1 tt.TestCmd(t) } } + +func Test_AdminComponentCmd_Delete(t *testing.T) { + tests := []*e2e.Test[adminv2.ComponentServiceDeleteResponse, *apiv2.Component]{ + { + Name: "delete", + CmdArgs: []string{"admin", "component", "delete", component1().Uuid}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.ComponentServiceDeleteRequest{ + Uuid: component1().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceDeleteResponse{ + Component: component1(), + }) + }, + }, + }, + }), + WantObject: component1(), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From 02badc73f5b613f98d99e9f2b3599b78b15c0e33 Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:37:33 +0000 Subject: [PATCH 25/34] Add admin switch describe test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 75 +++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go index 1bfca3d..2e8a966 100644 --- a/pkg/tests/e2e/api/admin_test.go +++ b/pkg/tests/e2e/api/admin_test.go @@ -397,3 +397,78 @@ func Test_AdminComponentCmd_Delete(t *testing.T) { tt.TestCmd(t) } } + +var ( + switch1 = func() *apiv2.Switch { + return &apiv2.Switch{ + Id: "leaf01", + Partition: "fra-equ01", + Rack: new("rack-1"), + Description: "leaf switch 1", + ManagementIp: "10.0.0.1", + ManagementUser: new("admin"), + Os: &apiv2.SwitchOS{ + Vendor: apiv2.SwitchOSVendor_SWITCH_OS_VENDOR_SONIC, + Version: "4.2.0", + MetalCoreVersion: "v0.9.1 (abc1234), tags/v0.9.1", + }, + LastSync: &apiv2.SwitchSync{ + Time: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Duration: durationpb.New(100 * time.Millisecond), + }, + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } + switch2 = func() *apiv2.Switch { + return &apiv2.Switch{ + Id: "leaf02", + Partition: "fra-equ01", + Rack: new("rack-1"), + Description: "leaf switch 2", + ManagementIp: "10.0.0.2", + ManagementUser: new("admin"), + Os: &apiv2.SwitchOS{ + Vendor: apiv2.SwitchOSVendor_SWITCH_OS_VENDOR_SONIC, + Version: "4.2.0", + MetalCoreVersion: "v0.9.1 (abc1234), tags/v0.9.1", + }, + LastSync: &apiv2.SwitchSync{ + Time: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Duration: durationpb.New(200 * time.Millisecond), + }, + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } +) + +func Test_AdminSwitchCmd_Describe(t *testing.T) { + tests := []*e2e.Test[adminv2.SwitchServiceGetResponse, *apiv2.Switch]{ + { + Name: "describe", + CmdArgs: []string{"admin", "switch", "describe", switch1().Id}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.SwitchServiceGetRequest{ + Id: switch1().Id, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.SwitchServiceGetResponse{ + Switch: switch1(), + }) + }, + }, + }, + }), + WantObject: switch1(), + WantProtoObject: switch1(), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From d5c3bdbb1c1dbbfd5ed74a05c6be933bd4fe23d8 Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:38:56 +0000 Subject: [PATCH 26/34] Add admin switch list test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go index 2e8a966..177f18c 100644 --- a/pkg/tests/e2e/api/admin_test.go +++ b/pkg/tests/e2e/api/admin_test.go @@ -472,3 +472,55 @@ func Test_AdminSwitchCmd_Describe(t *testing.T) { tt.TestCmd(t) } } + +func Test_AdminSwitchCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.SwitchServiceListResponse, apiv2.Switch]{ + { + Name: "list", + CmdArgs: []string{"admin", "switch", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.SwitchServiceListRequest{ + Query: &apiv2.SwitchQuery{ + Os: &apiv2.SwitchOSQuery{}, + }, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.SwitchServiceListResponse{ + Switches: []*apiv2.Switch{ + switch1(), + switch2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID PARTITION RACK OS STATUS LAST SYNC + leaf01 fra-equ01 rack-1 🦔 ● + leaf02 fra-equ01 rack-1 🦔 ● + `), + WantWideTable: new(` + ID PARTITION RACK OS METALCORE IP MODE LAST SYNC SYNC DURATION LAST ERROR + leaf01 fra-equ01 rack-1 SONiC (4.2.0) v0.9.1 (abc1234) 10.0.0.1 operational 100ms + leaf02 fra-equ01 rack-1 SONiC (4.2.0) v0.9.1 (abc1234) 10.0.0.2 operational 200ms + `), + Template: new("{{ .id }} {{ .partition }}"), + WantTemplate: new(` +leaf01 fra-equ01 +leaf02 fra-equ01 + `), + WantMarkdown: new(` + | ID | PARTITION | RACK | OS | STATUS | LAST SYNC | + |--------|-----------|--------|----|--------|-----------| + | leaf01 | fra-equ01 | rack-1 | 🦔 | ● | | + | leaf02 | fra-equ01 | rack-1 | 🦔 | ● | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From a4e9a703f5e14b0db6982ac7e2d30072c5d0ba97 Mon Sep 17 00:00:00 2001 From: Christian Brunner Date: Thu, 19 Mar 2026 13:39:28 +0000 Subject: [PATCH 27/34] Add admin switch delete test Co-Authored-By: Claude Opus 4.6 --- pkg/tests/e2e/api/admin_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go index 177f18c..ba9b2b5 100644 --- a/pkg/tests/e2e/api/admin_test.go +++ b/pkg/tests/e2e/api/admin_test.go @@ -524,3 +524,30 @@ leaf02 fra-equ01 tt.TestCmd(t) } } + +func Test_AdminSwitchCmd_Delete(t *testing.T) { + tests := []*e2e.Test[adminv2.SwitchServiceDeleteResponse, *apiv2.Switch]{ + { + Name: "delete", + CmdArgs: []string{"admin", "switch", "delete", switch1().Id}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.SwitchServiceDeleteRequest{ + Id: switch1().Id, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.SwitchServiceDeleteResponse{ + Switch: switch1(), + }) + }, + }, + }, + }), + WantObject: switch1(), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} From 9353f7cf4e7d02f01c97b86d45a211c566d26a3a Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 15:32:45 +0100 Subject: [PATCH 28/34] Refactor. --- pkg/tests/e2e/admin/component_test.go | 172 ++++++++ pkg/tests/e2e/admin/project_test.go | 59 +++ pkg/tests/e2e/admin/switch_test.go | 167 +++++++ pkg/tests/e2e/admin/tenant_test.go | 115 +++++ pkg/tests/e2e/admin/token_test.go | 87 ++++ pkg/tests/e2e/api/admin_test.go | 553 ------------------------ pkg/tests/e2e/api/image_test.go | 36 +- pkg/tests/e2e/api/ip_test.go | 231 ++++------ pkg/tests/e2e/api/project_test.go | 98 ++--- pkg/tests/e2e/api/tenant_test.go | 42 +- pkg/tests/e2e/api/token_test.go | 48 +- pkg/tests/e2e/testresources/images.go | 24 + pkg/tests/e2e/testresources/ips.go | 45 ++ pkg/tests/e2e/testresources/projects.go | 33 ++ pkg/tests/e2e/testresources/tenants.go | 33 ++ pkg/tests/e2e/testresources/tokens.go | 39 ++ 16 files changed, 930 insertions(+), 852 deletions(-) create mode 100644 pkg/tests/e2e/admin/component_test.go create mode 100644 pkg/tests/e2e/admin/project_test.go create mode 100644 pkg/tests/e2e/admin/switch_test.go create mode 100644 pkg/tests/e2e/admin/tenant_test.go create mode 100644 pkg/tests/e2e/admin/token_test.go delete mode 100644 pkg/tests/e2e/api/admin_test.go create mode 100644 pkg/tests/e2e/testresources/images.go create mode 100644 pkg/tests/e2e/testresources/ips.go create mode 100644 pkg/tests/e2e/testresources/projects.go create mode 100644 pkg/tests/e2e/testresources/tenants.go create mode 100644 pkg/tests/e2e/testresources/tokens.go diff --git a/pkg/tests/e2e/admin/component_test.go b/pkg/tests/e2e/admin/component_test.go new file mode 100644 index 0000000..b75be84 --- /dev/null +++ b/pkg/tests/e2e/admin/component_test.go @@ -0,0 +1,172 @@ +package admin_e2e + +import ( + "testing" + "time" + + "connectrpc.com/connect" + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + component1 = func() *apiv2.Component { + return &apiv2.Component{ + Uuid: "c1a2b3d4-e5f6-7890-abcd-ef1234567890", + Type: apiv2.ComponentType_COMPONENT_TYPE_METAL_CORE, + Identifier: "metal-core-1", + StartedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ReportedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Interval: durationpb.New(10 * time.Second), + Version: &apiv2.Version{ + Version: "v1.0.0", + }, + Token: &apiv2.Token{ + Uuid: "t1a2b3d4-e5f6-7890-abcd-ef1234567890", + Expires: timestamppb.New(time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC)), + }, + } + } + component2 = func() *apiv2.Component { + return &apiv2.Component{ + Uuid: "d2b3c4e5-f6a7-8901-bcde-f12345678901", + Type: apiv2.ComponentType_COMPONENT_TYPE_PIXIECORE, + Identifier: "pixiecore-1", + StartedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + ReportedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Interval: durationpb.New(10 * time.Second), + Version: &apiv2.Version{ + Version: "v2.0.0", + }, + Token: &apiv2.Token{ + Uuid: "t2b3c4e5-f6a7-8901-bcde-f12345678901", + Expires: timestamppb.New(time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC)), + }, + } + } +) + +func Test_AdminComponentCmd_Describe(t *testing.T) { + tests := []*e2e.Test[adminv2.ComponentServiceGetResponse, *apiv2.Component]{ + { + Name: "describe", + CmdArgs: []string{"admin", "component", "describe", component1().Uuid}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.ComponentServiceGetRequest{ + Uuid: component1().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceGetResponse{ + Component: component1(), + }) + }, + }, + }, + }), + WantObject: component1(), + WantProtoObject: component1(), + WantTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d + `), + WantWideTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d + `), + Template: new("{{ .uuid }} {{ .identifier }}"), + WantTemplate: new(` + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core-1 + `), + WantMarkdown: new(` + | ID | TYPE | IDENTIFIER | STARTED | AGE | VERSION | TOKEN | TOKEN EXPIRES IN | + |--------------------------------------|------------|--------------|---------|-----|---------|--------------------------------------|------------------| + | c1a2b3d4-e5f6-7890-abcd-ef1234567890 | metal-core | metal-core-1 | 0s | 0s | v1.0.0 | t1a2b3d4-e5f6-7890-abcd-ef1234567890 | 1d | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminComponentCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.ComponentServiceListResponse, apiv2.Component]{ + { + Name: "list", + CmdArgs: []string{"admin", "component", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.ComponentServiceListRequest{ + Query: &apiv2.ComponentQuery{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceListResponse{ + Components: []*apiv2.Component{ + component1(), + component2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d + d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore pixiecore-1 0s 0s v2.0.0 t2b3c4e5-f6a7-8901-bcde-f12345678901 2d + `), + WantWideTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d + d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore pixiecore-1 0s 0s v2.0.0 t2b3c4e5-f6a7-8901-bcde-f12345678901 2d + `), + Template: new("{{ .uuid }} {{ .identifier }}"), + WantTemplate: new(` +c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core-1 +d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore-1 + `), + WantMarkdown: new(` + | ID | TYPE | IDENTIFIER | STARTED | AGE | VERSION | TOKEN | TOKEN EXPIRES IN | + |--------------------------------------|------------|--------------|---------|-----|---------|--------------------------------------|------------------| + | c1a2b3d4-e5f6-7890-abcd-ef1234567890 | metal-core | metal-core-1 | 0s | 0s | v1.0.0 | t1a2b3d4-e5f6-7890-abcd-ef1234567890 | 1d | + | d2b3c4e5-f6a7-8901-bcde-f12345678901 | pixiecore | pixiecore-1 | 0s | 0s | v2.0.0 | t2b3c4e5-f6a7-8901-bcde-f12345678901 | 2d | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminComponentCmd_Delete(t *testing.T) { + tests := []*e2e.Test[adminv2.ComponentServiceDeleteResponse, *apiv2.Component]{ + { + Name: "delete", + CmdArgs: []string{"admin", "component", "delete", component1().Uuid}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.ComponentServiceDeleteRequest{ + Uuid: component1().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceDeleteResponse{ + Component: component1(), + }) + }, + }, + }, + }), + WantObject: component1(), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/pkg/tests/e2e/admin/project_test.go b/pkg/tests/e2e/admin/project_test.go new file mode 100644 index 0000000..56f7b82 --- /dev/null +++ b/pkg/tests/e2e/admin/project_test.go @@ -0,0 +1,59 @@ +package admin_e2e + +import ( + "testing" + + "connectrpc.com/connect" + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/metal-stack/cli/pkg/tests/e2e/testresources" +) + +func Test_AdminProjectCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.ProjectServiceListResponse, apiv2.Project]{ + { + Name: "list", + CmdArgs: []string{"admin", "project", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.ProjectServiceListRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ProjectServiceListResponse{ + Projects: []*apiv2.Project{ + testresources.Project1(), + testresources.Project2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + `), + WantWideTable: new(` + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + `), + Template: new("{{ .uuid }} {{ .name }}"), + WantTemplate: new(` +0d81bca7-73f6-4da3-8397-4a8c52a0c583 project-a +f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c project-b + `), + WantMarkdown: new(` + | ID | TENANT | NAME | DESCRIPTION | CREATION DATE | + |--------------------------------------|-------------|-----------|----------------|-------------------------| + | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | + | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | metal-stack | project-b | second project | 2025-07-15 14:30:00 UTC | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/pkg/tests/e2e/admin/switch_test.go b/pkg/tests/e2e/admin/switch_test.go new file mode 100644 index 0000000..c9585ff --- /dev/null +++ b/pkg/tests/e2e/admin/switch_test.go @@ -0,0 +1,167 @@ +package admin_e2e + +import ( + "testing" + "time" + + "connectrpc.com/connect" + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + switch1 = func() *apiv2.Switch { + return &apiv2.Switch{ + Id: "leaf01", + Partition: "fra-equ01", + Rack: new("rack-1"), + Description: "leaf switch 1", + ManagementIp: "10.0.0.1", + ManagementUser: new("admin"), + Os: &apiv2.SwitchOS{ + Vendor: apiv2.SwitchOSVendor_SWITCH_OS_VENDOR_SONIC, + Version: "4.2.0", + MetalCoreVersion: "v0.9.1 (abc1234), tags/v0.9.1", + }, + LastSync: &apiv2.SwitchSync{ + Time: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Duration: durationpb.New(100 * time.Millisecond), + }, + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } + switch2 = func() *apiv2.Switch { + return &apiv2.Switch{ + Id: "leaf02", + Partition: "fra-equ01", + Rack: new("rack-1"), + Description: "leaf switch 2", + ManagementIp: "10.0.0.2", + ManagementUser: new("admin"), + Os: &apiv2.SwitchOS{ + Vendor: apiv2.SwitchOSVendor_SWITCH_OS_VENDOR_SONIC, + Version: "4.2.0", + MetalCoreVersion: "v0.9.1 (abc1234), tags/v0.9.1", + }, + LastSync: &apiv2.SwitchSync{ + Time: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Duration: durationpb.New(200 * time.Millisecond), + }, + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } +) + +func Test_AdminSwitchCmd_Describe(t *testing.T) { + tests := []*e2e.Test[adminv2.SwitchServiceGetResponse, *apiv2.Switch]{ + { + Name: "describe", + CmdArgs: []string{"admin", "switch", "describe", switch1().Id}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.SwitchServiceGetRequest{ + Id: switch1().Id, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.SwitchServiceGetResponse{ + Switch: switch1(), + }) + }, + }, + }, + }), + WantObject: switch1(), + WantProtoObject: switch1(), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminSwitchCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.SwitchServiceListResponse, apiv2.Switch]{ + { + Name: "list", + CmdArgs: []string{"admin", "switch", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.SwitchServiceListRequest{ + Query: &apiv2.SwitchQuery{ + Os: &apiv2.SwitchOSQuery{}, + }, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.SwitchServiceListResponse{ + Switches: []*apiv2.Switch{ + switch1(), + switch2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID PARTITION RACK OS STATUS LAST SYNC + leaf01 fra-equ01 rack-1 🦔 ● + leaf02 fra-equ01 rack-1 🦔 ● + `), + WantWideTable: new(` + ID PARTITION RACK OS METALCORE IP MODE LAST SYNC SYNC DURATION LAST ERROR + leaf01 fra-equ01 rack-1 SONiC (4.2.0) v0.9.1 (abc1234) 10.0.0.1 operational 100ms + leaf02 fra-equ01 rack-1 SONiC (4.2.0) v0.9.1 (abc1234) 10.0.0.2 operational 200ms + `), + Template: new("{{ .id }} {{ .partition }}"), + WantTemplate: new(` +leaf01 fra-equ01 +leaf02 fra-equ01 + `), + WantMarkdown: new(` + | ID | PARTITION | RACK | OS | STATUS | LAST SYNC | + |--------|-----------|--------|----|--------|-----------| + | leaf01 | fra-equ01 | rack-1 | 🦔 | ● | | + | leaf02 | fra-equ01 | rack-1 | 🦔 | ● | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminSwitchCmd_Delete(t *testing.T) { + tests := []*e2e.Test[adminv2.SwitchServiceDeleteResponse, *apiv2.Switch]{ + { + Name: "delete", + CmdArgs: []string{"admin", "switch", "delete", switch1().Id}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.SwitchServiceDeleteRequest{ + Id: switch1().Id, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.SwitchServiceDeleteResponse{ + Switch: switch1(), + }) + }, + }, + }, + }), + WantObject: switch1(), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/pkg/tests/e2e/admin/tenant_test.go b/pkg/tests/e2e/admin/tenant_test.go new file mode 100644 index 0000000..6be8d53 --- /dev/null +++ b/pkg/tests/e2e/admin/tenant_test.go @@ -0,0 +1,115 @@ +package admin_e2e + +import ( + "testing" + "time" + + "connectrpc.com/connect" + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + adminTenant1 = func() *apiv2.Tenant { + return &apiv2.Tenant{ + Login: "metal-stack", + Name: "Metal Stack", + Email: "info@metal-stack.io", + Description: "a tenant", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } + adminTenant2 = func() *apiv2.Tenant { + return &apiv2.Tenant{ + Login: "acme-corp", + Name: "ACME Corp", + Email: "admin@acme.io", + Description: "another tenant", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } +) + +func Test_AdminTenantCmd_Create(t *testing.T) { + tests := []*e2e.Test[adminv2.TenantServiceCreateResponse, *apiv2.Tenant]{ + { + Name: "create", + CmdArgs: []string{"admin", "tenant", "create", "--name", adminTenant1().Name, "--description", adminTenant1().Description, "--email", adminTenant1().Email}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.TenantServiceCreateRequest{ + Name: adminTenant1().Name, + Description: new(adminTenant1().Description), + Email: new(adminTenant1().Email), + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.TenantServiceCreateResponse{ + Tenant: adminTenant1(), + }) + }, + }, + }, + }), + WantObject: adminTenant1(), + WantProtoObject: adminTenant1(), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminTenantCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.TenantServiceListResponse, apiv2.Tenant]{ + { + Name: "list", + CmdArgs: []string{"admin", "tenant", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.TenantServiceListRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.TenantServiceListResponse{ + Tenants: []*apiv2.Tenant{ + adminTenant1(), + adminTenant2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS + metal-stack Metal Stack info@metal-stack.io now - + acme-corp ACME Corp admin@acme.io now - + `), + WantWideTable: new(` + ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS + metal-stack Metal Stack info@metal-stack.io now - + acme-corp ACME Corp admin@acme.io now - + `), + Template: new("{{ .login }} {{ .name }}"), + WantTemplate: new(` +metal-stack Metal Stack +acme-corp ACME Corp + `), + WantMarkdown: new(` + | ID | NAME | EMAIL | REGISTERED | COUPONS | TERMS AND CONDITIONS | + |-------------|-------------|---------------------|------------|---------|----------------------| + | metal-stack | Metal Stack | info@metal-stack.io | now | - | | + | acme-corp | ACME Corp | admin@acme.io | now | - | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/pkg/tests/e2e/admin/token_test.go b/pkg/tests/e2e/admin/token_test.go new file mode 100644 index 0000000..f698983 --- /dev/null +++ b/pkg/tests/e2e/admin/token_test.go @@ -0,0 +1,87 @@ +package admin_e2e + +import ( + "testing" + + "connectrpc.com/connect" + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/metal-stack/cli/pkg/tests/e2e/testresources" +) + +func Test_AdminTokenCmd_List(t *testing.T) { + tests := []*e2e.Test[adminv2.TokenServiceListResponse, apiv2.Token]{ + { + Name: "list", + CmdArgs: []string{"admin", "token", "list"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.TokenServiceListRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.TokenServiceListResponse{ + Tokens: []*apiv2.Token{ + testresources.Token1(), + testresources.Token2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES + TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) + TOKEN_TYPE_API b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev@metal-stack.io dev token 0 0 2000-01-03 00:00:00 UTC (in 2d) + `), + WantWideTable: new(` + TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES + TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) + TOKEN_TYPE_API b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev@metal-stack.io dev token 0 0 2000-01-03 00:00:00 UTC (in 2d) + `), + Template: new("{{ .uuid }} {{ .description }}"), + WantTemplate: new(` +a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token +b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev token + `), + WantMarkdown: new(` + | TYPE | ID | ADMIN | USER | DESCRIPTION | ROLES | PERMS | EXPIRES | + |----------------|--------------------------------------|-------|----------------------|-------------|-------|-------|---------------------------------| + | TOKEN_TYPE_API | a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 | | admin@metal-stack.io | ci token | 0 | 0 | 2000-01-02 00:00:00 UTC (in 1d) | + | TOKEN_TYPE_API | b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 | | dev@metal-stack.io | dev token | 0 | 0 | 2000-01-03 00:00:00 UTC (in 2d) | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminTokenCmd_Delete(t *testing.T) { + tests := []*e2e.Test[adminv2.TokenServiceRevokeResponse, *apiv2.Token]{ + { + Name: "delete", + CmdArgs: []string{"admin", "token", "delete", testresources.Token1().Uuid, "--user", "user-123"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: adminv2.TokenServiceRevokeRequest{ + Uuid: testresources.Token1().Uuid, + User: "user-123", + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.TokenServiceRevokeResponse{}) + }, + }, + }, + }), + WantObject: &apiv2.Token{ + Uuid: testresources.Token1().Uuid, + }, + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/pkg/tests/e2e/api/admin_test.go b/pkg/tests/e2e/api/admin_test.go deleted file mode 100644 index ba9b2b5..0000000 --- a/pkg/tests/e2e/api/admin_test.go +++ /dev/null @@ -1,553 +0,0 @@ -package api_e2e - -import ( - "testing" - "time" - - "connectrpc.com/connect" - adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" - apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" - "google.golang.org/protobuf/types/known/durationpb" - "google.golang.org/protobuf/types/known/timestamppb" -) - -var ( - adminTenant1 = func() *apiv2.Tenant { - return &apiv2.Tenant{ - Login: "metal-stack", - Name: "Metal Stack", - Email: "info@metal-stack.io", - Description: "a tenant", - Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - }, - } - } - adminTenant2 = func() *apiv2.Tenant { - return &apiv2.Tenant{ - Login: "acme-corp", - Name: "ACME Corp", - Email: "admin@acme.io", - Description: "another tenant", - Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - }, - } - } -) - -func Test_AdminTenantCmd_Create(t *testing.T) { - tests := []*e2e.Test[adminv2.TenantServiceCreateResponse, *apiv2.Tenant]{ - { - Name: "create", - CmdArgs: []string{"admin", "tenant", "create", "--name", adminTenant1().Name, "--description", adminTenant1().Description, "--email", adminTenant1().Email}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.TenantServiceCreateRequest{ - Name: adminTenant1().Name, - Description: new(adminTenant1().Description), - Email: new(adminTenant1().Email), - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.TenantServiceCreateResponse{ - Tenant: adminTenant1(), - }) - }, - }, - }, - }), - WantObject: adminTenant1(), - WantProtoObject: adminTenant1(), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} - -func Test_AdminTenantCmd_List(t *testing.T) { - tests := []*e2e.Test[adminv2.TenantServiceListResponse, apiv2.Tenant]{ - { - Name: "list", - CmdArgs: []string{"admin", "tenant", "list"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.TenantServiceListRequest{}, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.TenantServiceListResponse{ - Tenants: []*apiv2.Tenant{ - adminTenant1(), - adminTenant2(), - }, - }) - }, - }, - }, - }), - WantTable: new(` - ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS - metal-stack Metal Stack info@metal-stack.io now - - acme-corp ACME Corp admin@acme.io now - - `), - WantWideTable: new(` - ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS - metal-stack Metal Stack info@metal-stack.io now - - acme-corp ACME Corp admin@acme.io now - - `), - Template: new("{{ .login }} {{ .name }}"), - WantTemplate: new(` -metal-stack Metal Stack -acme-corp ACME Corp - `), - WantMarkdown: new(` - | ID | NAME | EMAIL | REGISTERED | COUPONS | TERMS AND CONDITIONS | - |-------------|-------------|---------------------|------------|---------|----------------------| - | metal-stack | Metal Stack | info@metal-stack.io | now | - | | - | acme-corp | ACME Corp | admin@acme.io | now | - | | - `), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} - -func Test_AdminTokenCmd_List(t *testing.T) { - tests := []*e2e.Test[adminv2.TokenServiceListResponse, apiv2.Token]{ - { - Name: "list", - CmdArgs: []string{"admin", "token", "list"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.TokenServiceListRequest{}, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.TokenServiceListResponse{ - Tokens: []*apiv2.Token{ - token1(), - token2(), - }, - }) - }, - }, - }, - }), - WantTable: new(` - TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES - TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) - TOKEN_TYPE_API b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev@metal-stack.io dev token 0 0 2000-01-03 00:00:00 UTC (in 2d) - `), - WantWideTable: new(` - TYPE ID ADMIN USER DESCRIPTION ROLES PERMS EXPIRES - TOKEN_TYPE_API a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 admin@metal-stack.io ci token 0 0 2000-01-02 00:00:00 UTC (in 1d) - TOKEN_TYPE_API b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev@metal-stack.io dev token 0 0 2000-01-03 00:00:00 UTC (in 2d) - `), - Template: new("{{ .uuid }} {{ .description }}"), - WantTemplate: new(` -a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token -b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 dev token - `), - WantMarkdown: new(` - | TYPE | ID | ADMIN | USER | DESCRIPTION | ROLES | PERMS | EXPIRES | - |----------------|--------------------------------------|-------|----------------------|-------------|-------|-------|---------------------------------| - | TOKEN_TYPE_API | a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 | | admin@metal-stack.io | ci token | 0 | 0 | 2000-01-02 00:00:00 UTC (in 1d) | - | TOKEN_TYPE_API | b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01 | | dev@metal-stack.io | dev token | 0 | 0 | 2000-01-03 00:00:00 UTC (in 2d) | - `), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} - -func Test_AdminTokenCmd_Delete(t *testing.T) { - tests := []*e2e.Test[adminv2.TokenServiceRevokeResponse, *apiv2.Token]{ - { - Name: "delete", - CmdArgs: []string{"admin", "token", "delete", token1().Uuid, "--user", "user-123"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.TokenServiceRevokeRequest{ - Uuid: token1().Uuid, - User: "user-123", - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.TokenServiceRevokeResponse{}) - }, - }, - }, - }), - WantObject: &apiv2.Token{ - Uuid: token1().Uuid, - }, - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} - -func Test_AdminProjectCmd_List(t *testing.T) { - tests := []*e2e.Test[adminv2.ProjectServiceListResponse, apiv2.Project]{ - { - Name: "list", - CmdArgs: []string{"admin", "project", "list"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.ProjectServiceListRequest{}, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.ProjectServiceListResponse{ - Projects: []*apiv2.Project{ - project1(), - project2(), - }, - }) - }, - }, - }, - }), - WantTable: new(` - ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC - f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC - `), - WantWideTable: new(` - ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC - f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC - `), - Template: new("{{ .uuid }} {{ .name }}"), - WantTemplate: new(` -0d81bca7-73f6-4da3-8397-4a8c52a0c583 project-a -f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c project-b - `), - WantMarkdown: new(` - | ID | TENANT | NAME | DESCRIPTION | CREATION DATE | - |--------------------------------------|-------------|-----------|----------------|-------------------------| - | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | - | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | metal-stack | project-b | second project | 2025-07-15 14:30:00 UTC | - `), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} - -var ( - component1 = func() *apiv2.Component { - return &apiv2.Component{ - Uuid: "c1a2b3d4-e5f6-7890-abcd-ef1234567890", - Type: apiv2.ComponentType_COMPONENT_TYPE_METAL_CORE, - Identifier: "metal-core-1", - StartedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - ReportedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - Interval: durationpb.New(10 * time.Second), - Version: &apiv2.Version{ - Version: "v1.0.0", - }, - Token: &apiv2.Token{ - Uuid: "t1a2b3d4-e5f6-7890-abcd-ef1234567890", - Expires: timestamppb.New(time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC)), - }, - } - } - component2 = func() *apiv2.Component { - return &apiv2.Component{ - Uuid: "d2b3c4e5-f6a7-8901-bcde-f12345678901", - Type: apiv2.ComponentType_COMPONENT_TYPE_PIXIECORE, - Identifier: "pixiecore-1", - StartedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - ReportedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - Interval: durationpb.New(10 * time.Second), - Version: &apiv2.Version{ - Version: "v2.0.0", - }, - Token: &apiv2.Token{ - Uuid: "t2b3c4e5-f6a7-8901-bcde-f12345678901", - Expires: timestamppb.New(time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC)), - }, - } - } -) - -func Test_AdminComponentCmd_Describe(t *testing.T) { - tests := []*e2e.Test[adminv2.ComponentServiceGetResponse, *apiv2.Component]{ - { - Name: "describe", - CmdArgs: []string{"admin", "component", "describe", component1().Uuid}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.ComponentServiceGetRequest{ - Uuid: component1().Uuid, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.ComponentServiceGetResponse{ - Component: component1(), - }) - }, - }, - }, - }), - WantObject: component1(), - WantProtoObject: component1(), - WantTable: new(` - ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN - c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d - `), - WantWideTable: new(` - ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN - c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d - `), - Template: new("{{ .uuid }} {{ .identifier }}"), - WantTemplate: new(` - c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core-1 - `), - WantMarkdown: new(` - | ID | TYPE | IDENTIFIER | STARTED | AGE | VERSION | TOKEN | TOKEN EXPIRES IN | - |--------------------------------------|------------|--------------|---------|-----|---------|--------------------------------------|------------------| - | c1a2b3d4-e5f6-7890-abcd-ef1234567890 | metal-core | metal-core-1 | 0s | 0s | v1.0.0 | t1a2b3d4-e5f6-7890-abcd-ef1234567890 | 1d | - `), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} - -func Test_AdminComponentCmd_List(t *testing.T) { - tests := []*e2e.Test[adminv2.ComponentServiceListResponse, apiv2.Component]{ - { - Name: "list", - CmdArgs: []string{"admin", "component", "list"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.ComponentServiceListRequest{ - Query: &apiv2.ComponentQuery{}, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.ComponentServiceListResponse{ - Components: []*apiv2.Component{ - component1(), - component2(), - }, - }) - }, - }, - }, - }), - WantTable: new(` - ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN - c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d - d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore pixiecore-1 0s 0s v2.0.0 t2b3c4e5-f6a7-8901-bcde-f12345678901 2d - `), - WantWideTable: new(` - ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN - c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core metal-core-1 0s 0s v1.0.0 t1a2b3d4-e5f6-7890-abcd-ef1234567890 1d - d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore pixiecore-1 0s 0s v2.0.0 t2b3c4e5-f6a7-8901-bcde-f12345678901 2d - `), - Template: new("{{ .uuid }} {{ .identifier }}"), - WantTemplate: new(` -c1a2b3d4-e5f6-7890-abcd-ef1234567890 metal-core-1 -d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore-1 - `), - WantMarkdown: new(` - | ID | TYPE | IDENTIFIER | STARTED | AGE | VERSION | TOKEN | TOKEN EXPIRES IN | - |--------------------------------------|------------|--------------|---------|-----|---------|--------------------------------------|------------------| - | c1a2b3d4-e5f6-7890-abcd-ef1234567890 | metal-core | metal-core-1 | 0s | 0s | v1.0.0 | t1a2b3d4-e5f6-7890-abcd-ef1234567890 | 1d | - | d2b3c4e5-f6a7-8901-bcde-f12345678901 | pixiecore | pixiecore-1 | 0s | 0s | v2.0.0 | t2b3c4e5-f6a7-8901-bcde-f12345678901 | 2d | - `), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} - -func Test_AdminComponentCmd_Delete(t *testing.T) { - tests := []*e2e.Test[adminv2.ComponentServiceDeleteResponse, *apiv2.Component]{ - { - Name: "delete", - CmdArgs: []string{"admin", "component", "delete", component1().Uuid}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.ComponentServiceDeleteRequest{ - Uuid: component1().Uuid, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.ComponentServiceDeleteResponse{ - Component: component1(), - }) - }, - }, - }, - }), - WantObject: component1(), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} - -var ( - switch1 = func() *apiv2.Switch { - return &apiv2.Switch{ - Id: "leaf01", - Partition: "fra-equ01", - Rack: new("rack-1"), - Description: "leaf switch 1", - ManagementIp: "10.0.0.1", - ManagementUser: new("admin"), - Os: &apiv2.SwitchOS{ - Vendor: apiv2.SwitchOSVendor_SWITCH_OS_VENDOR_SONIC, - Version: "4.2.0", - MetalCoreVersion: "v0.9.1 (abc1234), tags/v0.9.1", - }, - LastSync: &apiv2.SwitchSync{ - Time: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - Duration: durationpb.New(100 * time.Millisecond), - }, - Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - }, - } - } - switch2 = func() *apiv2.Switch { - return &apiv2.Switch{ - Id: "leaf02", - Partition: "fra-equ01", - Rack: new("rack-1"), - Description: "leaf switch 2", - ManagementIp: "10.0.0.2", - ManagementUser: new("admin"), - Os: &apiv2.SwitchOS{ - Vendor: apiv2.SwitchOSVendor_SWITCH_OS_VENDOR_SONIC, - Version: "4.2.0", - MetalCoreVersion: "v0.9.1 (abc1234), tags/v0.9.1", - }, - LastSync: &apiv2.SwitchSync{ - Time: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - Duration: durationpb.New(200 * time.Millisecond), - }, - Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - }, - } - } -) - -func Test_AdminSwitchCmd_Describe(t *testing.T) { - tests := []*e2e.Test[adminv2.SwitchServiceGetResponse, *apiv2.Switch]{ - { - Name: "describe", - CmdArgs: []string{"admin", "switch", "describe", switch1().Id}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.SwitchServiceGetRequest{ - Id: switch1().Id, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.SwitchServiceGetResponse{ - Switch: switch1(), - }) - }, - }, - }, - }), - WantObject: switch1(), - WantProtoObject: switch1(), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} - -func Test_AdminSwitchCmd_List(t *testing.T) { - tests := []*e2e.Test[adminv2.SwitchServiceListResponse, apiv2.Switch]{ - { - Name: "list", - CmdArgs: []string{"admin", "switch", "list"}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.SwitchServiceListRequest{ - Query: &apiv2.SwitchQuery{ - Os: &apiv2.SwitchOSQuery{}, - }, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.SwitchServiceListResponse{ - Switches: []*apiv2.Switch{ - switch1(), - switch2(), - }, - }) - }, - }, - }, - }), - WantTable: new(` - ID PARTITION RACK OS STATUS LAST SYNC - leaf01 fra-equ01 rack-1 🦔 ● - leaf02 fra-equ01 rack-1 🦔 ● - `), - WantWideTable: new(` - ID PARTITION RACK OS METALCORE IP MODE LAST SYNC SYNC DURATION LAST ERROR - leaf01 fra-equ01 rack-1 SONiC (4.2.0) v0.9.1 (abc1234) 10.0.0.1 operational 100ms - leaf02 fra-equ01 rack-1 SONiC (4.2.0) v0.9.1 (abc1234) 10.0.0.2 operational 200ms - `), - Template: new("{{ .id }} {{ .partition }}"), - WantTemplate: new(` -leaf01 fra-equ01 -leaf02 fra-equ01 - `), - WantMarkdown: new(` - | ID | PARTITION | RACK | OS | STATUS | LAST SYNC | - |--------|-----------|--------|----|--------|-----------| - | leaf01 | fra-equ01 | rack-1 | 🦔 | ● | | - | leaf02 | fra-equ01 | rack-1 | 🦔 | ● | | - `), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} - -func Test_AdminSwitchCmd_Delete(t *testing.T) { - tests := []*e2e.Test[adminv2.SwitchServiceDeleteResponse, *apiv2.Switch]{ - { - Name: "delete", - CmdArgs: []string{"admin", "switch", "delete", switch1().Id}, - NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ - ClientCalls: []e2e.ClientCall{ - { - WantRequest: adminv2.SwitchServiceDeleteRequest{ - Id: switch1().Id, - }, - WantResponse: func() connect.AnyResponse { - return connect.NewResponse(&adminv2.SwitchServiceDeleteResponse{ - Switch: switch1(), - }) - }, - }, - }, - }), - WantObject: switch1(), - }, - } - for _, tt := range tests { - tt.TestCmd(t) - } -} diff --git a/pkg/tests/e2e/api/image_test.go b/pkg/tests/e2e/api/image_test.go index 890b374..e618682 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -6,27 +6,7 @@ import ( "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" -) - -var ( - image1 = func() *apiv2.Image { - return &apiv2.Image{ - Id: "ubuntu-24.04", - Name: new("Ubuntu 24.04"), - Description: new("Ubuntu 24.04 LTS"), - Features: []apiv2.ImageFeature{apiv2.ImageFeature_IMAGE_FEATURE_MACHINE}, - Classification: apiv2.ImageClassification_IMAGE_CLASSIFICATION_SUPPORTED, - } - } - image2 = func() *apiv2.Image { - return &apiv2.Image{ - Id: "firewall-3.0", - Name: new("Firewall 3.0"), - Description: new("Metal Firewall"), - Features: []apiv2.ImageFeature{apiv2.ImageFeature_IMAGE_FEATURE_FIREWALL}, - Classification: apiv2.ImageClassification_IMAGE_CLASSIFICATION_PREVIEW, - } - } + "github.com/metal-stack/cli/pkg/tests/e2e/testresources" ) func Test_ImageCmd_List(t *testing.T) { @@ -43,8 +23,8 @@ func Test_ImageCmd_List(t *testing.T) { WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ImageServiceListResponse{ Images: []*apiv2.Image{ - image1(), - image2(), + testresources.Image1(), + testresources.Image2(), }, }) }, @@ -84,23 +64,23 @@ func Test_ImageCmd_Describe(t *testing.T) { tests := []*e2e.Test[apiv2.ImageServiceGetResponse, *apiv2.Image]{ { Name: "describe", - CmdArgs: []string{"image", "describe", image1().Id}, + CmdArgs: []string{"image", "describe", testresources.Image1().Id}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.ImageServiceGetRequest{ - Id: image1().Id, + Id: testresources.Image1().Id, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ImageServiceGetResponse{ - Image: image1(), + Image: testresources.Image1(), }) }, }, }, }), - WantObject: image1(), - WantProtoObject: image1(), + WantObject: testresources.Image1(), + WantProtoObject: testresources.Image1(), WantTable: new(` ID NAME DESCRIPTION FEATURES EXPIRATION STATUS ubuntu-24.04 Ubuntu 24.04 Ubuntu 24.04 LTS machine supported diff --git a/pkg/tests/e2e/api/ip_test.go b/pkg/tests/e2e/api/ip_test.go index 7588fe6..16a6480 100644 --- a/pkg/tests/e2e/api/ip_test.go +++ b/pkg/tests/e2e/api/ip_test.go @@ -7,66 +7,27 @@ import ( "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" - "github.com/metal-stack/metal-lib/pkg/tag" + "github.com/metal-stack/cli/pkg/tests/e2e/testresources" "github.com/spf13/afero" "github.com/stretchr/testify/require" ) -var ( - ip1 = func() *apiv2.IP { - return &apiv2.IP{ - Uuid: "2e0144a2-09ef-42b7-b629-4263295db6e8", - Network: "internet", - Ip: "1.1.1.1", - Name: "a", - Description: "a description", - Project: "ce19a655-7933-4745-8f3e-9592b4a90488", - Type: apiv2.IPType_IP_TYPE_STATIC, - Meta: &apiv2.Meta{ - Labels: &apiv2.Labels{ - Labels: map[string]string{ - tag.ClusterServiceFQN: "/default/ingress-nginx", - }, - }, - }, - } - } - ip2 = func() *apiv2.IP { - return &apiv2.IP{ - Uuid: "9cef40ec-29c6-4dfa-aee8-47ee1f49223d", - Network: "internet", - Ip: "4.3.2.1", - Name: "b", - Description: "b description", - Project: "46bdfc45-9c8d-4268-b359-b40e3079d384", - Type: apiv2.IPType_IP_TYPE_EPHEMERAL, - Meta: &apiv2.Meta{ - Labels: &apiv2.Labels{ - Labels: map[string]string{ - "a": "b", - }, - }, - }, - } - } -) - func Test_IPCmd_List(t *testing.T) { tests := []*e2e.Test[apiv2.IPServiceListResponse, apiv2.IP]{ { Name: "list", - CmdArgs: []string{"ip", "list", "--project", ip1().Project}, + CmdArgs: []string{"ip", "list", "--project", testresources.IP1().Project}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.IPServiceListRequest{ - Project: ip1().Project, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceListResponse{ Ips: []*apiv2.IP{ - ip1(), - ip2(), + testresources.IP1(), + testresources.IP2(), }, }) }, @@ -105,24 +66,24 @@ func Test_IPCmd_Describe(t *testing.T) { tests := []*e2e.Test[apiv2.IPServiceGetResponse, *apiv2.IP]{ { Name: "describe", - CmdArgs: []string{"ip", "describe", "--project", ip1().Project, ip1().Ip}, + CmdArgs: []string{"ip", "describe", "--project", testresources.IP1().Project, testresources.IP1().Ip}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, }, }), - WantObject: ip1(), - WantProtoObject: ip1(), + WantObject: testresources.IP1(), + WantProtoObject: testresources.IP1(), WantTable: new(` IP PROJECT ID TYPE NAME ATTACHED SERVICE 1.1.1.1 ce19a655-7933-4745-8f3e-9592b4a90488 2e0144a2-09ef-42b7-b629-4263295db6e8 static a @@ -151,24 +112,24 @@ func Test_IPCmd_Create(t *testing.T) { tests := []*e2e.Test[apiv2.IPServiceGetResponse, *apiv2.IP]{ { Name: "create", - CmdArgs: []string{"ip", "create", "--project", ip1().Project, "--network", ip1().Network, "--static=true"}, + CmdArgs: []string{"ip", "create", "--project", testresources.IP1().Project, "--network", testresources.IP1().Network, "--static=true"}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.IPServiceCreateRequest{ - Project: ip1().Project, - Network: ip1().Network, - Type: &ip1().Type, + Project: testresources.IP1().Project, + Network: testresources.IP1().Network, + Type: &testresources.IP1().Type, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceCreateResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, }, }), - WantObject: ip1(), + WantObject: testresources.IP1(), }, { Name: "create from file", @@ -176,34 +137,34 @@ func Test_IPCmd_Create(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { - require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.IP1()), 0755)) }, ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, { WantRequest: apiv2.IPServiceCreateRequest{ - Ip: &ip1().Ip, - Project: ip1().Project, - Network: ip1().Network, - Name: &ip1().Name, - Description: &ip1().Description, - Labels: ip1().Meta.Labels, - Type: &ip1().Type, + Ip: &testresources.IP1().Ip, + Project: testresources.IP1().Project, + Network: testresources.IP1().Network, + Name: &testresources.IP1().Name, + Description: &testresources.IP1().Description, + Labels: testresources.IP1().Meta.Labels, + Type: &testresources.IP1().Type, AddressFamily: nil, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceCreateResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, @@ -224,23 +185,23 @@ func Test_IPCmd_Delete(t *testing.T) { tests := []*e2e.Test[apiv2.IPServiceDeleteResponse, *apiv2.IP]{ { Name: "delete", - CmdArgs: []string{"ip", "delete", "--project", ip1().Project, ip1().Ip}, + CmdArgs: []string{"ip", "delete", "--project", testresources.IP1().Project, testresources.IP1().Ip}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.IPServiceDeleteRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceDeleteResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, }, }), - WantObject: ip1(), + WantObject: testresources.IP1(), }, { Name: "delete from file", @@ -248,28 +209,28 @@ func Test_IPCmd_Delete(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { - require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.IP1()), 0755)) }, ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, { WantRequest: apiv2.IPServiceDeleteRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceDeleteResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, @@ -291,56 +252,56 @@ func Test_IPCmd_Update(t *testing.T) { tests := []*e2e.Test[apiv2.IPServiceDeleteResponse, *apiv2.IP]{ { Name: "update", - CmdArgs: []string{"ip", "update", "--project", ip1().Project, ip1().Ip, "--name", "foo"}, + CmdArgs: []string{"ip", "update", "--project", testresources.IP1().Project, testresources.IP1().Ip, "--name", "foo"}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ // TODO: the client gets the IP two times? { WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, { WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, { WantRequest: apiv2.IPServiceUpdateRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, Name: new("foo"), // TODO: these fields do not need to be sent? - Description: &ip1().Description, + Description: &testresources.IP1().Description, Labels: &apiv2.UpdateLabels{ Update: &apiv2.Labels{}, }, - Type: &ip1().Type, + Type: &testresources.IP1().Type, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, }, }, ), - WantObject: ip1(), + WantObject: testresources.IP1(), }, { Name: "update from file", @@ -348,34 +309,34 @@ func Test_IPCmd_Update(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { - require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.IP1()), 0755)) }, ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, { WantRequest: apiv2.IPServiceUpdateRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - Description: &ip1().Description, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, + Description: &testresources.IP1().Description, Labels: &apiv2.UpdateLabels{ Update: &apiv2.Labels{}, }, - Name: &ip1().Name, - Type: &ip1().Type, + Name: &testresources.IP1().Name, + Type: &testresources.IP1().Type, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, @@ -401,34 +362,34 @@ func Test_IPCmd_Apply(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { - require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.IP1()), 0755)) }, ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, { WantRequest: apiv2.IPServiceCreateRequest{ - Ip: &ip1().Ip, - Project: ip1().Project, - Network: ip1().Network, - Name: &ip1().Name, - Description: &ip1().Description, - Labels: ip1().Meta.Labels, - Type: &ip1().Type, + Ip: &testresources.IP1().Ip, + Project: testresources.IP1().Project, + Network: testresources.IP1().Network, + Name: &testresources.IP1().Name, + Description: &testresources.IP1().Description, + Labels: testresources.IP1().Meta.Labels, + Type: &testresources.IP1().Type, AddressFamily: nil, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceCreateResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, @@ -446,58 +407,58 @@ func Test_IPCmd_Apply(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { - require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, ip1()), 0755)) + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.IP1()), 0755)) }, ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, { WantRequest: apiv2.IPServiceCreateRequest{ - Ip: &ip1().Ip, - Project: ip1().Project, - Network: ip1().Network, - Name: &ip1().Name, - Description: &ip1().Description, - Labels: ip1().Meta.Labels, - Type: &ip1().Type, + Ip: &testresources.IP1().Ip, + Project: testresources.IP1().Project, + Network: testresources.IP1().Network, + Name: &testresources.IP1().Name, + Description: &testresources.IP1().Description, + Labels: testresources.IP1().Meta.Labels, + Type: &testresources.IP1().Type, AddressFamily: nil, }, WantError: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("already exists")), }, { WantRequest: apiv2.IPServiceGetRequest{ - Ip: ip1().Ip, - Project: ip1().Project, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceGetResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, { WantRequest: apiv2.IPServiceUpdateRequest{ - Ip: ip1().Ip, - Project: ip1().Project, - Description: &ip1().Description, + Ip: testresources.IP1().Ip, + Project: testresources.IP1().Project, + Description: &testresources.IP1().Description, Labels: &apiv2.UpdateLabels{ Update: &apiv2.Labels{}, }, - Name: &ip1().Name, - Type: &ip1().Type, + Name: &testresources.IP1().Name, + Type: &testresources.IP1().Type, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.IPServiceUpdateResponse{ - Ip: ip1(), + Ip: testresources.IP1(), }) }, }, diff --git a/pkg/tests/e2e/api/project_test.go b/pkg/tests/e2e/api/project_test.go index e27ea67..09952ac 100644 --- a/pkg/tests/e2e/api/project_test.go +++ b/pkg/tests/e2e/api/project_test.go @@ -2,62 +2,36 @@ package api_e2e import ( "testing" - "time" "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/metal-stack/cli/pkg/tests/e2e/testresources" "github.com/spf13/afero" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/timestamppb" -) - -var ( - project1 = func() *apiv2.Project { - return &apiv2.Project{ - Uuid: "0d81bca7-73f6-4da3-8397-4a8c52a0c583", - Name: "project-a", - Description: "first project", - Tenant: "metal-stack", - Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2025, 6, 1, 10, 0, 0, 0, time.UTC)), - }, - } - } - project2 = func() *apiv2.Project { - return &apiv2.Project{ - Uuid: "f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c", - Name: "project-b", - Description: "second project", - Tenant: "metal-stack", - Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2025, 7, 15, 14, 30, 0, 0, time.UTC)), - }, - } - } ) func Test_ProjectCmd_Describe(t *testing.T) { tests := []*e2e.Test[apiv2.ProjectServiceGetResponse, *apiv2.Project]{ { Name: "describe", - CmdArgs: []string{"project", "describe", project1().Uuid}, + CmdArgs: []string{"project", "describe", testresources.Project1().Uuid}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.ProjectServiceGetRequest{ - Project: project1().Uuid, + Project: testresources.Project1().Uuid, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ProjectServiceGetResponse{ - Project: project1(), + Project: testresources.Project1(), }) }, }, }, }), - WantObject: project1(), - WantProtoObject: project1(), + WantObject: testresources.Project1(), + WantProtoObject: testresources.Project1(), WantTable: new(` ID TENANT NAME DESCRIPTION CREATION DATE 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC @@ -86,43 +60,43 @@ func Test_ProjectCmd_Create(t *testing.T) { tests := []*e2e.Test[apiv2.ProjectServiceCreateResponse, *apiv2.Project]{ { Name: "create", - CmdArgs: []string{"project", "create", "--name", project1().Name, "--description", project1().Description, "--tenant", project1().Tenant}, + CmdArgs: []string{"project", "create", "--name", testresources.Project1().Name, "--description", testresources.Project1().Description, "--tenant", testresources.Project1().Tenant}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.ProjectServiceCreateRequest{ - Login: project1().Tenant, - Name: project1().Name, - Description: project1().Description, + Login: testresources.Project1().Tenant, + Name: testresources.Project1().Name, + Description: testresources.Project1().Description, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ProjectServiceCreateResponse{ - Project: project1(), + Project: testresources.Project1(), }) }, }, }, }), - WantObject: project1(), - WantProtoObject: project1(), + WantObject: testresources.Project1(), + WantProtoObject: testresources.Project1(), }, { Name: "create from file", CmdArgs: append([]string{"project", "create"}, e2e.AppendFromFileCommonArgs()...), NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { - require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, project1()), 0755)) + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Project1()), 0755)) }, ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.ProjectServiceCreateRequest{ - Login: project1().Tenant, - Name: project1().Name, - Description: project1().Description, + Login: testresources.Project1().Tenant, + Name: testresources.Project1().Name, + Description: testresources.Project1().Description, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ProjectServiceCreateResponse{ - Project: project1(), + Project: testresources.Project1(), }) }, }, @@ -143,38 +117,38 @@ func Test_ProjectCmd_Delete(t *testing.T) { tests := []*e2e.Test[apiv2.ProjectServiceDeleteResponse, *apiv2.Project]{ { Name: "delete", - CmdArgs: []string{"project", "delete", project1().Uuid}, + CmdArgs: []string{"project", "delete", testresources.Project1().Uuid}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.ProjectServiceDeleteRequest{ - Project: project1().Uuid, + Project: testresources.Project1().Uuid, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ProjectServiceDeleteResponse{ - Project: project1(), + Project: testresources.Project1(), }) }, }, }, }), - WantObject: project1(), + WantObject: testresources.Project1(), }, { Name: "delete from file", CmdArgs: append([]string{"project", "delete"}, e2e.AppendFromFileCommonArgs()...), NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { - require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, project1()), 0755)) + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Project1()), 0755)) }, ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.ProjectServiceDeleteRequest{ - Project: project1().Uuid, + Project: testresources.Project1().Uuid, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ProjectServiceDeleteResponse{ - Project: project1(), + Project: testresources.Project1(), }) }, }, @@ -195,42 +169,42 @@ func Test_ProjectCmd_Update(t *testing.T) { tests := []*e2e.Test[apiv2.ProjectServiceUpdateResponse, *apiv2.Project]{ { Name: "update", - CmdArgs: []string{"project", "update", project1().Uuid, "--name", "new-name", "--description", "new-desc"}, + CmdArgs: []string{"project", "update", testresources.Project1().Uuid, "--name", "new-name", "--description", "new-desc"}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.ProjectServiceUpdateRequest{ - Project: project1().Uuid, + Project: testresources.Project1().Uuid, Name: new("new-name"), Description: new("new-desc"), }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ProjectServiceUpdateResponse{ - Project: project1(), + Project: testresources.Project1(), }) }, }, }, }), - WantObject: project1(), + WantObject: testresources.Project1(), }, { Name: "update from file", CmdArgs: append([]string{"project", "update"}, e2e.AppendFromFileCommonArgs()...), NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ FsMocks: func(fs *afero.Afero) { - require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, project1()), 0755)) + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Project1()), 0755)) }, ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.ProjectServiceUpdateRequest{ - Project: project1().Uuid, - Name: new(project1().Name), - Description: new(project1().Description), + Project: testresources.Project1().Uuid, + Name: new(testresources.Project1().Name), + Description: new(testresources.Project1().Description), }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ProjectServiceUpdateResponse{ - Project: project1(), + Project: testresources.Project1(), }) }, }, @@ -259,8 +233,8 @@ func Test_ProjectCmd_List(t *testing.T) { WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ProjectServiceListResponse{ Projects: []*apiv2.Project{ - project1(), - project2(), + testresources.Project1(), + testresources.Project2(), }, }) }, diff --git a/pkg/tests/e2e/api/tenant_test.go b/pkg/tests/e2e/api/tenant_test.go index 2a45d41..a89c9ff 100644 --- a/pkg/tests/e2e/api/tenant_test.go +++ b/pkg/tests/e2e/api/tenant_test.go @@ -2,53 +2,27 @@ package api_e2e import ( "testing" - "time" "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" - "google.golang.org/protobuf/types/known/timestamppb" -) - -var ( - tenant1 = func() *apiv2.Tenant { - return &apiv2.Tenant{ - Login: "metal-stack", - Name: "Metal Stack", - Email: "info@metal-stack.io", - Description: "a tenant", - Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - }, - } - } - tenant2 = func() *apiv2.Tenant { - return &apiv2.Tenant{ - Login: "acme-corp", - Name: "ACME Corp", - Email: "admin@acme.io", - Description: "another tenant", - Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - }, - } - } + "github.com/metal-stack/cli/pkg/tests/e2e/testresources" ) func Test_TenantCmd_Describe(t *testing.T) { tests := []*e2e.Test[apiv2.TenantServiceGetResponse, *apiv2.Tenant]{ { Name: "describe", - CmdArgs: []string{"tenant", "describe", tenant1().Login}, + CmdArgs: []string{"tenant", "describe", testresources.Tenant1().Login}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.TenantServiceGetRequest{ - Login: tenant1().Login, + Login: testresources.Tenant1().Login, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.TenantServiceGetResponse{ - Tenant: tenant1(), + Tenant: testresources.Tenant1(), }) }, }, @@ -67,8 +41,8 @@ func Test_TenantCmd_Describe(t *testing.T) { |-------------|-------------|---------------------|------------|---------|----------------------| | metal-stack | Metal Stack | info@metal-stack.io | now | - | | `), - WantObject: tenant1(), - WantProtoObject: tenant1(), + WantObject: testresources.Tenant1(), + WantProtoObject: testresources.Tenant1(), Template: new("{{ .login }} {{ .name }}"), WantTemplate: new(` metal-stack Metal Stack @@ -92,8 +66,8 @@ func Test_TenantCmd_List(t *testing.T) { WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.TenantServiceListResponse{ Tenants: []*apiv2.Tenant{ - tenant1(), - tenant2(), + testresources.Tenant1(), + testresources.Tenant2(), }, }) }, diff --git a/pkg/tests/e2e/api/token_test.go b/pkg/tests/e2e/api/token_test.go index 8139f78..b91117e 100644 --- a/pkg/tests/e2e/api/token_test.go +++ b/pkg/tests/e2e/api/token_test.go @@ -2,59 +2,27 @@ package api_e2e import ( "testing" - "time" "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/pkg/tests/e2e" - "google.golang.org/protobuf/types/known/timestamppb" -) - -var ( - token1 = func() *apiv2.Token { - return &apiv2.Token{ - Uuid: "a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90", - User: "admin@metal-stack.io", - Description: "ci token", - TokenType: apiv2.TokenType_TOKEN_TYPE_API, - Expires: timestamppb.New(time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC)), - IssuedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - Permissions: nil, - Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - }, - } - } - token2 = func() *apiv2.Token { - return &apiv2.Token{ - Uuid: "b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01", - User: "dev@metal-stack.io", - Description: "dev token", - TokenType: apiv2.TokenType_TOKEN_TYPE_API, - Expires: timestamppb.New(time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC)), - IssuedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - Permissions: nil, - Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - }, - } - } + "github.com/metal-stack/cli/pkg/tests/e2e/testresources" ) func Test_TokenCmd_Describe(t *testing.T) { tests := []*e2e.Test[apiv2.TokenServiceGetResponse, *apiv2.Token]{ { Name: "describe", - CmdArgs: []string{"token", "describe", token1().Uuid}, + CmdArgs: []string{"token", "describe", testresources.Token1().Uuid}, NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { WantRequest: apiv2.TokenServiceGetRequest{ - Uuid: token1().Uuid, + Uuid: testresources.Token1().Uuid, }, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.TokenServiceGetResponse{ - Token: token1(), + Token: testresources.Token1(), }) }, }, @@ -73,8 +41,8 @@ func Test_TokenCmd_Describe(t *testing.T) { |----------------|--------------------------------------|-------|----------------------|-------------|-------|-------|---------------------------------| | TOKEN_TYPE_API | a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 | | admin@metal-stack.io | ci token | 0 | 0 | 2000-01-02 00:00:00 UTC (in 1d) | `), - WantObject: token1(), - WantProtoObject: token1(), + WantObject: testresources.Token1(), + WantProtoObject: testresources.Token1(), Template: new("{{ .uuid }} {{ .description }}"), WantTemplate: new(` a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90 ci token @@ -98,8 +66,8 @@ func Test_TokenCmd_List(t *testing.T) { WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.TokenServiceListResponse{ Tokens: []*apiv2.Token{ - token1(), - token2(), + testresources.Token1(), + testresources.Token2(), }, }) }, diff --git a/pkg/tests/e2e/testresources/images.go b/pkg/tests/e2e/testresources/images.go new file mode 100644 index 0000000..6adff27 --- /dev/null +++ b/pkg/tests/e2e/testresources/images.go @@ -0,0 +1,24 @@ +package testresources + +import apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + +var ( + Image1 = func() *apiv2.Image { + return &apiv2.Image{ + Id: "ubuntu-24.04", + Name: new("Ubuntu 24.04"), + Description: new("Ubuntu 24.04 LTS"), + Features: []apiv2.ImageFeature{apiv2.ImageFeature_IMAGE_FEATURE_MACHINE}, + Classification: apiv2.ImageClassification_IMAGE_CLASSIFICATION_SUPPORTED, + } + } + Image2 = func() *apiv2.Image { + return &apiv2.Image{ + Id: "firewall-3.0", + Name: new("Firewall 3.0"), + Description: new("Metal Firewall"), + Features: []apiv2.ImageFeature{apiv2.ImageFeature_IMAGE_FEATURE_FIREWALL}, + Classification: apiv2.ImageClassification_IMAGE_CLASSIFICATION_PREVIEW, + } + } +) diff --git a/pkg/tests/e2e/testresources/ips.go b/pkg/tests/e2e/testresources/ips.go new file mode 100644 index 0000000..5aef78e --- /dev/null +++ b/pkg/tests/e2e/testresources/ips.go @@ -0,0 +1,45 @@ +package testresources + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/tag" +) + +var ( + IP1 = func() *apiv2.IP { + return &apiv2.IP{ + Uuid: "2e0144a2-09ef-42b7-b629-4263295db6e8", + Network: "internet", + Ip: "1.1.1.1", + Name: "a", + Description: "a description", + Project: "ce19a655-7933-4745-8f3e-9592b4a90488", + Type: apiv2.IPType_IP_TYPE_STATIC, + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: map[string]string{ + tag.ClusterServiceFQN: "/default/ingress-nginx", + }, + }, + }, + } + } + IP2 = func() *apiv2.IP { + return &apiv2.IP{ + Uuid: "9cef40ec-29c6-4dfa-aee8-47ee1f49223d", + Network: "internet", + Ip: "4.3.2.1", + Name: "b", + Description: "b description", + Project: "46bdfc45-9c8d-4268-b359-b40e3079d384", + Type: apiv2.IPType_IP_TYPE_EPHEMERAL, + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "a": "b", + }, + }, + }, + } + } +) diff --git a/pkg/tests/e2e/testresources/projects.go b/pkg/tests/e2e/testresources/projects.go new file mode 100644 index 0000000..364265b --- /dev/null +++ b/pkg/tests/e2e/testresources/projects.go @@ -0,0 +1,33 @@ +package testresources + +import ( + "time" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + Project1 = func() *apiv2.Project { + return &apiv2.Project{ + Uuid: "0d81bca7-73f6-4da3-8397-4a8c52a0c583", + Name: "project-a", + Description: "first project", + Tenant: "metal-stack", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2025, 6, 1, 10, 0, 0, 0, time.UTC)), + }, + } + } + Project2 = func() *apiv2.Project { + return &apiv2.Project{ + Uuid: "f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c", + Name: "project-b", + Description: "second project", + Tenant: "metal-stack", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2025, 7, 15, 14, 30, 0, 0, time.UTC)), + }, + } + } +) diff --git a/pkg/tests/e2e/testresources/tenants.go b/pkg/tests/e2e/testresources/tenants.go new file mode 100644 index 0000000..43e4eae --- /dev/null +++ b/pkg/tests/e2e/testresources/tenants.go @@ -0,0 +1,33 @@ +package testresources + +import ( + "time" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + Tenant1 = func() *apiv2.Tenant { + return &apiv2.Tenant{ + Login: "metal-stack", + Name: "Metal Stack", + Email: "info@metal-stack.io", + Description: "a tenant", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } + Tenant2 = func() *apiv2.Tenant { + return &apiv2.Tenant{ + Login: "acme-corp", + Name: "ACME Corp", + Email: "admin@acme.io", + Description: "another tenant", + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } +) diff --git a/pkg/tests/e2e/testresources/tokens.go b/pkg/tests/e2e/testresources/tokens.go new file mode 100644 index 0000000..41b08fa --- /dev/null +++ b/pkg/tests/e2e/testresources/tokens.go @@ -0,0 +1,39 @@ +package testresources + +import ( + "time" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + Token1 = func() *apiv2.Token { + return &apiv2.Token{ + Uuid: "a3b1f6d2-4e8c-4f7a-9d2e-1b5c8f3a7e90", + User: "admin@metal-stack.io", + Description: "ci token", + TokenType: apiv2.TokenType_TOKEN_TYPE_API, + Expires: timestamppb.New(time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC)), + IssuedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Permissions: nil, + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } + Token2 = func() *apiv2.Token { + return &apiv2.Token{ + Uuid: "b4c2e7f3-5a9d-4b8e-a1c3-2d6f9e4b8a01", + User: "dev@metal-stack.io", + Description: "dev token", + TokenType: apiv2.TokenType_TOKEN_TYPE_API, + Expires: timestamppb.New(time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC)), + IssuedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Permissions: nil, + Meta: &apiv2.Meta{ + CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + }, + } + } +) From 8b1e31d553d6d7b797b2897107c8a9db34ca4949 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 15:40:54 +0100 Subject: [PATCH 29/34] Naming. --- pkg/tests/e2e/test_framework.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/tests/e2e/test_framework.go b/pkg/tests/e2e/test_framework.go index 3f7939c..5b006e8 100644 --- a/pkg/tests/e2e/test_framework.go +++ b/pkg/tests/e2e/test_framework.go @@ -129,13 +129,13 @@ func (c *Test[Response, RawObject]) assertExhaustiveArgs(t *testing.T) { }) } -func outputFormats[Response, Object any](c *Test[Response, Object]) []outputFormat { +func outputFormats[Response, RawObject any](c *Test[Response, RawObject]) []outputFormat { var formats []outputFormat if !pointer.IsZero(c.WantObject) { formats = append(formats, - &rawYamlOutputFormat[Object]{want: c.WantObject}, - &rawJsonOutputFormat[Object]{want: c.WantObject}, + &rawYamlOutputFormat[RawObject]{want: c.WantObject}, + &rawJsonOutputFormat[RawObject]{want: c.WantObject}, ) } From ed7ba4571517344378eb79950efa2a696a335456 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Mar 2026 15:48:09 +0100 Subject: [PATCH 30/34] Remove coupons. --- cmd/tableprinters/tenant.go | 17 +++------------ pkg/tests/e2e/admin/tenant_test.go | 20 +++++++++--------- pkg/tests/e2e/api/tenant_test.go | 34 +++++++++++++++--------------- 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/cmd/tableprinters/tenant.go b/cmd/tableprinters/tenant.go index 5e09727..5e26223 100644 --- a/cmd/tableprinters/tenant.go +++ b/cmd/tableprinters/tenant.go @@ -8,28 +8,17 @@ import ( func (t *TablePrinter) TenantTable(data []*apiv2.Tenant, wide bool) ([]string, [][]string, error) { var ( - rows [][]string + rows [][]string + header = []string{"ID", "Name", "Email", "Registered"} ) - header := []string{"ID", "Name", "Email", "Registered", "Coupons", "Terms And Conditions"} - if wide { - header = []string{"ID", "Name", "Email", "Registered", "Coupons", "Terms And Conditions"} - } - for _, tenant := range data { id := tenant.Login name := tenant.Name email := tenant.Email since := humanize.Time(tenant.Meta.CreatedAt.AsTime()) - coupons := "-" - couponsWide := coupons - termsAndConditions := "" - if wide { - rows = append(rows, []string{id, name, email, since, couponsWide, termsAndConditions}) - } else { - rows = append(rows, []string{id, name, email, since, coupons, termsAndConditions}) - } + rows = append(rows, []string{id, name, email, since}) } return header, rows, nil diff --git a/pkg/tests/e2e/admin/tenant_test.go b/pkg/tests/e2e/admin/tenant_test.go index 6be8d53..680bf85 100644 --- a/pkg/tests/e2e/admin/tenant_test.go +++ b/pkg/tests/e2e/admin/tenant_test.go @@ -87,14 +87,14 @@ func Test_AdminTenantCmd_List(t *testing.T) { }, }), WantTable: new(` - ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS - metal-stack Metal Stack info@metal-stack.io now - - acme-corp ACME Corp admin@acme.io now - + ID NAME EMAIL REGISTERED + metal-stack Metal Stack info@metal-stack.io now + acme-corp ACME Corp admin@acme.io now `), WantWideTable: new(` - ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS - metal-stack Metal Stack info@metal-stack.io now - - acme-corp ACME Corp admin@acme.io now - + ID NAME EMAIL REGISTERED + metal-stack Metal Stack info@metal-stack.io now + acme-corp ACME Corp admin@acme.io now `), Template: new("{{ .login }} {{ .name }}"), WantTemplate: new(` @@ -102,10 +102,10 @@ metal-stack Metal Stack acme-corp ACME Corp `), WantMarkdown: new(` - | ID | NAME | EMAIL | REGISTERED | COUPONS | TERMS AND CONDITIONS | - |-------------|-------------|---------------------|------------|---------|----------------------| - | metal-stack | Metal Stack | info@metal-stack.io | now | - | | - | acme-corp | ACME Corp | admin@acme.io | now | - | | + | ID | NAME | EMAIL | REGISTERED | + |-------------|-------------|---------------------|------------| + | metal-stack | Metal Stack | info@metal-stack.io | now | + | acme-corp | ACME Corp | admin@acme.io | now | `), }, } diff --git a/pkg/tests/e2e/api/tenant_test.go b/pkg/tests/e2e/api/tenant_test.go index a89c9ff..524586b 100644 --- a/pkg/tests/e2e/api/tenant_test.go +++ b/pkg/tests/e2e/api/tenant_test.go @@ -29,17 +29,17 @@ func Test_TenantCmd_Describe(t *testing.T) { }, }), WantTable: new(` - ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS - metal-stack Metal Stack info@metal-stack.io now - + ID NAME EMAIL REGISTERED + metal-stack Metal Stack info@metal-stack.io now `), WantWideTable: new(` - ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS - metal-stack Metal Stack info@metal-stack.io now - + ID NAME EMAIL REGISTERED + metal-stack Metal Stack info@metal-stack.io now `), WantMarkdown: new(` - | ID | NAME | EMAIL | REGISTERED | COUPONS | TERMS AND CONDITIONS | - |-------------|-------------|---------------------|------------|---------|----------------------| - | metal-stack | Metal Stack | info@metal-stack.io | now | - | | + | ID | NAME | EMAIL | REGISTERED | + |-------------|-------------|---------------------|------------| + | metal-stack | Metal Stack | info@metal-stack.io | now | `), WantObject: testresources.Tenant1(), WantProtoObject: testresources.Tenant1(), @@ -75,14 +75,14 @@ func Test_TenantCmd_List(t *testing.T) { }, }), WantTable: new(` - ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS - metal-stack Metal Stack info@metal-stack.io now - - acme-corp ACME Corp admin@acme.io now - + ID NAME EMAIL REGISTERED + metal-stack Metal Stack info@metal-stack.io now + acme-corp ACME Corp admin@acme.io now `), WantWideTable: new(` - ID NAME EMAIL REGISTERED COUPONS TERMS AND CONDITIONS - metal-stack Metal Stack info@metal-stack.io now - - acme-corp ACME Corp admin@acme.io now - + ID NAME EMAIL REGISTERED + metal-stack Metal Stack info@metal-stack.io now + acme-corp ACME Corp admin@acme.io now `), Template: new("{{ .login }} {{ .name }}"), WantTemplate: new(` @@ -90,10 +90,10 @@ metal-stack Metal Stack acme-corp ACME Corp `), WantMarkdown: new(` - | ID | NAME | EMAIL | REGISTERED | COUPONS | TERMS AND CONDITIONS | - |-------------|-------------|---------------------|------------|---------|----------------------| - | metal-stack | Metal Stack | info@metal-stack.io | now | - | | - | acme-corp | ACME Corp | admin@acme.io | now | - | | + | ID | NAME | EMAIL | REGISTERED | + |-------------|-------------|---------------------|------------| + | metal-stack | Metal Stack | info@metal-stack.io | now | + | acme-corp | ACME Corp | admin@acme.io | now | `), }, } From 964f8040858079e16484433fbf12739985beb221 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Fri, 20 Mar 2026 09:04:06 +0100 Subject: [PATCH 31/34] One more test. --- pkg/tests/e2e/api/project_test.go | 40 +++++++++++++++++++++++++++++++ pkg/tests/e2e/test_framework.go | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/pkg/tests/e2e/api/project_test.go b/pkg/tests/e2e/api/project_test.go index 09952ac..9a83f10 100644 --- a/pkg/tests/e2e/api/project_test.go +++ b/pkg/tests/e2e/api/project_test.go @@ -107,6 +107,46 @@ func Test_ProjectCmd_Create(t *testing.T) { 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC `), }, + { + Name: "create many from file", + CmdArgs: append([]string{"project", "create"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshalToMultiYAML(t, testresources.Project1(), testresources.Project2()), 0755)) + }, + ClientCalls: []e2e.ClientCall{ + { + WantRequest: apiv2.ProjectServiceCreateRequest{ + Login: testresources.Project1().Tenant, + Name: testresources.Project1().Name, + Description: testresources.Project1().Description, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceCreateResponse{ + Project: testresources.Project1(), + }) + }, + }, + { + WantRequest: apiv2.ProjectServiceCreateRequest{ + Login: testresources.Project2().Tenant, + Name: testresources.Project2().Name, + Description: testresources.Project2().Description, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.ProjectServiceCreateResponse{ + Project: testresources.Project2(), + }) + }, + }, + }, + }), + WantTable: new(` + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + `), + }, } for _, tt := range tests { tt.TestCmd(t) diff --git a/pkg/tests/e2e/test_framework.go b/pkg/tests/e2e/test_framework.go index 5b006e8..deba4ed 100644 --- a/pkg/tests/e2e/test_framework.go +++ b/pkg/tests/e2e/test_framework.go @@ -359,7 +359,7 @@ func MustMarshal(t *testing.T, d any) []byte { return b } -func MustMarshalToMultiYAML[R any](t *testing.T, data []R) []byte { +func MustMarshalToMultiYAML(t *testing.T, data ...any) []byte { var parts []string for _, elem := range data { parts = append(parts, string(MustMarshal(t, elem))) From 55f8a652768b30271a75240714770774e12e1e55 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Fri, 20 Mar 2026 09:36:14 +0100 Subject: [PATCH 32/34] Helper func. --- cmd/api/v2/ip.go | 5 ++--- pkg/helpers/errors.go | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 pkg/helpers/errors.go diff --git a/cmd/api/v2/ip.go b/cmd/api/v2/ip.go index 4cab305..dd9a45d 100644 --- a/cmd/api/v2/ip.go +++ b/cmd/api/v2/ip.go @@ -1,10 +1,8 @@ package v2 import ( - "errors" "fmt" - "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/cli/cmd/sorters" @@ -128,9 +126,10 @@ func (c *ip) Create(rq *apiv2.IPServiceCreateRequest) (*apiv2.IP, error) { resp, err := c.c.Client.Apiv2().IP().Create(ctx, rq) if err != nil { - if connectErr, ok := errors.AsType[*connect.Error](err); ok && connectErr.Code() == connect.CodeAlreadyExists { + if helpers.IsAlreadyExists(err) { return nil, genericcli.AlreadyExistsError() } + return nil, err } diff --git a/pkg/helpers/errors.go b/pkg/helpers/errors.go new file mode 100644 index 0000000..813c1da --- /dev/null +++ b/pkg/helpers/errors.go @@ -0,0 +1,15 @@ +package helpers + +import ( + "errors" + + "connectrpc.com/connect" +) + +func IsAlreadyExists(err error) bool { + if connectErr, ok := errors.AsType[*connect.Error](err); ok && connectErr.Code() == connect.CodeAlreadyExists { + return true + } + + return false +} From 8a97c27abbe2bedce51f71b0a0af17ca5eeb78d7 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Fri, 20 Mar 2026 10:02:05 +0100 Subject: [PATCH 33/34] Review. --- cmd/root.go | 2 +- pkg/tests/e2e/admin/component_test.go | 18 +++---- pkg/tests/e2e/admin/project_test.go | 22 ++++----- pkg/tests/e2e/admin/switch_test.go | 14 +++--- pkg/tests/e2e/admin/tenant_test.go | 9 ++-- pkg/tests/e2e/admin/token_test.go | 4 +- pkg/tests/e2e/api/health_test.go | 66 +++++++++++++++++++++++++ pkg/tests/e2e/api/image_test.go | 4 +- pkg/tests/e2e/api/ip_test.go | 38 +++++++------- pkg/tests/e2e/api/project_test.go | 58 +++++++++++----------- pkg/tests/e2e/api/tenant_test.go | 4 +- pkg/tests/e2e/api/token_test.go | 4 +- pkg/tests/e2e/test_framework.go | 5 ++ pkg/tests/e2e/test_interceptor.go | 3 +- pkg/tests/e2e/testresources/projects.go | 7 ++- pkg/tests/e2e/testresources/tenants.go | 7 ++- pkg/tests/e2e/testresources/tokens.go | 13 ++--- 17 files changed, 174 insertions(+), 104 deletions(-) create mode 100644 pkg/tests/e2e/api/health_test.go diff --git a/cmd/root.go b/cmd/root.go index fc27a8f..7e4cfd5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -57,7 +57,7 @@ func NewRootCmd(c *config.Config) *cobra.Command { }, } rootCmd.PersistentFlags().StringP("config", "c", "", "alternative config file path, (default is ~/.metal-stack/config.yaml)") - rootCmd.PersistentFlags().StringP("output-format", "o", "table", "output format (table|wide|markdown|json|yaml|template|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact.") + rootCmd.PersistentFlags().StringP("output-format", "o", "table", "output format (table|wide|markdown|json|yaml|template|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact (for apply, create, update, delete commands from file the raw output formatters must be used).") genericcli.Must(rootCmd.RegisterFlagCompletionFunc("output-format", cobra.FixedCompletions([]string{"table", "wide", "markdown", "json", "yaml", "template"}, cobra.ShellCompDirectiveNoFileComp))) diff --git a/pkg/tests/e2e/admin/component_test.go b/pkg/tests/e2e/admin/component_test.go index b75be84..b446b5f 100644 --- a/pkg/tests/e2e/admin/component_test.go +++ b/pkg/tests/e2e/admin/component_test.go @@ -18,15 +18,15 @@ var ( Uuid: "c1a2b3d4-e5f6-7890-abcd-ef1234567890", Type: apiv2.ComponentType_COMPONENT_TYPE_METAL_CORE, Identifier: "metal-core-1", - StartedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - ReportedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + StartedAt: timestamppb.New(e2e.TimeBubbleStartTime()), + ReportedAt: timestamppb.New(e2e.TimeBubbleStartTime()), Interval: durationpb.New(10 * time.Second), Version: &apiv2.Version{ Version: "v1.0.0", }, Token: &apiv2.Token{ Uuid: "t1a2b3d4-e5f6-7890-abcd-ef1234567890", - Expires: timestamppb.New(time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC)), + Expires: timestamppb.New(e2e.TimeBubbleStartTime().Add(24 * time.Hour)), }, } } @@ -35,15 +35,15 @@ var ( Uuid: "d2b3c4e5-f6a7-8901-bcde-f12345678901", Type: apiv2.ComponentType_COMPONENT_TYPE_PIXIECORE, Identifier: "pixiecore-1", - StartedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), - ReportedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + StartedAt: timestamppb.New(e2e.TimeBubbleStartTime()), + ReportedAt: timestamppb.New(e2e.TimeBubbleStartTime()), Interval: durationpb.New(10 * time.Second), Version: &apiv2.Version{ Version: "v2.0.0", }, Token: &apiv2.Token{ Uuid: "t2b3c4e5-f6a7-8901-bcde-f12345678901", - Expires: timestamppb.New(time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC)), + Expires: timestamppb.New(e2e.TimeBubbleStartTime().Add(48 * time.Hour)), }, } } @@ -57,7 +57,7 @@ func Test_AdminComponentCmd_Describe(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.ComponentServiceGetRequest{ + WantRequest: &adminv2.ComponentServiceGetRequest{ Uuid: component1().Uuid, }, WantResponse: func() connect.AnyResponse { @@ -102,7 +102,7 @@ func Test_AdminComponentCmd_List(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.ComponentServiceListRequest{ + WantRequest: &adminv2.ComponentServiceListRequest{ Query: &apiv2.ComponentQuery{}, }, WantResponse: func() connect.AnyResponse { @@ -152,7 +152,7 @@ func Test_AdminComponentCmd_Delete(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.ComponentServiceDeleteRequest{ + WantRequest: &adminv2.ComponentServiceDeleteRequest{ Uuid: component1().Uuid, }, WantResponse: func() connect.AnyResponse { diff --git a/pkg/tests/e2e/admin/project_test.go b/pkg/tests/e2e/admin/project_test.go index 56f7b82..dc83935 100644 --- a/pkg/tests/e2e/admin/project_test.go +++ b/pkg/tests/e2e/admin/project_test.go @@ -18,7 +18,7 @@ func Test_AdminProjectCmd_List(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.ProjectServiceListRequest{}, + WantRequest: &adminv2.ProjectServiceListRequest{}, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&adminv2.ProjectServiceListResponse{ Projects: []*apiv2.Project{ @@ -31,14 +31,14 @@ func Test_AdminProjectCmd_List(t *testing.T) { }, }), WantTable: new(` - ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC - f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2000-01-01 00:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2000-01-01 00:00:00 UTC `), WantWideTable: new(` - ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC - f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2000-01-01 00:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2000-01-01 00:00:00 UTC `), Template: new("{{ .uuid }} {{ .name }}"), WantTemplate: new(` @@ -46,10 +46,10 @@ func Test_AdminProjectCmd_List(t *testing.T) { f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c project-b `), WantMarkdown: new(` - | ID | TENANT | NAME | DESCRIPTION | CREATION DATE | - |--------------------------------------|-------------|-----------|----------------|-------------------------| - | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | - | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | metal-stack | project-b | second project | 2025-07-15 14:30:00 UTC | + | ID | TENANT | NAME | DESCRIPTION | CREATION DATE | + |--------------------------------------|-------------|-----------|----------------|-------------------------| + | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2000-01-01 00:00:00 UTC | + | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | metal-stack | project-b | second project | 2000-01-01 00:00:00 UTC | `), }, } diff --git a/pkg/tests/e2e/admin/switch_test.go b/pkg/tests/e2e/admin/switch_test.go index c9585ff..3a11668 100644 --- a/pkg/tests/e2e/admin/switch_test.go +++ b/pkg/tests/e2e/admin/switch_test.go @@ -27,11 +27,11 @@ var ( MetalCoreVersion: "v0.9.1 (abc1234), tags/v0.9.1", }, LastSync: &apiv2.SwitchSync{ - Time: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Time: timestamppb.New(e2e.TimeBubbleStartTime()), Duration: durationpb.New(100 * time.Millisecond), }, Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime()), }, } } @@ -49,11 +49,11 @@ var ( MetalCoreVersion: "v0.9.1 (abc1234), tags/v0.9.1", }, LastSync: &apiv2.SwitchSync{ - Time: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Time: timestamppb.New(e2e.TimeBubbleStartTime()), Duration: durationpb.New(200 * time.Millisecond), }, Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime()), }, } } @@ -67,7 +67,7 @@ func Test_AdminSwitchCmd_Describe(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.SwitchServiceGetRequest{ + WantRequest: &adminv2.SwitchServiceGetRequest{ Id: switch1().Id, }, WantResponse: func() connect.AnyResponse { @@ -95,7 +95,7 @@ func Test_AdminSwitchCmd_List(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.SwitchServiceListRequest{ + WantRequest: &adminv2.SwitchServiceListRequest{ Query: &apiv2.SwitchQuery{ Os: &apiv2.SwitchOSQuery{}, }, @@ -147,7 +147,7 @@ func Test_AdminSwitchCmd_Delete(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.SwitchServiceDeleteRequest{ + WantRequest: &adminv2.SwitchServiceDeleteRequest{ Id: switch1().Id, }, WantResponse: func() connect.AnyResponse { diff --git a/pkg/tests/e2e/admin/tenant_test.go b/pkg/tests/e2e/admin/tenant_test.go index 680bf85..be1446f 100644 --- a/pkg/tests/e2e/admin/tenant_test.go +++ b/pkg/tests/e2e/admin/tenant_test.go @@ -2,7 +2,6 @@ package admin_e2e import ( "testing" - "time" "connectrpc.com/connect" adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" @@ -19,7 +18,7 @@ var ( Email: "info@metal-stack.io", Description: "a tenant", Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime()), }, } } @@ -30,7 +29,7 @@ var ( Email: "admin@acme.io", Description: "another tenant", Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime()), }, } } @@ -44,7 +43,7 @@ func Test_AdminTenantCmd_Create(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.TenantServiceCreateRequest{ + WantRequest: &adminv2.TenantServiceCreateRequest{ Name: adminTenant1().Name, Description: new(adminTenant1().Description), Email: new(adminTenant1().Email), @@ -74,7 +73,7 @@ func Test_AdminTenantCmd_List(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.TenantServiceListRequest{}, + WantRequest: &adminv2.TenantServiceListRequest{}, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&adminv2.TenantServiceListResponse{ Tenants: []*apiv2.Tenant{ diff --git a/pkg/tests/e2e/admin/token_test.go b/pkg/tests/e2e/admin/token_test.go index f698983..1082b48 100644 --- a/pkg/tests/e2e/admin/token_test.go +++ b/pkg/tests/e2e/admin/token_test.go @@ -18,7 +18,7 @@ func Test_AdminTokenCmd_List(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.TokenServiceListRequest{}, + WantRequest: &adminv2.TokenServiceListRequest{}, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&adminv2.TokenServiceListResponse{ Tokens: []*apiv2.Token{ @@ -66,7 +66,7 @@ func Test_AdminTokenCmd_Delete(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: adminv2.TokenServiceRevokeRequest{ + WantRequest: &adminv2.TokenServiceRevokeRequest{ Uuid: testresources.Token1().Uuid, User: "user-123", }, diff --git a/pkg/tests/e2e/api/health_test.go b/pkg/tests/e2e/api/health_test.go new file mode 100644 index 0000000..9f1b0c4 --- /dev/null +++ b/pkg/tests/e2e/api/health_test.go @@ -0,0 +1,66 @@ +package api_e2e + +import ( + "testing" + + "connectrpc.com/connect" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" +) + +var ( + health1 = func() *apiv2.Health { + return &apiv2.Health{ + Services: []*apiv2.HealthStatus{ + { + Name: apiv2.Service_SERVICE_IPAM, + Status: apiv2.ServiceStatus_SERVICE_STATUS_HEALTHY, + Message: "i am healthy", + }, + }, + } + } +) + +func Test_HealthCmd(t *testing.T) { + tests := []*e2e.Test[apiv2.HealthServiceGetResponse, *apiv2.Health]{ + { + Name: "health", + CmdArgs: []string{"health"}, + NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ + ClientCalls: []e2e.ClientCall{ + { + WantRequest: &apiv2.HealthServiceGetRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.HealthServiceGetResponse{ + Health: health1(), + }) + }, + }, + }, + }), + WantTable: new(` + NAME MESSAGE + ✔ ipam i am healthy + `), + WantWideTable: new(` + NAME MESSAGE + ✔ ipam i am healthy + `), + WantMarkdown: new(` + | | NAME | MESSAGE | + |---|------|--------------| + | ✔ | ipam | i am healthy | + `), + WantObject: health1(), + WantProtoObject: health1(), + Template: new("{{ range $s := .services }}{{ $s.message }} {{ end }}"), + WantTemplate: new(` + i am healthy + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/pkg/tests/e2e/api/image_test.go b/pkg/tests/e2e/api/image_test.go index e618682..678fcbe 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/pkg/tests/e2e/api/image_test.go @@ -17,7 +17,7 @@ func Test_ImageCmd_List(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ImageServiceListRequest{ + WantRequest: &apiv2.ImageServiceListRequest{ Query: &apiv2.ImageQuery{}, }, WantResponse: func() connect.AnyResponse { @@ -68,7 +68,7 @@ func Test_ImageCmd_Describe(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ImageServiceGetRequest{ + WantRequest: &apiv2.ImageServiceGetRequest{ Id: testresources.Image1().Id, }, WantResponse: func() connect.AnyResponse { diff --git a/pkg/tests/e2e/api/ip_test.go b/pkg/tests/e2e/api/ip_test.go index 16a6480..4f702eb 100644 --- a/pkg/tests/e2e/api/ip_test.go +++ b/pkg/tests/e2e/api/ip_test.go @@ -20,7 +20,7 @@ func Test_IPCmd_List(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.IPServiceListRequest{ + WantRequest: &apiv2.IPServiceListRequest{ Project: testresources.IP1().Project, }, WantResponse: func() connect.AnyResponse { @@ -70,7 +70,7 @@ func Test_IPCmd_Describe(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.IPServiceGetRequest{ + WantRequest: &apiv2.IPServiceGetRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -116,7 +116,7 @@ func Test_IPCmd_Create(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.IPServiceCreateRequest{ + WantRequest: &apiv2.IPServiceCreateRequest{ Project: testresources.IP1().Project, Network: testresources.IP1().Network, Type: &testresources.IP1().Type, @@ -141,7 +141,7 @@ func Test_IPCmd_Create(t *testing.T) { }, ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.IPServiceGetRequest{ + WantRequest: &apiv2.IPServiceGetRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -152,7 +152,7 @@ func Test_IPCmd_Create(t *testing.T) { }, }, { - WantRequest: apiv2.IPServiceCreateRequest{ + WantRequest: &apiv2.IPServiceCreateRequest{ Ip: &testresources.IP1().Ip, Project: testresources.IP1().Project, Network: testresources.IP1().Network, @@ -189,7 +189,7 @@ func Test_IPCmd_Delete(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.IPServiceDeleteRequest{ + WantRequest: &apiv2.IPServiceDeleteRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -213,7 +213,7 @@ func Test_IPCmd_Delete(t *testing.T) { }, ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.IPServiceGetRequest{ + WantRequest: &apiv2.IPServiceGetRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -224,7 +224,7 @@ func Test_IPCmd_Delete(t *testing.T) { }, }, { - WantRequest: apiv2.IPServiceDeleteRequest{ + WantRequest: &apiv2.IPServiceDeleteRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -258,7 +258,7 @@ func Test_IPCmd_Update(t *testing.T) { ClientCalls: []e2e.ClientCall{ // TODO: the client gets the IP two times? { - WantRequest: apiv2.IPServiceGetRequest{ + WantRequest: &apiv2.IPServiceGetRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -269,7 +269,7 @@ func Test_IPCmd_Update(t *testing.T) { }, }, { - WantRequest: apiv2.IPServiceGetRequest{ + WantRequest: &apiv2.IPServiceGetRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -280,7 +280,7 @@ func Test_IPCmd_Update(t *testing.T) { }, }, { - WantRequest: apiv2.IPServiceUpdateRequest{ + WantRequest: &apiv2.IPServiceUpdateRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, Name: new("foo"), @@ -313,7 +313,7 @@ func Test_IPCmd_Update(t *testing.T) { }, ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.IPServiceGetRequest{ + WantRequest: &apiv2.IPServiceGetRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -324,7 +324,7 @@ func Test_IPCmd_Update(t *testing.T) { }, }, { - WantRequest: apiv2.IPServiceUpdateRequest{ + WantRequest: &apiv2.IPServiceUpdateRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, Description: &testresources.IP1().Description, @@ -366,7 +366,7 @@ func Test_IPCmd_Apply(t *testing.T) { }, ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.IPServiceGetRequest{ + WantRequest: &apiv2.IPServiceGetRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -377,7 +377,7 @@ func Test_IPCmd_Apply(t *testing.T) { }, }, { - WantRequest: apiv2.IPServiceCreateRequest{ + WantRequest: &apiv2.IPServiceCreateRequest{ Ip: &testresources.IP1().Ip, Project: testresources.IP1().Project, Network: testresources.IP1().Network, @@ -411,7 +411,7 @@ func Test_IPCmd_Apply(t *testing.T) { }, ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.IPServiceGetRequest{ + WantRequest: &apiv2.IPServiceGetRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -422,7 +422,7 @@ func Test_IPCmd_Apply(t *testing.T) { }, }, { - WantRequest: apiv2.IPServiceCreateRequest{ + WantRequest: &apiv2.IPServiceCreateRequest{ Ip: &testresources.IP1().Ip, Project: testresources.IP1().Project, Network: testresources.IP1().Network, @@ -435,7 +435,7 @@ func Test_IPCmd_Apply(t *testing.T) { WantError: connect.NewError(connect.CodeAlreadyExists, fmt.Errorf("already exists")), }, { - WantRequest: apiv2.IPServiceGetRequest{ + WantRequest: &apiv2.IPServiceGetRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, }, @@ -446,7 +446,7 @@ func Test_IPCmd_Apply(t *testing.T) { }, }, { - WantRequest: apiv2.IPServiceUpdateRequest{ + WantRequest: &apiv2.IPServiceUpdateRequest{ Ip: testresources.IP1().Ip, Project: testresources.IP1().Project, Description: &testresources.IP1().Description, diff --git a/pkg/tests/e2e/api/project_test.go b/pkg/tests/e2e/api/project_test.go index 9a83f10..84f2cdb 100644 --- a/pkg/tests/e2e/api/project_test.go +++ b/pkg/tests/e2e/api/project_test.go @@ -19,7 +19,7 @@ func Test_ProjectCmd_Describe(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ProjectServiceGetRequest{ + WantRequest: &apiv2.ProjectServiceGetRequest{ Project: testresources.Project1().Uuid, }, WantResponse: func() connect.AnyResponse { @@ -33,21 +33,21 @@ func Test_ProjectCmd_Describe(t *testing.T) { WantObject: testresources.Project1(), WantProtoObject: testresources.Project1(), WantTable: new(` - ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2000-01-01 00:00:00 UTC `), WantWideTable: new(` - ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2000-01-01 00:00:00 UTC `), Template: new("{{ .uuid }} {{ .name }}"), WantTemplate: new(` 0d81bca7-73f6-4da3-8397-4a8c52a0c583 project-a `), WantMarkdown: new(` - | ID | TENANT | NAME | DESCRIPTION | CREATION DATE | - |--------------------------------------|-------------|-----------|---------------|-------------------------| - | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | + | ID | TENANT | NAME | DESCRIPTION | CREATION DATE | + |--------------------------------------|-------------|-----------|---------------|-------------------------| + | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2000-01-01 00:00:00 UTC | `), }, } @@ -64,7 +64,7 @@ func Test_ProjectCmd_Create(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ProjectServiceCreateRequest{ + WantRequest: &apiv2.ProjectServiceCreateRequest{ Login: testresources.Project1().Tenant, Name: testresources.Project1().Name, Description: testresources.Project1().Description, @@ -89,7 +89,7 @@ func Test_ProjectCmd_Create(t *testing.T) { }, ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ProjectServiceCreateRequest{ + WantRequest: &apiv2.ProjectServiceCreateRequest{ Login: testresources.Project1().Tenant, Name: testresources.Project1().Name, Description: testresources.Project1().Description, @@ -103,8 +103,8 @@ func Test_ProjectCmd_Create(t *testing.T) { }, }), WantTable: new(` - ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + ID TENANT NAME DESCRIPTION CREATION DATE + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2000-01-01 00:00:00 UTC `), }, { @@ -116,7 +116,7 @@ func Test_ProjectCmd_Create(t *testing.T) { }, ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ProjectServiceCreateRequest{ + WantRequest: &apiv2.ProjectServiceCreateRequest{ Login: testresources.Project1().Tenant, Name: testresources.Project1().Name, Description: testresources.Project1().Description, @@ -128,7 +128,7 @@ func Test_ProjectCmd_Create(t *testing.T) { }, }, { - WantRequest: apiv2.ProjectServiceCreateRequest{ + WantRequest: &apiv2.ProjectServiceCreateRequest{ Login: testresources.Project2().Tenant, Name: testresources.Project2().Name, Description: testresources.Project2().Description, @@ -143,8 +143,8 @@ func Test_ProjectCmd_Create(t *testing.T) { }), WantTable: new(` ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC - f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2000-01-01 00:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2000-01-01 00:00:00 UTC `), }, } @@ -161,7 +161,7 @@ func Test_ProjectCmd_Delete(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ProjectServiceDeleteRequest{ + WantRequest: &apiv2.ProjectServiceDeleteRequest{ Project: testresources.Project1().Uuid, }, WantResponse: func() connect.AnyResponse { @@ -183,7 +183,7 @@ func Test_ProjectCmd_Delete(t *testing.T) { }, ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ProjectServiceDeleteRequest{ + WantRequest: &apiv2.ProjectServiceDeleteRequest{ Project: testresources.Project1().Uuid, }, WantResponse: func() connect.AnyResponse { @@ -196,7 +196,7 @@ func Test_ProjectCmd_Delete(t *testing.T) { }), WantTable: new(` ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2000-01-01 00:00:00 UTC `), }, } @@ -213,7 +213,7 @@ func Test_ProjectCmd_Update(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ProjectServiceUpdateRequest{ + WantRequest: &apiv2.ProjectServiceUpdateRequest{ Project: testresources.Project1().Uuid, Name: new("new-name"), Description: new("new-desc"), @@ -237,7 +237,7 @@ func Test_ProjectCmd_Update(t *testing.T) { }, ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ProjectServiceUpdateRequest{ + WantRequest: &apiv2.ProjectServiceUpdateRequest{ Project: testresources.Project1().Uuid, Name: new(testresources.Project1().Name), Description: new(testresources.Project1().Description), @@ -252,7 +252,7 @@ func Test_ProjectCmd_Update(t *testing.T) { }), WantTable: new(` ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2000-01-01 00:00:00 UTC `), }, } @@ -269,7 +269,7 @@ func Test_ProjectCmd_List(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.ProjectServiceListRequest{}, + WantRequest: &apiv2.ProjectServiceListRequest{}, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.ProjectServiceListResponse{ Projects: []*apiv2.Project{ @@ -283,13 +283,13 @@ func Test_ProjectCmd_List(t *testing.T) { }), WantTable: new(` ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC - f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2000-01-01 00:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2000-01-01 00:00:00 UTC `), WantWideTable: new(` ID TENANT NAME DESCRIPTION CREATION DATE - 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2025-06-01 10:00:00 UTC - f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2025-07-15 14:30:00 UTC + 0d81bca7-73f6-4da3-8397-4a8c52a0c583 metal-stack project-a first project 2000-01-01 00:00:00 UTC + f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c metal-stack project-b second project 2000-01-01 00:00:00 UTC `), Template: new("{{ .uuid }} {{ .name }}"), WantTemplate: new(` @@ -299,8 +299,8 @@ f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c project-b WantMarkdown: new(` | ID | TENANT | NAME | DESCRIPTION | CREATION DATE | |--------------------------------------|-------------|-----------|----------------|-------------------------| - | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2025-06-01 10:00:00 UTC | - | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | metal-stack | project-b | second project | 2025-07-15 14:30:00 UTC | + | 0d81bca7-73f6-4da3-8397-4a8c52a0c583 | metal-stack | project-a | first project | 2000-01-01 00:00:00 UTC | + | f3b4e6a1-2c8d-4e5f-a7b9-1d3e5f7a9b0c | metal-stack | project-b | second project | 2000-01-01 00:00:00 UTC | `), }, } diff --git a/pkg/tests/e2e/api/tenant_test.go b/pkg/tests/e2e/api/tenant_test.go index 524586b..3200667 100644 --- a/pkg/tests/e2e/api/tenant_test.go +++ b/pkg/tests/e2e/api/tenant_test.go @@ -17,7 +17,7 @@ func Test_TenantCmd_Describe(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.TenantServiceGetRequest{ + WantRequest: &apiv2.TenantServiceGetRequest{ Login: testresources.Tenant1().Login, }, WantResponse: func() connect.AnyResponse { @@ -62,7 +62,7 @@ func Test_TenantCmd_List(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.TenantServiceListRequest{}, + WantRequest: &apiv2.TenantServiceListRequest{}, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.TenantServiceListResponse{ Tenants: []*apiv2.Tenant{ diff --git a/pkg/tests/e2e/api/token_test.go b/pkg/tests/e2e/api/token_test.go index b91117e..cc562b8 100644 --- a/pkg/tests/e2e/api/token_test.go +++ b/pkg/tests/e2e/api/token_test.go @@ -17,7 +17,7 @@ func Test_TokenCmd_Describe(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.TokenServiceGetRequest{ + WantRequest: &apiv2.TokenServiceGetRequest{ Uuid: testresources.Token1().Uuid, }, WantResponse: func() connect.AnyResponse { @@ -62,7 +62,7 @@ func Test_TokenCmd_List(t *testing.T) { NewRootCmd: e2e.NewRootCmd(t, &e2e.TestConfig{ ClientCalls: []e2e.ClientCall{ { - WantRequest: apiv2.TokenServiceListRequest{}, + WantRequest: &apiv2.TokenServiceListRequest{}, WantResponse: func() connect.AnyResponse { return connect.NewResponse(&apiv2.TokenServiceListResponse{ Tokens: []*apiv2.Token{ diff --git a/pkg/tests/e2e/test_framework.go b/pkg/tests/e2e/test_framework.go index deba4ed..af794df 100644 --- a/pkg/tests/e2e/test_framework.go +++ b/pkg/tests/e2e/test_framework.go @@ -8,6 +8,7 @@ import ( "strings" "testing" "testing/synctest" + "time" "slices" @@ -56,6 +57,10 @@ type Test[Response, RawObject any] struct { WantErr error } +func TimeBubbleStartTime() time.Time { + return time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) +} + func (c *Test[Response, RawObject]) TestCmd(t *testing.T) { require.NotEmpty(t, c.Name, "test name must not be empty") require.NotEmpty(t, c.CmdArgs, "cmd must not be empty") diff --git a/pkg/tests/e2e/test_interceptor.go b/pkg/tests/e2e/test_interceptor.go index 0dee0fb..dc43709 100644 --- a/pkg/tests/e2e/test_interceptor.go +++ b/pkg/tests/e2e/test_interceptor.go @@ -8,6 +8,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/metal-stack/metal-lib/pkg/testcommon" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/runtime/protoimpl" "google.golang.org/protobuf/testing/protocmp" ) @@ -19,7 +20,7 @@ type testClientInterceptor struct { } type ClientCall struct { - WantRequest any + WantRequest proto.Message WantResponse func() connect.AnyResponse WantError *connect.Error } diff --git a/pkg/tests/e2e/testresources/projects.go b/pkg/tests/e2e/testresources/projects.go index 364265b..6e7ae0a 100644 --- a/pkg/tests/e2e/testresources/projects.go +++ b/pkg/tests/e2e/testresources/projects.go @@ -1,9 +1,8 @@ package testresources import ( - "time" - apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -15,7 +14,7 @@ var ( Description: "first project", Tenant: "metal-stack", Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2025, 6, 1, 10, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime()), }, } } @@ -26,7 +25,7 @@ var ( Description: "second project", Tenant: "metal-stack", Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2025, 7, 15, 14, 30, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime()), }, } } diff --git a/pkg/tests/e2e/testresources/tenants.go b/pkg/tests/e2e/testresources/tenants.go index 43e4eae..c1e0d74 100644 --- a/pkg/tests/e2e/testresources/tenants.go +++ b/pkg/tests/e2e/testresources/tenants.go @@ -1,9 +1,8 @@ package testresources import ( - "time" - apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -15,7 +14,7 @@ var ( Email: "info@metal-stack.io", Description: "a tenant", Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime()), }, } } @@ -26,7 +25,7 @@ var ( Email: "admin@acme.io", Description: "another tenant", Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime()), }, } } diff --git a/pkg/tests/e2e/testresources/tokens.go b/pkg/tests/e2e/testresources/tokens.go index 41b08fa..5c8ffdf 100644 --- a/pkg/tests/e2e/testresources/tokens.go +++ b/pkg/tests/e2e/testresources/tokens.go @@ -4,6 +4,7 @@ import ( "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/pkg/tests/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -14,11 +15,11 @@ var ( User: "admin@metal-stack.io", Description: "ci token", TokenType: apiv2.TokenType_TOKEN_TYPE_API, - Expires: timestamppb.New(time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC)), - IssuedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Expires: timestamppb.New(e2e.TimeBubbleStartTime().Add(24 * time.Hour)), + IssuedAt: timestamppb.New(e2e.TimeBubbleStartTime()), Permissions: nil, Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime()), }, } } @@ -28,11 +29,11 @@ var ( User: "dev@metal-stack.io", Description: "dev token", TokenType: apiv2.TokenType_TOKEN_TYPE_API, - Expires: timestamppb.New(time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC)), - IssuedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + Expires: timestamppb.New(e2e.TimeBubbleStartTime().Add(48 * time.Hour)), + IssuedAt: timestamppb.New(e2e.TimeBubbleStartTime()), Permissions: nil, Meta: &apiv2.Meta{ - CreatedAt: timestamppb.New(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)), + CreatedAt: timestamppb.New(e2e.TimeBubbleStartTime()), }, } } From 38ed827142b7d70f31a8c3f39a4163fe00610e18 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Fri, 20 Mar 2026 10:06:53 +0100 Subject: [PATCH 34/34] Move stuff around. --- {pkg/tests => testing}/e2e/test_client.go | 0 {pkg/tests => testing}/e2e/test_framework.go | 0 {pkg/tests => testing}/e2e/test_interceptor.go | 0 {pkg/tests => testing}/e2e/test_interceptor_test.go | 0 {pkg/tests => tests}/e2e/admin/component_test.go | 2 +- {pkg/tests => tests}/e2e/admin/project_test.go | 4 ++-- {pkg/tests => tests}/e2e/admin/switch_test.go | 2 +- {pkg/tests => tests}/e2e/admin/tenant_test.go | 2 +- {pkg/tests => tests}/e2e/admin/token_test.go | 4 ++-- {pkg/tests => tests}/e2e/api/health_test.go | 2 +- {pkg/tests => tests}/e2e/api/image_test.go | 4 ++-- {pkg/tests => tests}/e2e/api/ip_test.go | 4 ++-- {pkg/tests => tests}/e2e/api/project_test.go | 4 ++-- {pkg/tests => tests}/e2e/api/tenant_test.go | 4 ++-- {pkg/tests => tests}/e2e/api/token_test.go | 4 ++-- {pkg/tests => tests}/e2e/testresources/images.go | 0 {pkg/tests => tests}/e2e/testresources/ips.go | 0 {pkg/tests => tests}/e2e/testresources/projects.go | 2 +- {pkg/tests => tests}/e2e/testresources/tenants.go | 2 +- {pkg/tests => tests}/e2e/testresources/tokens.go | 2 +- 20 files changed, 21 insertions(+), 21 deletions(-) rename {pkg/tests => testing}/e2e/test_client.go (100%) rename {pkg/tests => testing}/e2e/test_framework.go (100%) rename {pkg/tests => testing}/e2e/test_interceptor.go (100%) rename {pkg/tests => testing}/e2e/test_interceptor_test.go (100%) rename {pkg/tests => tests}/e2e/admin/component_test.go (99%) rename {pkg/tests => tests}/e2e/admin/project_test.go (95%) rename {pkg/tests => tests}/e2e/admin/switch_test.go (99%) rename {pkg/tests => tests}/e2e/admin/tenant_test.go (98%) rename {pkg/tests => tests}/e2e/admin/token_test.go (97%) rename {pkg/tests => tests}/e2e/api/health_test.go (97%) rename {pkg/tests => tests}/e2e/api/image_test.go (97%) rename {pkg/tests => tests}/e2e/api/ip_test.go (99%) rename {pkg/tests => tests}/e2e/api/project_test.go (99%) rename {pkg/tests => tests}/e2e/api/tenant_test.go (96%) rename {pkg/tests => tests}/e2e/api/token_test.go (97%) rename {pkg/tests => tests}/e2e/testresources/images.go (100%) rename {pkg/tests => tests}/e2e/testresources/ips.go (100%) rename {pkg/tests => tests}/e2e/testresources/projects.go (94%) rename {pkg/tests => tests}/e2e/testresources/tenants.go (94%) rename {pkg/tests => tests}/e2e/testresources/tokens.go (96%) diff --git a/pkg/tests/e2e/test_client.go b/testing/e2e/test_client.go similarity index 100% rename from pkg/tests/e2e/test_client.go rename to testing/e2e/test_client.go diff --git a/pkg/tests/e2e/test_framework.go b/testing/e2e/test_framework.go similarity index 100% rename from pkg/tests/e2e/test_framework.go rename to testing/e2e/test_framework.go diff --git a/pkg/tests/e2e/test_interceptor.go b/testing/e2e/test_interceptor.go similarity index 100% rename from pkg/tests/e2e/test_interceptor.go rename to testing/e2e/test_interceptor.go diff --git a/pkg/tests/e2e/test_interceptor_test.go b/testing/e2e/test_interceptor_test.go similarity index 100% rename from pkg/tests/e2e/test_interceptor_test.go rename to testing/e2e/test_interceptor_test.go diff --git a/pkg/tests/e2e/admin/component_test.go b/tests/e2e/admin/component_test.go similarity index 99% rename from pkg/tests/e2e/admin/component_test.go rename to tests/e2e/admin/component_test.go index b446b5f..26f68ae 100644 --- a/pkg/tests/e2e/admin/component_test.go +++ b/tests/e2e/admin/component_test.go @@ -7,7 +7,7 @@ import ( "connectrpc.com/connect" adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/metal-stack/cli/testing/e2e" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/pkg/tests/e2e/admin/project_test.go b/tests/e2e/admin/project_test.go similarity index 95% rename from pkg/tests/e2e/admin/project_test.go rename to tests/e2e/admin/project_test.go index dc83935..2739239 100644 --- a/pkg/tests/e2e/admin/project_test.go +++ b/tests/e2e/admin/project_test.go @@ -6,8 +6,8 @@ import ( "connectrpc.com/connect" adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" - "github.com/metal-stack/cli/pkg/tests/e2e/testresources" + "github.com/metal-stack/cli/testing/e2e" + "github.com/metal-stack/cli/tests/e2e/testresources" ) func Test_AdminProjectCmd_List(t *testing.T) { diff --git a/pkg/tests/e2e/admin/switch_test.go b/tests/e2e/admin/switch_test.go similarity index 99% rename from pkg/tests/e2e/admin/switch_test.go rename to tests/e2e/admin/switch_test.go index 3a11668..60a9581 100644 --- a/pkg/tests/e2e/admin/switch_test.go +++ b/tests/e2e/admin/switch_test.go @@ -7,7 +7,7 @@ import ( "connectrpc.com/connect" adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/metal-stack/cli/testing/e2e" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/pkg/tests/e2e/admin/tenant_test.go b/tests/e2e/admin/tenant_test.go similarity index 98% rename from pkg/tests/e2e/admin/tenant_test.go rename to tests/e2e/admin/tenant_test.go index be1446f..9c96572 100644 --- a/pkg/tests/e2e/admin/tenant_test.go +++ b/tests/e2e/admin/tenant_test.go @@ -6,7 +6,7 @@ import ( "connectrpc.com/connect" adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/metal-stack/cli/testing/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/pkg/tests/e2e/admin/token_test.go b/tests/e2e/admin/token_test.go similarity index 97% rename from pkg/tests/e2e/admin/token_test.go rename to tests/e2e/admin/token_test.go index 1082b48..1393e31 100644 --- a/pkg/tests/e2e/admin/token_test.go +++ b/tests/e2e/admin/token_test.go @@ -6,8 +6,8 @@ import ( "connectrpc.com/connect" adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" - "github.com/metal-stack/cli/pkg/tests/e2e/testresources" + "github.com/metal-stack/cli/testing/e2e" + "github.com/metal-stack/cli/tests/e2e/testresources" ) func Test_AdminTokenCmd_List(t *testing.T) { diff --git a/pkg/tests/e2e/api/health_test.go b/tests/e2e/api/health_test.go similarity index 97% rename from pkg/tests/e2e/api/health_test.go rename to tests/e2e/api/health_test.go index 9f1b0c4..eab32ef 100644 --- a/pkg/tests/e2e/api/health_test.go +++ b/tests/e2e/api/health_test.go @@ -5,7 +5,7 @@ import ( "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/metal-stack/cli/testing/e2e" ) var ( diff --git a/pkg/tests/e2e/api/image_test.go b/tests/e2e/api/image_test.go similarity index 97% rename from pkg/tests/e2e/api/image_test.go rename to tests/e2e/api/image_test.go index 678fcbe..f25f151 100644 --- a/pkg/tests/e2e/api/image_test.go +++ b/tests/e2e/api/image_test.go @@ -5,8 +5,8 @@ import ( "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" - "github.com/metal-stack/cli/pkg/tests/e2e/testresources" + "github.com/metal-stack/cli/testing/e2e" + "github.com/metal-stack/cli/tests/e2e/testresources" ) func Test_ImageCmd_List(t *testing.T) { diff --git a/pkg/tests/e2e/api/ip_test.go b/tests/e2e/api/ip_test.go similarity index 99% rename from pkg/tests/e2e/api/ip_test.go rename to tests/e2e/api/ip_test.go index 4f702eb..69985bc 100644 --- a/pkg/tests/e2e/api/ip_test.go +++ b/tests/e2e/api/ip_test.go @@ -6,8 +6,8 @@ import ( "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" - "github.com/metal-stack/cli/pkg/tests/e2e/testresources" + "github.com/metal-stack/cli/testing/e2e" + "github.com/metal-stack/cli/tests/e2e/testresources" "github.com/spf13/afero" "github.com/stretchr/testify/require" ) diff --git a/pkg/tests/e2e/api/project_test.go b/tests/e2e/api/project_test.go similarity index 99% rename from pkg/tests/e2e/api/project_test.go rename to tests/e2e/api/project_test.go index 84f2cdb..e232912 100644 --- a/pkg/tests/e2e/api/project_test.go +++ b/tests/e2e/api/project_test.go @@ -5,8 +5,8 @@ import ( "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" - "github.com/metal-stack/cli/pkg/tests/e2e/testresources" + "github.com/metal-stack/cli/testing/e2e" + "github.com/metal-stack/cli/tests/e2e/testresources" "github.com/spf13/afero" "github.com/stretchr/testify/require" ) diff --git a/pkg/tests/e2e/api/tenant_test.go b/tests/e2e/api/tenant_test.go similarity index 96% rename from pkg/tests/e2e/api/tenant_test.go rename to tests/e2e/api/tenant_test.go index 3200667..01d3fb7 100644 --- a/pkg/tests/e2e/api/tenant_test.go +++ b/tests/e2e/api/tenant_test.go @@ -5,8 +5,8 @@ import ( "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" - "github.com/metal-stack/cli/pkg/tests/e2e/testresources" + "github.com/metal-stack/cli/testing/e2e" + "github.com/metal-stack/cli/tests/e2e/testresources" ) func Test_TenantCmd_Describe(t *testing.T) { diff --git a/pkg/tests/e2e/api/token_test.go b/tests/e2e/api/token_test.go similarity index 97% rename from pkg/tests/e2e/api/token_test.go rename to tests/e2e/api/token_test.go index cc562b8..45af157 100644 --- a/pkg/tests/e2e/api/token_test.go +++ b/tests/e2e/api/token_test.go @@ -5,8 +5,8 @@ import ( "connectrpc.com/connect" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" - "github.com/metal-stack/cli/pkg/tests/e2e/testresources" + "github.com/metal-stack/cli/testing/e2e" + "github.com/metal-stack/cli/tests/e2e/testresources" ) func Test_TokenCmd_Describe(t *testing.T) { diff --git a/pkg/tests/e2e/testresources/images.go b/tests/e2e/testresources/images.go similarity index 100% rename from pkg/tests/e2e/testresources/images.go rename to tests/e2e/testresources/images.go diff --git a/pkg/tests/e2e/testresources/ips.go b/tests/e2e/testresources/ips.go similarity index 100% rename from pkg/tests/e2e/testresources/ips.go rename to tests/e2e/testresources/ips.go diff --git a/pkg/tests/e2e/testresources/projects.go b/tests/e2e/testresources/projects.go similarity index 94% rename from pkg/tests/e2e/testresources/projects.go rename to tests/e2e/testresources/projects.go index 6e7ae0a..3f06a38 100644 --- a/pkg/tests/e2e/testresources/projects.go +++ b/tests/e2e/testresources/projects.go @@ -2,7 +2,7 @@ package testresources import ( apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/metal-stack/cli/testing/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/pkg/tests/e2e/testresources/tenants.go b/tests/e2e/testresources/tenants.go similarity index 94% rename from pkg/tests/e2e/testresources/tenants.go rename to tests/e2e/testresources/tenants.go index c1e0d74..05b958b 100644 --- a/pkg/tests/e2e/testresources/tenants.go +++ b/tests/e2e/testresources/tenants.go @@ -2,7 +2,7 @@ package testresources import ( apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/metal-stack/cli/testing/e2e" "google.golang.org/protobuf/types/known/timestamppb" ) diff --git a/pkg/tests/e2e/testresources/tokens.go b/tests/e2e/testresources/tokens.go similarity index 96% rename from pkg/tests/e2e/testresources/tokens.go rename to tests/e2e/testresources/tokens.go index 5c8ffdf..b3349d6 100644 --- a/pkg/tests/e2e/testresources/tokens.go +++ b/tests/e2e/testresources/tokens.go @@ -4,7 +4,7 @@ import ( "time" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" - "github.com/metal-stack/cli/pkg/tests/e2e" + "github.com/metal-stack/cli/testing/e2e" "google.golang.org/protobuf/types/known/timestamppb" )