Skip to content

Commit 25edf52

Browse files
committed
feat(cli): add artifact get command tests and align with GetCLIClient pattern
- switch artifact get to use cli.GetCLIClient for both artifact and profile clients - remove custom injection logic and align with upstream CLI patterns - add golden-output tests for artifact get (table/json/yaml + error case) - use gomock-based clients with realistic evaluation data - update goldens to reflect current CLI formatting - regenerate profile/status goldens after rebase to match latest output
1 parent 0d42254 commit 25edf52

17 files changed

Lines changed: 305 additions & 234 deletions

cmd/cli/app/artifact/artifact_get.go

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414

1515
"github.com/spf13/cobra"
1616
"github.com/spf13/viper"
17-
"google.golang.org/grpc"
1817
"google.golang.org/protobuf/reflect/protoreflect"
1918

2019
"github.com/mindersec/minder/cmd/cli/app"
@@ -31,13 +30,28 @@ var getCmd = &cobra.Command{
3130
Use: "get",
3231
Short: "Get artifact details",
3332
Long: `The artifact get subcommand will get artifact details from an artifact, for a given ID.`,
34-
RunE: cli.GRPCClientWrapRunE(getCommand),
33+
RunE: getCommand,
3534
}
3635

3736
// getCommand is the artifact get subcommand
38-
func getCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.ClientConn) error {
39-
client := getArtifactClient(ctx, conn)
40-
profileClient := getProfileClient(ctx, conn)
37+
func getCommand(cmd *cobra.Command, _ []string) error {
38+
if err := viper.BindPFlags(cmd.Flags()); err != nil {
39+
return fmt.Errorf("error binding flags: %w", err)
40+
}
41+
42+
client, cleanup, err := cli.GetCLIClient(cmd, minderv1.NewArtifactServiceClient)
43+
if err != nil {
44+
return err
45+
}
46+
defer cleanup()
47+
48+
profileClient, profileCleanup, err := cli.GetCLIClient(cmd, minderv1.NewProfileServiceClient)
49+
if err != nil {
50+
return err
51+
}
52+
defer profileCleanup()
53+
54+
ctx := cmd.Context()
4155

4256
provider := viper.GetString("provider")
4357
project := viper.GetString("project")
@@ -75,22 +89,6 @@ func getCommand(ctx context.Context, cmd *cobra.Command, _ []string, conn *grpc.
7589
return nil
7690
}
7791

78-
func getArtifactClient(ctx context.Context, conn *grpc.ClientConn) minderv1.ArtifactServiceClient {
79-
if mockClient, ok := cli.GetRPCClient[minderv1.ArtifactServiceClient](ctx); ok {
80-
return mockClient
81-
}
82-
83-
return minderv1.NewArtifactServiceClient(conn)
84-
}
85-
86-
func getProfileClient(ctx context.Context, conn *grpc.ClientConn) minderv1.ProfileServiceClient {
87-
if mockClient, ok := cli.GetRPCClient[minderv1.ProfileServiceClient](ctx); ok {
88-
return mockClient
89-
}
90-
91-
return minderv1.NewProfileServiceClient(conn)
92-
}
93-
9492
func artifactGet(
9593
ctx context.Context,
9694
client minderv1.ArtifactServiceClient,

cmd/cli/app/artifact/artifact_get_test.go

Lines changed: 51 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -8,109 +8,63 @@ import (
88
"testing"
99

1010
"go.uber.org/mock/gomock"
11-
"google.golang.org/grpc"
1211
"google.golang.org/grpc/codes"
1312
"google.golang.org/grpc/status"
1413

1514
"github.com/mindersec/minder/internal/util/cli"
1615
minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
16+
mockv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1/mock"
1717
)
1818

19-
type artifactClientStub struct {
20-
getArtifactByIDResp *minderv1.GetArtifactByIdResponse
21-
getArtifactByIDErr error
22-
getArtifactByNameResp *minderv1.GetArtifactByNameResponse
23-
getArtifactByNameErr error
24-
}
25-
26-
func (s *artifactClientStub) ListArtifacts(context.Context, *minderv1.ListArtifactsRequest, ...grpc.CallOption) (*minderv1.ListArtifactsResponse, error) {
27-
_ = s
28-
return nil, status.Error(codes.Unimplemented, "unexpected call")
29-
}
30-
31-
func (s *artifactClientStub) GetArtifactById(_ context.Context, _ *minderv1.GetArtifactByIdRequest, _ ...grpc.CallOption) (*minderv1.GetArtifactByIdResponse, error) {
32-
return s.getArtifactByIDResp, s.getArtifactByIDErr
33-
}
34-
35-
func (s *artifactClientStub) GetArtifactByName(_ context.Context, _ *minderv1.GetArtifactByNameRequest, _ ...grpc.CallOption) (*minderv1.GetArtifactByNameResponse, error) {
36-
return s.getArtifactByNameResp, s.getArtifactByNameErr
37-
}
38-
39-
type profileClientStub struct {
40-
listProfilesResp *minderv1.ListProfilesResponse
41-
listProfilesErr error
42-
statusResp *minderv1.GetProfileStatusByNameResponse
43-
statusErr error
44-
}
45-
46-
func (s *profileClientStub) CreateProfile(context.Context, *minderv1.CreateProfileRequest, ...grpc.CallOption) (*minderv1.CreateProfileResponse, error) {
47-
_ = s
48-
return nil, status.Error(codes.Unimplemented, "unexpected call")
49-
}
50-
51-
func (s *profileClientStub) UpdateProfile(context.Context, *minderv1.UpdateProfileRequest, ...grpc.CallOption) (*minderv1.UpdateProfileResponse, error) {
52-
_ = s
53-
return nil, status.Error(codes.Unimplemented, "unexpected call")
54-
}
55-
56-
func (s *profileClientStub) PatchProfile(context.Context, *minderv1.PatchProfileRequest, ...grpc.CallOption) (*minderv1.PatchProfileResponse, error) {
57-
_ = s
58-
return nil, status.Error(codes.Unimplemented, "unexpected call")
59-
}
60-
61-
func (s *profileClientStub) DeleteProfile(context.Context, *minderv1.DeleteProfileRequest, ...grpc.CallOption) (*minderv1.DeleteProfileResponse, error) {
62-
_ = s
63-
return nil, status.Error(codes.Unimplemented, "unexpected call")
64-
}
65-
66-
func (s *profileClientStub) ListProfiles(_ context.Context, _ *minderv1.ListProfilesRequest, _ ...grpc.CallOption) (*minderv1.ListProfilesResponse, error) {
67-
return s.listProfilesResp, s.listProfilesErr
68-
}
69-
70-
func (s *profileClientStub) GetProfileById(context.Context, *minderv1.GetProfileByIdRequest, ...grpc.CallOption) (*minderv1.GetProfileByIdResponse, error) {
71-
_ = s
72-
return nil, status.Error(codes.Unimplemented, "unexpected call")
73-
}
74-
75-
func (s *profileClientStub) GetProfileByName(context.Context, *minderv1.GetProfileByNameRequest, ...grpc.CallOption) (*minderv1.GetProfileByNameResponse, error) {
76-
_ = s
77-
return nil, status.Error(codes.Unimplemented, "unexpected call")
78-
}
79-
80-
func (s *profileClientStub) GetProfileStatusByName(_ context.Context, _ *minderv1.GetProfileStatusByNameRequest, _ ...grpc.CallOption) (*minderv1.GetProfileStatusByNameResponse, error) {
81-
return s.statusResp, s.statusErr
82-
}
83-
84-
func (s *profileClientStub) GetProfileStatusById(context.Context, *minderv1.GetProfileStatusByIdRequest, ...grpc.CallOption) (*minderv1.GetProfileStatusByIdResponse, error) {
85-
_ = s
86-
return nil, status.Error(codes.Unimplemented, "unexpected call")
87-
}
88-
89-
func (s *profileClientStub) GetProfileStatusByProject(context.Context, *minderv1.GetProfileStatusByProjectRequest, ...grpc.CallOption) (*minderv1.GetProfileStatusByProjectResponse, error) {
90-
_ = s
91-
return nil, status.Error(codes.Unimplemented, "unexpected call")
92-
}
93-
94-
// Tests rely on injected ArtifactServiceClient; profile evaluation is skipped in this mode.
95-
//
9619
//nolint:paralleltest // Cannot run in parallel because it swaps global Viper/Stdout state
9720
func TestArtifactGetCommand(t *testing.T) {
98-
setupSuccess := func(t *testing.T, _ *gomock.Controller) context.Context {
21+
setupSuccess := func(t *testing.T, ctrl *gomock.Controller) context.Context {
9922
t.Helper()
100-
client := &artifactClientStub{}
10123

102-
mockResp := &minderv1.GetArtifactByIdResponse{}
103-
cli.LoadFixture(t, "mock_artifact_get.json", mockResp)
24+
artifactClient := mockv1.NewMockArtifactServiceClient(ctrl)
25+
profileClient := mockv1.NewMockProfileServiceClient(ctrl)
26+
27+
artifactResp := &minderv1.GetArtifactByIdResponse{}
28+
cli.LoadFixture(t, "mock_artifact_get.json", artifactResp)
10429

105-
client.getArtifactByIDResp = mockResp
106-
client.getArtifactByIDErr = nil
30+
artifactClient.EXPECT().
31+
GetArtifactById(gomock.Any(), gomock.Any()).
32+
Return(artifactResp, nil).
33+
Times(1)
10734

108-
profileClient := &profileClientStub{
109-
listProfilesResp: &minderv1.ListProfilesResponse{},
110-
statusResp: &minderv1.GetProfileStatusByNameResponse{},
35+
listProfilesResp := &minderv1.ListProfilesResponse{
36+
Profiles: []*minderv1.Profile{
37+
{Name: "artifact-security-baseline"},
38+
},
39+
}
40+
profileClient.EXPECT().
41+
ListProfiles(gomock.Any(), gomock.Any()).
42+
Return(listProfilesResp, nil).
43+
Times(1)
44+
45+
statusResp := &minderv1.GetProfileStatusByNameResponse{
46+
RuleEvaluationStatus: []*minderv1.RuleEvaluationStatus{
47+
{
48+
ProfileId: "artifact-security-baseline",
49+
RuleDescriptionName: "Require artifact attestation",
50+
RuleTypeName: "artifact_attestation_slsa",
51+
Status: "failure",
52+
Details: "artifact attestation is disabled for this image",
53+
Guidance: "enable artifact attestations before release",
54+
RemediationUrl: "https://example.com/remediate/artifact-111",
55+
EntityInfo: map[string]string{
56+
"name": "owner-1/artifact-1",
57+
},
58+
Entity: "artifact",
59+
},
60+
},
11161
}
62+
profileClient.EXPECT().
63+
GetProfileStatusByName(gomock.Any(), gomock.Any()).
64+
Return(statusResp, nil).
65+
Times(1)
11266

113-
ctx := cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), client)
67+
ctx := cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), artifactClient)
11468
ctx = cli.WithRPCClient[minderv1.ProfileServiceClient](ctx, profileClient)
11569
return ctx
11670
}
@@ -137,12 +91,17 @@ func TestArtifactGetCommand(t *testing.T) {
13791
{
13892
Name: "server error handling",
13993
Args: []string{"artifact", "get", "-i", "111"},
140-
MockSetup: func(t *testing.T, _ *gomock.Controller) context.Context {
94+
MockSetup: func(t *testing.T, ctrl *gomock.Controller) context.Context {
14195
t.Helper()
142-
client := &artifactClientStub{getArtifactByIDErr: status.Error(codes.NotFound, "artifact not found")}
143-
profileClient := &profileClientStub{listProfilesResp: &minderv1.ListProfilesResponse{}, statusResp: &minderv1.GetProfileStatusByNameResponse{}}
96+
artifactClient := mockv1.NewMockArtifactServiceClient(ctrl)
97+
profileClient := mockv1.NewMockProfileServiceClient(ctrl)
98+
99+
artifactClient.EXPECT().
100+
GetArtifactById(gomock.Any(), gomock.Any()).
101+
Return(nil, status.Error(codes.NotFound, "artifact not found")).
102+
Times(1)
144103

145-
ctx := cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), client)
104+
ctx := cli.WithRPCClient[minderv1.ArtifactServiceClient](context.Background(), artifactClient)
146105
ctx = cli.WithRPCClient[minderv1.ProfileServiceClient](ctx, profileClient)
147106
return ctx
148107
},
Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
{
2-
"artifact": {
3-
"artifactPk": "111",
4-
"owner": "owner-1",
5-
"name": "artifact-1",
6-
"type": "image",
7-
"visibility": "public",
8-
"repository": "org/repo",
9-
"createdAt": "2024-01-02T15:04:05Z"
2+
"artifact": {
3+
"artifactPk": "111",
4+
"owner": "owner-1",
5+
"name": "artifact-1",
6+
"type": "image",
7+
"visibility": "public",
8+
"repository": "org/repo",
9+
"createdAt": "2024-01-02T15:04:05Z"
1010
}
1111
}
12+
[
13+
{
14+
"profileId": "artifact-security-baseline",
15+
"entity": "artifact",
16+
"status": "failure",
17+
"entityInfo": {
18+
"name": "owner-1/artifact-1"
19+
},
20+
"details": "artifact attestation is disabled for this image",
21+
"guidance": "enable artifact attestations before release",
22+
"ruleTypeName": "artifact_attestation_slsa",
23+
"ruleDescriptionName": "Require artifact attestation",
24+
"remediationUrl": "https://example.com/remediate/artifact-111"
25+
}
26+
]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
ID │ TYPE │ OWNER │ NAME │ REPOSITORY │ VISIBILITY │ CREATION DATE
22
───────┼────────┼──────────┼──────────────┼──────────────┼──────────────┼───────────────────────────
33
111 │ image │ owner-1 │ artifact-1 │ org/repo │ public │ 2024-01-02T15:04:05Z
4+
PROFILE │ RULE │ RESULT │ DETAILS
5+
────────────────────────────┼──────────────────────────────┼─────────┼──────────────────────────────
6+
artifact-security-baseline │ Require artifact attestation │ failure │ - artifact attestation is
7+
│ │ │ disabled for this image

cmd/cli/app/artifact/testdata/artifact_get.yaml.golden

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,14 @@ artifact:
77
type: image
88
visibility: public
99

10+
- details: artifact attestation is disabled for this image
11+
entity: artifact
12+
entityInfo:
13+
name: owner-1/artifact-1
14+
guidance: enable artifact attestations before release
15+
profileId: artifact-security-baseline
16+
remediationUrl: https://example.com/remediate/artifact-111
17+
ruleDescriptionName: Require artifact attestation
18+
ruleTypeName: artifact_attestation_slsa
19+
status: failure
20+
Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,48 @@
11
{
2-
"profileStatus": {
3-
"profileId": "11111111-1111-1111-1111-111111111111",
4-
"profileName": "mock-profile",
5-
"profileStatus": "success",
6-
"lastUpdated": "2024-01-01T00:00:00Z"
2+
"profileStatus": {
3+
"profileId": "11111111-1111-1111-1111-111111111111",
4+
"profileName": "mock-profile",
5+
"profileStatus": "success",
6+
"lastUpdated": "2024-01-01T00:00:00Z"
77
},
8-
"ruleEvaluationStatus": [
8+
"ruleEvaluationStatus": [
99
{
10-
"profileId": "11111111-1111-1111-1111-111111111111",
11-
"ruleId": "mock-rule-123",
12-
"ruleName": "secret_scanning",
13-
"entity": "repository",
14-
"status": "success",
15-
"lastUpdated": "2024-01-01T00:00:00Z",
16-
"entityInfo": {
17-
"entity_type": "repository",
18-
"name": "acme-corp/mock-repo",
19-
"provider": "github-app-mock-provider",
20-
"repo_name": "mock-repo",
21-
"repo_owner": "acme-corp",
22-
"repository_id": "22222222-2222-2222-2222-222222222222"
10+
"profileId": "11111111-1111-1111-1111-111111111111",
11+
"ruleId": "mock-rule-123",
12+
"ruleName": "secret_scanning",
13+
"entity": "repository",
14+
"status": "success",
15+
"lastUpdated": "2024-01-01T00:00:00Z",
16+
"entityInfo": {
17+
"entity_type": "repository",
18+
"name": "acme-corp/mock-repo",
19+
"provider": "github-app-mock-provider",
20+
"repo_name": "mock-repo",
21+
"repo_owner": "acme-corp",
22+
"repository_id": "22222222-2222-2222-2222-222222222222"
2323
},
24-
"details": "Mock rule evaluation succeeded.",
25-
"ruleTypeName": "secret_scanning",
26-
"ruleDisplayName": "Enable secret scanning to detect hardcoded secrets"
24+
"details": "Mock rule evaluation succeeded.",
25+
"ruleTypeName": "secret_scanning",
26+
"ruleDisplayName": "Enable secret scanning to detect hardcoded secrets"
2727
},
2828
{
29-
"profileId": "11111111-1111-1111-1111-111111111111",
30-
"ruleId": "mock-rule-456",
31-
"ruleName": "codeql_enabled",
32-
"entity": "repository",
33-
"status": "failure",
34-
"lastUpdated": "2024-01-01T00:00:00Z",
35-
"entityInfo": {
36-
"entity_type": "repository",
37-
"name": "acme-corp/mock-repo",
38-
"provider": "github-app-mock-provider",
39-
"repo_name": "mock-repo",
40-
"repo_owner": "acme-corp",
41-
"repository_id": "22222222-2222-2222-2222-222222222222"
29+
"profileId": "11111111-1111-1111-1111-111111111111",
30+
"ruleId": "mock-rule-456",
31+
"ruleName": "codeql_enabled",
32+
"entity": "repository",
33+
"status": "failure",
34+
"lastUpdated": "2024-01-01T00:00:00Z",
35+
"entityInfo": {
36+
"entity_type": "repository",
37+
"name": "acme-corp/mock-repo",
38+
"provider": "github-app-mock-provider",
39+
"repo_name": "mock-repo",
40+
"repo_owner": "acme-corp",
41+
"repository_id": "22222222-2222-2222-2222-222222222222"
4242
},
43-
"details": "Mock rule evaluation failed.",
44-
"ruleTypeName": "codeql_enabled",
45-
"ruleDisplayName": "Enable CodeQL for vulnerability scanning"
43+
"details": "Mock rule evaluation failed.",
44+
"ruleTypeName": "codeql_enabled",
45+
"ruleDisplayName": "Enable CodeQL for vulnerability scanning"
4646
}
4747
]
4848
}

0 commit comments

Comments
 (0)