Skip to content

Commit ca87f1d

Browse files
authored
test: add test coverage for untested files (#388)
* test: add test coverage for untested files * chore: add license headers to new fils * style: use Test_Function naming convention in new test files * refactor: consolidate collaborator tests into table-driven tests * chore: add TODO comments for afero.Fs refactor in archiveutil tests * test: fix casing of README.md in ShouldIgnore test data * chore: add TODO comments for afero.Fs refactor in recursivecopy tests * refactor: use slackdeps.NewFsMock() in image tests * test: fix GetHostname() test for unknown
1 parent 23baa9e commit ca87f1d

File tree

10 files changed

+1133
-0
lines changed

10 files changed

+1133
-0
lines changed

internal/api/client_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2022-2026 Salesforce, Inc.
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 api
16+
17+
import (
18+
"testing"
19+
20+
"github.com/stretchr/testify/assert"
21+
)
22+
23+
func Test_Client_Host(t *testing.T) {
24+
tests := map[string]struct {
25+
host string
26+
expected string
27+
}{
28+
"returns the configured host": {
29+
host: "https://slack.com",
30+
expected: "https://slack.com",
31+
},
32+
"returns empty when unset": {
33+
host: "",
34+
expected: "",
35+
},
36+
}
37+
for name, tc := range tests {
38+
t.Run(name, func(t *testing.T) {
39+
c := &Client{host: tc.host}
40+
assert.Equal(t, tc.expected, c.Host())
41+
})
42+
}
43+
}
44+
45+
func Test_Client_SetHost(t *testing.T) {
46+
tests := map[string]struct {
47+
initial string
48+
newHost string
49+
}{
50+
"sets a new host": {
51+
initial: "",
52+
newHost: "https://dev.slack.com",
53+
},
54+
"overwrites existing host": {
55+
initial: "https://slack.com",
56+
newHost: "https://dev.slack.com",
57+
},
58+
}
59+
for name, tc := range tests {
60+
t.Run(name, func(t *testing.T) {
61+
c := &Client{host: tc.initial}
62+
c.SetHost(tc.newHost)
63+
assert.Equal(t, tc.newHost, c.Host())
64+
})
65+
}
66+
}

internal/api/collaborators_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright 2022-2026 Salesforce, Inc.
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 api
16+
17+
import (
18+
"testing"
19+
20+
"github.com/slackapi/slack-cli/internal/shared/types"
21+
"github.com/slackapi/slack-cli/internal/slackcontext"
22+
"github.com/stretchr/testify/require"
23+
)
24+
25+
func Test_Client_AddCollaborator(t *testing.T) {
26+
tests := map[string]struct {
27+
response string
28+
user types.SlackUser
29+
expectErr string
30+
}{
31+
"adds a collaborator by email": {
32+
response: `{"ok":true}`,
33+
user: types.SlackUser{Email: "user@example.com", PermissionType: "owner"},
34+
},
35+
"adds a collaborator by user ID": {
36+
response: `{"ok":true}`,
37+
user: types.SlackUser{ID: "U123", PermissionType: "owner"},
38+
},
39+
"returns error when user not found": {
40+
response: `{"ok":false,"error":"user_not_found"}`,
41+
user: types.SlackUser{Email: "bad@example.com"},
42+
expectErr: "user_not_found",
43+
},
44+
}
45+
for name, tc := range tests {
46+
t.Run(name, func(t *testing.T) {
47+
ctx := slackcontext.MockContext(t.Context())
48+
c, teardown := NewFakeClient(t, FakeClientParams{
49+
ExpectedMethod: collaboratorsAddMethod,
50+
Response: tc.response,
51+
})
52+
defer teardown()
53+
err := c.AddCollaborator(ctx, "token", "A123", tc.user)
54+
if tc.expectErr != "" {
55+
require.Error(t, err)
56+
require.Contains(t, err.Error(), tc.expectErr)
57+
} else {
58+
require.NoError(t, err)
59+
}
60+
})
61+
}
62+
}
63+
64+
func Test_Client_ListCollaborators(t *testing.T) {
65+
tests := map[string]struct {
66+
response string
67+
expectedCount int
68+
expectedID string
69+
expectErr string
70+
}{
71+
"returns a list of collaborators": {
72+
response: `{"ok":true,"owners":[{"user_id":"U123","username":"Test User"}]}`,
73+
expectedCount: 1,
74+
expectedID: "U123",
75+
},
76+
"returns error when app not found": {
77+
response: `{"ok":false,"error":"app_not_found"}`,
78+
expectErr: "app_not_found",
79+
},
80+
}
81+
for name, tc := range tests {
82+
t.Run(name, func(t *testing.T) {
83+
ctx := slackcontext.MockContext(t.Context())
84+
c, teardown := NewFakeClient(t, FakeClientParams{
85+
ExpectedMethod: collaboratorsListMethod,
86+
Response: tc.response,
87+
})
88+
defer teardown()
89+
users, err := c.ListCollaborators(ctx, "token", "A123")
90+
if tc.expectErr != "" {
91+
require.Error(t, err)
92+
require.Contains(t, err.Error(), tc.expectErr)
93+
} else {
94+
require.NoError(t, err)
95+
require.Len(t, users, tc.expectedCount)
96+
require.Equal(t, tc.expectedID, users[0].ID)
97+
}
98+
})
99+
}
100+
}
101+
102+
func Test_Client_RemoveCollaborator(t *testing.T) {
103+
tests := map[string]struct {
104+
response string
105+
user types.SlackUser
106+
expectErr string
107+
}{
108+
"removes a collaborator": {
109+
response: `{"ok":true}`,
110+
user: types.SlackUser{ID: "U123"},
111+
},
112+
"returns error when removing owner": {
113+
response: `{"ok":false,"error":"cannot_remove_owner"}`,
114+
user: types.SlackUser{Email: "owner@example.com"},
115+
expectErr: "cannot_remove_owner",
116+
},
117+
}
118+
for name, tc := range tests {
119+
t.Run(name, func(t *testing.T) {
120+
ctx := slackcontext.MockContext(t.Context())
121+
c, teardown := NewFakeClient(t, FakeClientParams{
122+
ExpectedMethod: collaboratorsRemoveMethod,
123+
Response: tc.response,
124+
})
125+
defer teardown()
126+
warnings, err := c.RemoveCollaborator(ctx, "token", "A123", tc.user)
127+
if tc.expectErr != "" {
128+
require.Error(t, err)
129+
require.Contains(t, err.Error(), tc.expectErr)
130+
} else {
131+
require.NoError(t, err)
132+
require.Empty(t, warnings)
133+
}
134+
})
135+
}
136+
}
137+
138+
func Test_Client_UpdateCollaborator(t *testing.T) {
139+
tests := map[string]struct {
140+
response string
141+
user types.SlackUser
142+
expectErr string
143+
}{
144+
"updates a collaborator": {
145+
response: `{"ok":true}`,
146+
user: types.SlackUser{ID: "U123", PermissionType: "collaborator"},
147+
},
148+
"returns error for invalid permission": {
149+
response: `{"ok":false,"error":"invalid_permission"}`,
150+
user: types.SlackUser{ID: "U123", PermissionType: "invalid"},
151+
expectErr: "invalid_permission",
152+
},
153+
}
154+
for name, tc := range tests {
155+
t.Run(name, func(t *testing.T) {
156+
ctx := slackcontext.MockContext(t.Context())
157+
c, teardown := NewFakeClient(t, FakeClientParams{
158+
ExpectedMethod: collaboratorsUpdateMethod,
159+
Response: tc.response,
160+
})
161+
defer teardown()
162+
err := c.UpdateCollaborator(ctx, "token", "A123", tc.user)
163+
if tc.expectErr != "" {
164+
require.Error(t, err)
165+
require.Contains(t, err.Error(), tc.expectErr)
166+
} else {
167+
require.NoError(t, err)
168+
}
169+
})
170+
}
171+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2022-2026 Salesforce, Inc.
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 archiveutil
16+
17+
import (
18+
"archive/tar"
19+
"archive/zip"
20+
"compress/gzip"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
"testing"
25+
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
)
29+
30+
// TODO: Refactor to use afero.Fs once ExtractAndWriteFile accepts it. Currently uses t.TempDir() which is safe.
31+
func Test_ExtractAndWriteFile(t *testing.T) {
32+
tests := map[string]struct {
33+
fileName string
34+
content string
35+
isDir bool
36+
}{
37+
"extracts a regular file": {
38+
fileName: "hello.txt",
39+
content: "hello world",
40+
isDir: false,
41+
},
42+
"creates a directory": {
43+
fileName: "subdir/",
44+
isDir: true,
45+
},
46+
}
47+
for name, tc := range tests {
48+
t.Run(name, func(t *testing.T) {
49+
destDir := t.TempDir()
50+
reader := strings.NewReader(tc.content)
51+
52+
path, err := ExtractAndWriteFile(reader, destDir, tc.fileName, tc.isDir, 0644)
53+
require.NoError(t, err)
54+
assert.True(t, strings.HasPrefix(path, destDir))
55+
56+
if tc.isDir {
57+
info, err := os.Stat(path)
58+
require.NoError(t, err)
59+
assert.True(t, info.IsDir())
60+
} else {
61+
content, err := os.ReadFile(path)
62+
require.NoError(t, err)
63+
assert.Equal(t, tc.content, string(content))
64+
}
65+
})
66+
}
67+
68+
t.Run("rejects path traversal", func(t *testing.T) {
69+
destDir := t.TempDir()
70+
reader := strings.NewReader("malicious")
71+
_, err := ExtractAndWriteFile(reader, destDir, "../../../etc/passwd", false, 0644)
72+
assert.Error(t, err)
73+
assert.Contains(t, err.Error(), "illegal file path")
74+
})
75+
}
76+
77+
// TODO: Refactor to use afero.Fs once Unzip accepts it. Currently uses t.TempDir() which is safe.
78+
func Test_Unzip(t *testing.T) {
79+
t.Run("extracts a zip archive", func(t *testing.T) {
80+
srcDir := t.TempDir()
81+
destDir := t.TempDir()
82+
83+
// Create a zip file
84+
zipPath := filepath.Join(srcDir, "test.zip")
85+
zipFile, err := os.Create(zipPath)
86+
require.NoError(t, err)
87+
88+
w := zip.NewWriter(zipFile)
89+
f, err := w.Create("test.txt")
90+
require.NoError(t, err)
91+
_, err = f.Write([]byte("zip content"))
92+
require.NoError(t, err)
93+
err = w.Close()
94+
require.NoError(t, err)
95+
err = zipFile.Close()
96+
require.NoError(t, err)
97+
98+
files, err := Unzip(zipPath, destDir)
99+
require.NoError(t, err)
100+
assert.NotEmpty(t, files)
101+
102+
content, err := os.ReadFile(filepath.Join(destDir, "test.txt"))
103+
require.NoError(t, err)
104+
assert.Equal(t, "zip content", string(content))
105+
})
106+
107+
t.Run("returns error for non-existent file", func(t *testing.T) {
108+
_, err := Unzip("/nonexistent.zip", t.TempDir())
109+
assert.Error(t, err)
110+
})
111+
}
112+
113+
// TODO: Refactor to use afero.Fs once UntarGzip accepts it. Currently uses t.TempDir() which is safe.
114+
func Test_UntarGzip(t *testing.T) {
115+
t.Run("extracts a tar.gz archive", func(t *testing.T) {
116+
srcDir := t.TempDir()
117+
destDir := t.TempDir()
118+
119+
// Create a tar.gz file
120+
tgzPath := filepath.Join(srcDir, "test.tar.gz")
121+
tgzFile, err := os.Create(tgzPath)
122+
require.NoError(t, err)
123+
124+
gw := gzip.NewWriter(tgzFile)
125+
tw := tar.NewWriter(gw)
126+
127+
content := []byte("tar content")
128+
hdr := &tar.Header{
129+
Name: "test.txt",
130+
Mode: 0644,
131+
Size: int64(len(content)),
132+
Typeflag: tar.TypeReg,
133+
}
134+
err = tw.WriteHeader(hdr)
135+
require.NoError(t, err)
136+
_, err = tw.Write(content)
137+
require.NoError(t, err)
138+
139+
err = tw.Close()
140+
require.NoError(t, err)
141+
err = gw.Close()
142+
require.NoError(t, err)
143+
err = tgzFile.Close()
144+
require.NoError(t, err)
145+
146+
files, err := UntarGzip(tgzPath, destDir)
147+
require.NoError(t, err)
148+
assert.NotEmpty(t, files)
149+
150+
data, err := os.ReadFile(filepath.Join(destDir, "test.txt"))
151+
require.NoError(t, err)
152+
assert.Equal(t, "tar content", string(data))
153+
})
154+
155+
t.Run("returns error for non-existent file", func(t *testing.T) {
156+
_, err := UntarGzip("/nonexistent.tar.gz", t.TempDir())
157+
assert.Error(t, err)
158+
})
159+
}

0 commit comments

Comments
 (0)