Skip to content

Commit cfe7620

Browse files
Add test coverage and clean up lint warnings
- Add tests for config manager (Load, Write, Delete, TokenRefresh) - Add tests for GraphQLError, TruncateQuery, and MockClient - Add tests for pagination, style, and errors packages - Replace unused function params with _ across test files - Use value receivers on captureSentry stub methods - Collapse repeated append calls in root.go buildExampleText - Tighten file permissions in completion install (750/600)
1 parent d2b2def commit cfe7620

11 files changed

Lines changed: 713 additions & 14 deletions

File tree

command/completion/completion.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ func NewCmdCompletion() *cobra.Command {
1111
return &cobra.Command{
1212
Use: "completion",
1313
Short: "Install shell completions for bash, zsh, or fish",
14-
RunE: func(cmd *cobra.Command, args []string) error {
14+
RunE: func(cmd *cobra.Command, _ []string) error {
1515
return completion.Install(cmd.Root(), os.Stderr)
1616
},
1717
}

command/issues/tests/issues_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,7 +806,7 @@ func TestIssuesPaginationHardCap(t *testing.T) {
806806

807807
mock := graphqlclient.NewMockClient()
808808
callCount := 0
809-
mock.QueryFunc = func(_ context.Context, query string, vars map[string]any, result any) error {
809+
mock.QueryFunc = func(_ context.Context, query string, _ map[string]any, result any) error {
810810
callCount++
811811
return json.Unmarshal(pageJSON, result)
812812
}

command/root.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,7 @@ func buildExampleText() string {
115115
}
116116
var lines []string
117117
for _, ex := range examples {
118-
lines = append(lines, style.Gray("# %s", ex.comment))
119-
lines = append(lines, ex.cmd)
120-
lines = append(lines, "")
118+
lines = append(lines, style.Gray("# %s", ex.comment), ex.cmd, "")
121119
}
122120
// Remove trailing blank line
123121
if len(lines) > 0 {

config/manager_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package config
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57

8+
"github.com/deepsourcelabs/cli/buildinfo"
69
"github.com/deepsourcelabs/cli/internal/adapters"
710
"github.com/deepsourcelabs/cli/internal/secrets"
11+
"github.com/pelletier/go-toml"
812
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
914
)
1015

1116
type fakeSecretStore struct {
@@ -40,3 +45,151 @@ func TestManagerLoadTokenFromSecrets(t *testing.T) {
4045
assert.NoError(t, err)
4146
assert.Equal(t, "secret-token", cfg.Token)
4247
}
48+
49+
func TestManagerLoadFromFile(t *testing.T) {
50+
tempDir := t.TempDir()
51+
homeDir := func() (string, error) { return tempDir, nil }
52+
53+
configDir := filepath.Join(tempDir, buildinfo.ConfigDirName)
54+
require.NoError(t, os.MkdirAll(configDir, 0o700))
55+
56+
tomlData := `host = "app.deepsource.io"
57+
user = "alice"
58+
token = "file-token"
59+
`
60+
require.NoError(t, os.WriteFile(filepath.Join(configDir, ConfigFileName), []byte(tomlData), 0o644))
61+
62+
mgr := NewManager(adapters.NewOSFileSystem(), homeDir)
63+
cfg, err := mgr.Load()
64+
require.NoError(t, err)
65+
assert.Equal(t, "app.deepsource.io", cfg.Host)
66+
assert.Equal(t, "alice", cfg.User)
67+
assert.Equal(t, "file-token", cfg.Token)
68+
}
69+
70+
func TestManagerLoadNoFile(t *testing.T) {
71+
tempDir := t.TempDir()
72+
homeDir := func() (string, error) { return tempDir, nil }
73+
74+
mgr := NewManager(adapters.NewOSFileSystem(), homeDir)
75+
cfg, err := mgr.Load()
76+
require.NoError(t, err)
77+
assert.Empty(t, cfg.Host)
78+
assert.Empty(t, cfg.User)
79+
assert.Empty(t, cfg.Token)
80+
}
81+
82+
func TestManagerWrite(t *testing.T) {
83+
tempDir := t.TempDir()
84+
homeDir := func() (string, error) { return tempDir, nil }
85+
86+
// Use NoopStore so token stays in the TOML file
87+
mgr := NewManager(adapters.NewOSFileSystem(), homeDir)
88+
err := mgr.Write(&CLIConfig{
89+
Host: "example.com",
90+
User: "bob",
91+
})
92+
require.NoError(t, err)
93+
94+
// Read back the raw TOML
95+
path := filepath.Join(tempDir, buildinfo.ConfigDirName, ConfigFileName)
96+
data, err := os.ReadFile(path)
97+
require.NoError(t, err)
98+
99+
var got CLIConfig
100+
require.NoError(t, toml.Unmarshal(data, &got))
101+
assert.Equal(t, "example.com", got.Host)
102+
assert.Equal(t, "bob", got.User)
103+
}
104+
105+
func TestManagerWriteStoresTokenInSecrets(t *testing.T) {
106+
tempDir := t.TempDir()
107+
homeDir := func() (string, error) { return tempDir, nil }
108+
store := &fakeSecretStore{data: make(map[string]string)}
109+
110+
mgr := NewManagerWithSecrets(adapters.NewOSFileSystem(), homeDir, store, "mykey")
111+
err := mgr.Write(&CLIConfig{
112+
Host: "example.com",
113+
Token: "super-secret",
114+
})
115+
require.NoError(t, err)
116+
117+
// Token should be in secret store
118+
assert.Equal(t, "super-secret", store.data["mykey"])
119+
120+
// Token should NOT be in the TOML file
121+
path := filepath.Join(tempDir, buildinfo.ConfigDirName, ConfigFileName)
122+
data, err := os.ReadFile(path)
123+
require.NoError(t, err)
124+
125+
var got CLIConfig
126+
require.NoError(t, toml.Unmarshal(data, &got))
127+
assert.Empty(t, got.Token, "token should not be written to TOML when secret store succeeds")
128+
}
129+
130+
func TestManagerDelete(t *testing.T) {
131+
tempDir := t.TempDir()
132+
homeDir := func() (string, error) { return tempDir, nil }
133+
store := &fakeSecretStore{data: map[string]string{"mykey": "tok"}}
134+
135+
mgr := NewManagerWithSecrets(adapters.NewOSFileSystem(), homeDir, store, "mykey")
136+
137+
// Write a config first
138+
require.NoError(t, mgr.Write(&CLIConfig{Host: "example.com", Token: "tok"}))
139+
140+
// Delete it
141+
require.NoError(t, mgr.Delete())
142+
143+
// File should be gone
144+
path := filepath.Join(tempDir, buildinfo.ConfigDirName, ConfigFileName)
145+
_, err := os.Stat(path)
146+
assert.True(t, os.IsNotExist(err), "config file should be deleted")
147+
148+
// Secret should be gone
149+
_, ok := store.data["mykey"]
150+
assert.False(t, ok, "secret should be deleted")
151+
}
152+
153+
func TestManagerDeleteNonExistent(t *testing.T) {
154+
tempDir := t.TempDir()
155+
homeDir := func() (string, error) { return tempDir, nil }
156+
157+
mgr := NewManager(adapters.NewOSFileSystem(), homeDir)
158+
err := mgr.Delete()
159+
assert.NoError(t, err, "deleting non-existent config should not error")
160+
}
161+
162+
func TestManagerTokenRefreshCallback(t *testing.T) {
163+
tempDir := t.TempDir()
164+
homeDir := func() (string, error) { return tempDir, nil }
165+
store := &fakeSecretStore{data: make(map[string]string)}
166+
167+
mgr := NewManagerWithSecrets(adapters.NewOSFileSystem(), homeDir, store, "key")
168+
169+
// Write initial config
170+
require.NoError(t, mgr.Write(&CLIConfig{Host: "example.com", User: "old@test.com", Token: "old-token"}))
171+
172+
// Invoke the refresh callback
173+
cb := mgr.TokenRefreshCallback()
174+
cb("new-token", "2030-01-01T00:00:00Z", "new@test.com")
175+
176+
// Reload and verify
177+
cfg, err := mgr.Load()
178+
require.NoError(t, err)
179+
assert.Equal(t, "new-token", cfg.Token)
180+
assert.Equal(t, "new@test.com", cfg.User)
181+
assert.False(t, cfg.TokenExpiresIn.IsZero(), "token expiry should be set")
182+
}
183+
184+
func TestNewManagerDefaults(t *testing.T) {
185+
homeDir := func() (string, error) { return "/tmp", nil }
186+
187+
// NewManager uses NoopStore
188+
mgr := NewManager(adapters.NewOSFileSystem(), homeDir)
189+
assert.NotNil(t, mgr)
190+
191+
// nil store defaults to NoopStore, empty key defaults to KeychainKey
192+
mgr2 := NewManagerWithSecrets(adapters.NewOSFileSystem(), homeDir, nil, "")
193+
assert.NotNil(t, mgr2)
194+
assert.Equal(t, buildinfo.KeychainKey, mgr2.secretsKey)
195+
}

deepsource/graphqlclient/transport_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package graphqlclient
22

33
import (
4+
"context"
5+
"errors"
46
"fmt"
57
"io"
68
"net/http"
@@ -77,3 +79,126 @@ func TestIsTokenExpired(t *testing.T) {
7779
}
7880
}
7981
}
82+
83+
// --- GraphQLError tests ---
84+
85+
func TestGraphQLError_Error(t *testing.T) {
86+
err := &GraphQLError{
87+
Operation: "query",
88+
Query: "{ viewer { login } }",
89+
Cause: fmt.Errorf("connection refused"),
90+
}
91+
want := "GraphQL query failed: connection refused"
92+
if got := err.Error(); got != want {
93+
t.Errorf("Error() = %q, want %q", got, want)
94+
}
95+
}
96+
97+
func TestGraphQLError_Unwrap(t *testing.T) {
98+
cause := fmt.Errorf("root cause")
99+
err := &GraphQLError{Operation: "mutation", Cause: cause}
100+
if err.Unwrap() != cause {
101+
t.Error("Unwrap() should return the cause")
102+
}
103+
}
104+
105+
// --- TruncateQuery tests ---
106+
107+
func TestTruncateQuery_Short(t *testing.T) {
108+
q := "{ viewer { login } }"
109+
if got := TruncateQuery(q); got != q {
110+
t.Errorf("short query should be returned as-is, got %q", got)
111+
}
112+
}
113+
114+
func TestTruncateQuery_Long(t *testing.T) {
115+
q := strings.Repeat("a", 150)
116+
got := TruncateQuery(q)
117+
if len(got) != 102 { // 100 + ".."
118+
t.Errorf("expected length 102, got %d", len(got))
119+
}
120+
if !strings.HasSuffix(got, "..") {
121+
t.Errorf("expected '..' suffix, got %q", got)
122+
}
123+
}
124+
125+
func TestTruncateQuery_Exact100(t *testing.T) {
126+
q := strings.Repeat("b", 100)
127+
if got := TruncateQuery(q); got != q {
128+
t.Error("100-char query should be returned as-is")
129+
}
130+
}
131+
132+
// --- wrapper tests ---
133+
134+
func TestNew(t *testing.T) {
135+
client := New("https://api.deepsource.com/graphql/", "tok")
136+
if client == nil {
137+
t.Fatal("New() returned nil")
138+
}
139+
}
140+
141+
func TestSetAuthToken(t *testing.T) {
142+
client := New("https://api.deepsource.com/graphql/", "old-token")
143+
client.SetAuthToken("new-token")
144+
// Verify via the wrapper's internal state
145+
w, ok := client.(*wrapper)
146+
if !ok {
147+
t.Fatal("expected *wrapper type")
148+
}
149+
if w.token != "new-token" {
150+
t.Errorf("token = %q, want %q", w.token, "new-token")
151+
}
152+
}
153+
154+
func TestMockClient_QueryDelegation(t *testing.T) {
155+
called := false
156+
mock := NewMockClient()
157+
mock.QueryFunc = func(ctx context.Context, query string, vars map[string]any, result any) error {
158+
called = true
159+
if query != "{ viewer }" {
160+
t.Errorf("query = %q, want %q", query, "{ viewer }")
161+
}
162+
return nil
163+
}
164+
165+
err := mock.Query(context.Background(), "{ viewer }", nil, nil)
166+
if err != nil {
167+
t.Fatalf("unexpected error: %v", err)
168+
}
169+
if !called {
170+
t.Error("QueryFunc was not called")
171+
}
172+
}
173+
174+
func TestMockClient_MutateDelegation(t *testing.T) {
175+
wantErr := errors.New("mutation failed")
176+
mock := NewMockClient()
177+
mock.MutateFunc = func(ctx context.Context, mutation string, vars map[string]any, result any) error {
178+
return wantErr
179+
}
180+
181+
err := mock.Mutate(context.Background(), "mutation { ... }", nil, nil)
182+
if err != wantErr {
183+
t.Errorf("err = %v, want %v", err, wantErr)
184+
}
185+
}
186+
187+
func TestMockClient_NilFuncs(t *testing.T) {
188+
mock := NewMockClient()
189+
// Nil QueryFunc/MutateFunc should return nil
190+
if err := mock.Query(context.Background(), "", nil, nil); err != nil {
191+
t.Errorf("nil QueryFunc should return nil, got %v", err)
192+
}
193+
if err := mock.Mutate(context.Background(), "", nil, nil); err != nil {
194+
t.Errorf("nil MutateFunc should return nil, got %v", err)
195+
}
196+
}
197+
198+
func TestMockClient_SetAuthToken(t *testing.T) {
199+
mock := NewMockClient()
200+
mock.SetAuthToken("test-token")
201+
if mock.token != "test-token" {
202+
t.Errorf("token = %q, want %q", mock.token, "test-token")
203+
}
204+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package pagination
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
)
7+
8+
func TestConstants(t *testing.T) {
9+
if DefaultPageSize != 100 {
10+
t.Errorf("DefaultPageSize = %d, want 100", DefaultPageSize)
11+
}
12+
if NestedPageSize != 500 {
13+
t.Errorf("NestedPageSize = %d, want 500", NestedPageSize)
14+
}
15+
if MaxResults != 1000 {
16+
t.Errorf("MaxResults = %d, want 1000", MaxResults)
17+
}
18+
}
19+
20+
func TestPageInfo_JSONRoundTrip(t *testing.T) {
21+
cursor := "abc123"
22+
original := PageInfo{HasNextPage: true, EndCursor: &cursor}
23+
24+
data, err := json.Marshal(original)
25+
if err != nil {
26+
t.Fatalf("Marshal: %v", err)
27+
}
28+
29+
var decoded PageInfo
30+
if err := json.Unmarshal(data, &decoded); err != nil {
31+
t.Fatalf("Unmarshal: %v", err)
32+
}
33+
34+
if decoded.HasNextPage != original.HasNextPage {
35+
t.Errorf("HasNextPage = %v, want %v", decoded.HasNextPage, original.HasNextPage)
36+
}
37+
if decoded.EndCursor == nil || *decoded.EndCursor != cursor {
38+
t.Errorf("EndCursor = %v, want %q", decoded.EndCursor, cursor)
39+
}
40+
}
41+
42+
func TestPageInfo_NilCursor(t *testing.T) {
43+
original := PageInfo{HasNextPage: false, EndCursor: nil}
44+
45+
data, err := json.Marshal(original)
46+
if err != nil {
47+
t.Fatalf("Marshal: %v", err)
48+
}
49+
50+
var decoded PageInfo
51+
if err := json.Unmarshal(data, &decoded); err != nil {
52+
t.Fatalf("Unmarshal: %v", err)
53+
}
54+
55+
if decoded.HasNextPage != false {
56+
t.Error("HasNextPage should be false")
57+
}
58+
if decoded.EndCursor != nil {
59+
t.Errorf("EndCursor should be nil, got %v", decoded.EndCursor)
60+
}
61+
}

0 commit comments

Comments
 (0)