Skip to content

Commit 7413ce6

Browse files
committed
test/Unit tests for user list command and ListUser view
Refactored user list command (UserListCmd) to separate out PrintUsers and GetUsers logic, and wrote unit tests for PrintUsers. Refactored user list view (ListUsers) to separate out MakeUserRows logic and wrote unit tests for it. Made some minor refactory changes in pkg/utils/helper.go and pkg/utils/utils.go to enable writing clean test code Signed-off-by: Rayyan Khan <rayyanrehman101@gmail.com>
1 parent 4897b35 commit 7413ce6

File tree

6 files changed

+370
-62
lines changed

6 files changed

+370
-62
lines changed

cmd/harbor/root/user/list.go

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ package user
1515

1616
import (
1717
"fmt"
18+
"io"
19+
"os"
1820

1921
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
2022
"github.com/goharbor/harbor-cli/pkg/api"
@@ -25,64 +27,76 @@ import (
2527
"github.com/spf13/viper"
2628
)
2729

28-
func UserListCmd() *cobra.Command {
29-
var opts api.ListFlags
30+
func GetUsers(opts api.ListFlags) ([]*models.UserResp, error) {
3031
var allUsers []*models.UserResp
3132

32-
cmd := &cobra.Command{
33-
Use: "list",
34-
Short: "List users",
35-
Args: cobra.ExactArgs(0),
36-
Aliases: []string{"ls"},
37-
RunE: func(cmd *cobra.Command, args []string) error {
38-
if opts.PageSize > 100 {
39-
return fmt.Errorf("page size should be less than or equal to 100")
40-
}
33+
if opts.PageSize > 100 {
34+
return nil, fmt.Errorf("page size should be less than or equal to 100")
35+
}
4136

42-
if opts.PageSize == 0 {
43-
opts.PageSize = 100
44-
opts.Page = 1
37+
if opts.PageSize == 0 {
38+
opts.PageSize = 100
39+
opts.Page = 1
4540

46-
for {
47-
response, err := api.ListUsers(opts)
48-
if err != nil {
49-
if isUnauthorizedError(err) {
50-
return fmt.Errorf("Permission denied: Admin privileges are required to execute this command.")
51-
}
52-
return fmt.Errorf("failed to list users: %v", err)
53-
}
41+
for {
42+
response, err := api.ListUsers(opts)
43+
if err != nil {
44+
if isUnauthorizedError(err) {
45+
return nil, fmt.Errorf("Permission denied: Admin privileges are required to execute this command.")
46+
}
47+
return nil, fmt.Errorf("failed to list users: %v", err)
48+
}
5449

55-
allUsers = append(allUsers, response.Payload...)
50+
allUsers = append(allUsers, response.Payload...)
5651

57-
if len(response.Payload) < int(opts.PageSize) {
58-
break
59-
}
60-
opts.Page++
61-
}
62-
} else {
63-
response, err := api.ListUsers(opts)
64-
if err != nil {
65-
if isUnauthorizedError(err) {
66-
return fmt.Errorf("Permission denied: Admin privileges are required to execute this command.")
67-
}
68-
return fmt.Errorf("failed to list users: %v", err)
69-
}
70-
allUsers = response.Payload
52+
if len(response.Payload) < int(opts.PageSize) {
53+
break
7154
}
72-
if len(allUsers) == 0 {
73-
log.Info("No users found")
74-
return nil
55+
opts.Page++
56+
}
57+
} else {
58+
response, err := api.ListUsers(opts)
59+
if err != nil {
60+
if isUnauthorizedError(err) {
61+
return nil, fmt.Errorf("Permission denied: Admin privileges are required to execute this command.")
7562
}
76-
formatFlag := viper.GetString("output-format")
77-
if formatFlag != "" {
78-
err := utils.PrintFormat(allUsers, formatFlag)
79-
if err != nil {
80-
log.Error(err)
81-
}
82-
} else {
83-
list.ListUsers(allUsers)
63+
return nil, fmt.Errorf("failed to list users: %v", err)
64+
}
65+
allUsers = response.Payload
66+
}
67+
return allUsers, nil
68+
}
69+
func PrintUsers(w io.Writer, allUsers []*models.UserResp) error {
70+
if len(allUsers) == 0 {
71+
log.Info("No users found")
72+
return nil
73+
}
74+
formatFlag := viper.GetString("output-format")
75+
if formatFlag != "" {
76+
err := utils.FPrintFormat(w, allUsers, formatFlag)
77+
if err != nil {
78+
log.Error(err)
79+
}
80+
} else {
81+
if err := list.ListUsers(w, allUsers); err != nil {
82+
return err
83+
}
84+
}
85+
return nil
86+
}
87+
func UserListCmd() *cobra.Command {
88+
var opts api.ListFlags
89+
cmd := &cobra.Command{
90+
Use: "list",
91+
Short: "List users",
92+
Args: cobra.ExactArgs(0),
93+
Aliases: []string{"ls"},
94+
RunE: func(cmd *cobra.Command, args []string) error {
95+
allUsers, err := GetUsers(opts)
96+
if err != nil {
97+
return err
8498
}
85-
return nil
99+
return PrintUsers(os.Stdout, allUsers)
86100
},
87101
}
88102

cmd/harbor/root/user/list_test.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright Project Harbor Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package user
16+
17+
import (
18+
"bytes"
19+
"encoding/json"
20+
"strings"
21+
"testing"
22+
23+
"github.com/go-openapi/strfmt"
24+
"github.com/goharbor/go-client/pkg/sdk/v2.0/models"
25+
log "github.com/sirupsen/logrus"
26+
"github.com/spf13/viper"
27+
"go.yaml.in/yaml/v4"
28+
)
29+
30+
func TestPrintUsers(t *testing.T) {
31+
testDate, _ := strfmt.ParseDateTime("2023-01-01T12:00:00Z")
32+
testUsers := func() []*models.UserResp {
33+
return []*models.UserResp{
34+
{
35+
UserID: 1,
36+
Username: "testUser1",
37+
Email: "test1@domain.com",
38+
SysadminFlag: true,
39+
Realname: "Test1",
40+
CreationTime: testDate,
41+
},
42+
{
43+
UserID: 2,
44+
Username: "testUser2",
45+
Email: "test2@domain.com",
46+
SysadminFlag: false,
47+
Realname: "Test2",
48+
CreationTime: testDate,
49+
},
50+
{
51+
UserID: 3,
52+
Username: "testUser3",
53+
Email: "test3@domain.com",
54+
SysadminFlag: false,
55+
Realname: "Test3",
56+
CreationTime: testDate,
57+
},
58+
}
59+
}
60+
tests := []struct {
61+
name string
62+
setup func() []*models.UserResp
63+
outputFormat string
64+
}{
65+
{
66+
name: "Number of users not zero and output format is json",
67+
setup: func() []*models.UserResp {
68+
users := testUsers()
69+
return users
70+
},
71+
outputFormat: "json",
72+
},
73+
{
74+
name: "Number of users not zero and output format yaml",
75+
setup: func() []*models.UserResp {
76+
users := testUsers()
77+
return users
78+
},
79+
outputFormat: "yaml",
80+
},
81+
{
82+
name: "Number of users not zero and output format default",
83+
setup: func() []*models.UserResp {
84+
users := testUsers()
85+
return users
86+
},
87+
outputFormat: "",
88+
},
89+
{
90+
name: "Number of users is zero",
91+
setup: func() []*models.UserResp {
92+
users := []*models.UserResp{}
93+
return users
94+
},
95+
outputFormat: "default",
96+
},
97+
}
98+
for _, tt := range tests {
99+
t.Run(tt.name, func(t *testing.T) {
100+
allUsers := tt.setup()
101+
102+
var logBuf, contentBuf bytes.Buffer
103+
originalLogOutput := log.StandardLogger().Out
104+
log.SetOutput(&logBuf)
105+
defer log.SetOutput(originalLogOutput)
106+
107+
originalFormatFlag := viper.GetString("output-format")
108+
viper.Set("output-format", tt.outputFormat)
109+
defer viper.Set("output-format", originalFormatFlag)
110+
111+
if err := PrintUsers(&contentBuf, allUsers); err != nil {
112+
t.Fatalf("PrintUsers() returned error: %v", err)
113+
}
114+
115+
logs := logBuf.String()
116+
117+
switch {
118+
case len(allUsers) == 0:
119+
if !strings.Contains(logs, "No users found") {
120+
t.Errorf(`Expected logs to contain "No user found" but got: %s`, logs)
121+
}
122+
case tt.outputFormat == "json":
123+
if contentBuf.Len() == 0 {
124+
t.Fatal("Expected JSON output, but buffer was empty")
125+
}
126+
var decodedUsers []*models.UserResp
127+
if err := json.Unmarshal(contentBuf.Bytes(), &decodedUsers); err != nil {
128+
t.Fatalf("Output is not valid JSON: %v. Output:\n%s", err, contentBuf.String())
129+
}
130+
if len(decodedUsers) != len(allUsers) {
131+
t.Errorf("Expected %d users in JSON, got %d", len(allUsers), len(decodedUsers))
132+
}
133+
if len(decodedUsers) > 0 {
134+
if decodedUsers[0].Username != allUsers[0].Username {
135+
t.Errorf("Expected username '%s', got '%s'", allUsers[0].Username, decodedUsers[0].Username)
136+
}
137+
if decodedUsers[0].SysadminFlag != allUsers[0].SysadminFlag {
138+
t.Errorf("Expected SysadminFlag to be %v, got %v", allUsers[0].SysadminFlag, decodedUsers[0].SysadminFlag)
139+
}
140+
}
141+
case tt.outputFormat == "yaml":
142+
if contentBuf.Len() == 0 {
143+
t.Fatal("Expected YAML output, but buffer was empty")
144+
}
145+
var decodedUsers []*models.UserResp
146+
if err := yaml.Unmarshal(contentBuf.Bytes(), &decodedUsers); err != nil {
147+
t.Fatalf("Output is not valid YAML: %v. Output:\n%s", err, contentBuf.String())
148+
}
149+
if len(decodedUsers) != len(allUsers) {
150+
t.Errorf("Expected %d users in YAML, got %d", len(allUsers), len(decodedUsers))
151+
}
152+
if len(decodedUsers) > 0 {
153+
if decodedUsers[0].Username != allUsers[0].Username {
154+
t.Errorf("Expected username '%s', got '%s'", allUsers[0].Username, decodedUsers[0].Username)
155+
}
156+
if decodedUsers[0].SysadminFlag != allUsers[0].SysadminFlag {
157+
t.Errorf("Expected SysadminFlag to be %v, got %v", allUsers[0].SysadminFlag, decodedUsers[0].SysadminFlag)
158+
}
159+
}
160+
default:
161+
if contentBuf.Len() == 0 {
162+
t.Fatal("Expected TUI table output, but buffer was empty. Did you pass 'w' to ListUsers?")
163+
}
164+
output := contentBuf.String()
165+
if !strings.Contains(output, "ID") || !strings.Contains(output, "Name") || !strings.Contains(output, "Administrator") {
166+
t.Error("Expected table output to contain headers 'ID', 'Name' and 'Administrator among other headers")
167+
}
168+
if !strings.Contains(output, "testUser1") {
169+
t.Errorf("Expected table to contain username 'testUser1'")
170+
}
171+
}
172+
})
173+
}
174+
}

pkg/utils/helper.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ package utils
1616
import (
1717
"errors"
1818
"fmt"
19+
"io"
1920
"net"
2021
"net/url"
22+
"os"
2123
"regexp"
2224
"strconv"
2325
"strings"
@@ -194,17 +196,20 @@ func ValidateURL(rawURL string) error {
194196
return nil
195197
}
196198

197-
func PrintFormat[T any](resp T, format string) error {
199+
func FPrintFormat[T any](w io.Writer, resp T, format string) error {
198200
if format == "json" {
199-
PrintPayloadInJSONFormat(resp)
201+
FPrintPayloadInJSONFormat(w, resp)
200202
return nil
201203
}
202204
if format == "yaml" {
203-
PrintPayloadInYAMLFormat(resp)
205+
FPrintPayloadInYAMLFormat(w, resp)
204206
return nil
205207
}
206208
return fmt.Errorf("unable to output in the specified '%s' format", format)
207209
}
210+
func PrintFormat[T any](resp T, format string) error {
211+
return FPrintFormat(os.Stdout, resp, format)
212+
}
208213

209214
func EmptyStringValidator(variable string) func(string) error {
210215
return func(str string) error {

pkg/utils/utils.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"encoding/json"
1919
"errors"
2020
"fmt"
21+
"io"
2122
"os"
2223
"regexp"
2324
"strconv"
@@ -34,8 +35,7 @@ import (
3435
)
3536

3637
// Returns Harbor v2 client for given clientConfig
37-
38-
func PrintPayloadInJSONFormat(payload any) {
38+
func FPrintPayloadInJSONFormat(w io.Writer, payload any) {
3939
if payload == nil {
4040
return
4141
}
@@ -45,10 +45,10 @@ func PrintPayloadInJSONFormat(payload any) {
4545
panic(err)
4646
}
4747

48-
fmt.Println(string(jsonStr))
48+
fmt.Fprint(w, string(jsonStr))
4949
}
5050

51-
func PrintPayloadInYAMLFormat(payload any) {
51+
func FPrintPayloadInYAMLFormat(w io.Writer, payload any) {
5252
if payload == nil {
5353
return
5454
}
@@ -58,7 +58,15 @@ func PrintPayloadInYAMLFormat(payload any) {
5858
panic(err)
5959
}
6060

61-
fmt.Println(string(yamlStr))
61+
fmt.Fprint(w, string(yamlStr))
62+
}
63+
64+
func PrintPayloadInJSONFormat(payload any) {
65+
FPrintPayloadInJSONFormat(os.Stdout, payload)
66+
}
67+
68+
func PrintPayloadInYAMLFormat(payload any) {
69+
FPrintPayloadInYAMLFormat(os.Stdout, payload)
6270
}
6371

6472
func ParseProjectRepo(projectRepo string) (project, repo string, err error) {

0 commit comments

Comments
 (0)