Skip to content

Commit dc27ffe

Browse files
committed
test: significantly improve test coverage and edge cases handling across core packages
- **cmd & internal/pkg/http**: Mocked backend HTTP requests enabling stable network-independent command integration tests (e2e). - **internal/plugin**: Add comprehensive tests for BackendRPCClient and ProviderRPCClient utilizing net.Pipe(). - **internal/provider/native**: Introduce table-driven unit tests for language provider recipes. - **internal/task & internal/lockfile**: Improve test coverage to 95%+, covering timeouts, circular dependency validation, and error handling. - **Cleanup**: Remove temporary testing artifacts.
1 parent 544e444 commit dc27ffe

11 files changed

Lines changed: 820 additions & 9 deletions

File tree

internal/lockfile/validate_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package lockfile
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestValidate_Errors(t *testing.T) {
10+
lf := &LockFile{
11+
Tools: map[string][]*ToolLockEntry{
12+
"empty": {},
13+
"bad_version": {
14+
{Version: ""},
15+
{Version: "1.0"},
16+
{Version: "1.0"}, // duplicate
17+
},
18+
"bad_platform": {
19+
{
20+
Version: "2.0",
21+
Platforms: map[string]*PlatformEntry{
22+
"invalid-key": {},
23+
"linux-amd64": nil,
24+
"windows-amd64": {Checksum: "invalid", Size: -1, URL: "ftp://bad"},
25+
},
26+
},
27+
},
28+
},
29+
}
30+
31+
err := lf.Validate()
32+
assert.Error(t, err)
33+
34+
ve, ok := err.(*ValidationError)
35+
assert.True(t, ok)
36+
37+
errMsg := ve.Error()
38+
assert.Contains(t, errMsg, "tool \"empty\": empty entry list")
39+
assert.Contains(t, errMsg, "tool \"bad_version\": entry has empty version")
40+
assert.Contains(t, errMsg, "tool \"bad_version\": duplicate version \"1.0\"")
41+
assert.Contains(t, errMsg, "unknown platform key \"invalid-key\"")
42+
assert.Contains(t, errMsg, "nil entry")
43+
assert.Contains(t, errMsg, "checksum \"invalid\" must start with 'sha256:' or 'blake3:'")
44+
assert.Contains(t, errMsg, "size must be ≥ 0, got -1")
45+
assert.Contains(t, errMsg, "url \"ftp://bad\" does not look like a valid HTTP URL")
46+
}
47+
48+
func TestCheckStrict_Errors(t *testing.T) {
49+
lf := &LockFile{
50+
Tools: map[string][]*ToolLockEntry{
51+
"go": {
52+
{
53+
Version: "1.20",
54+
Backend: "go",
55+
Platforms: map[string]*PlatformEntry{
56+
"linux-amd64": {},
57+
},
58+
},
59+
},
60+
"github:cli/cli": {
61+
{
62+
Version: "2.0",
63+
Backend: "github",
64+
Platforms: map[string]*PlatformEntry{
65+
"linux-amd64": {URL: ""},
66+
},
67+
},
68+
},
69+
},
70+
}
71+
72+
// Missing tool
73+
err := lf.CheckStrict([]LockRequirement{
74+
{ToolKey: "missing", Version: "1.0"},
75+
})
76+
assert.Error(t, err)
77+
assert.Contains(t, err.Error(), "no locked entry for tool=\"missing\" version=\"1.0\"")
78+
79+
// Missing platform
80+
err = lf.CheckStrict([]LockRequirement{
81+
{ToolKey: "go", Version: "1.20", PlatformKey: "darwin-arm64"},
82+
})
83+
assert.Error(t, err)
84+
assert.Contains(t, err.Error(), "no locked platform \"darwin-arm64\" for tool=\"go\"")
85+
86+
// Missing URL for backend that requires it
87+
err = lf.CheckStrict([]LockRequirement{
88+
{ToolKey: "github:cli/cli", Version: "2.0", PlatformKey: "linux-amd64"},
89+
})
90+
assert.Error(t, err)
91+
assert.Contains(t, err.Error(), "no locked URL for tool=\"github:cli/cli\"")
92+
93+
// Valid URL for backend that doesn't require it
94+
err = lf.CheckStrict([]LockRequirement{
95+
{ToolKey: "go", Version: "1.20", PlatformKey: "linux-amd64"},
96+
})
97+
assert.NoError(t, err)
98+
}

internal/pkg/http/client.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,25 +68,28 @@ func DefaultTransport() *http.Transport {
6868
DisableHTTP2(trans)
6969
}
7070

71-
if MockTransport != nil {
72-
trans.RegisterProtocol("http", MockTransport)
73-
trans.RegisterProtocol("https", MockTransport)
74-
}
75-
7671
return trans
7772
}
7873

7974
// NewClient returns an http.Client pre-configured with UniRTM's robust transport.
8075
func NewClient() *http.Client {
76+
var tr http.RoundTripper = DefaultTransport()
77+
if MockTransport != nil {
78+
tr = MockTransport
79+
}
8180
return &http.Client{
82-
Transport: DefaultTransport(),
81+
Transport: tr,
8382
}
8483
}
8584

8685
// NewClientWithTimeout returns an http.Client with a timeout and the robust transport.
8786
func NewClientWithTimeout(timeout time.Duration) *http.Client {
87+
var tr http.RoundTripper = DefaultTransport()
88+
if MockTransport != nil {
89+
tr = MockTransport
90+
}
8891
return &http.Client{
8992
Timeout: timeout,
90-
Transport: DefaultTransport(),
93+
Transport: tr,
9194
}
9295
}

internal/plugin/backend_rpc.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,69 @@ func (m *BackendRPCClient) SupportsGPG() bool {
101101
return resp
102102
}
103103

104+
func (m *BackendRPCClient) AttestationType() string {
105+
var resp string
106+
err := m.client.Call("Plugin.AttestationType", new(interface{}), &resp)
107+
if err != nil {
108+
return ""
109+
}
110+
return resp
111+
}
112+
113+
func (m *BackendRPCClient) IsRecommended() bool {
114+
var resp bool
115+
err := m.client.Call("Plugin.IsRecommended", new(interface{}), &resp)
116+
if err != nil {
117+
return false
118+
}
119+
return resp
120+
}
121+
122+
func (m *BackendRPCClient) IsScriptless() bool {
123+
var resp bool
124+
err := m.client.Call("Plugin.IsScriptless", new(interface{}), &resp)
125+
if err != nil {
126+
return false
127+
}
128+
return resp
129+
}
130+
131+
func (m *BackendRPCClient) GetReach() string {
132+
var resp string
133+
err := m.client.Call("Plugin.GetReach", new(interface{}), &resp)
134+
if err != nil {
135+
return ""
136+
}
137+
return resp
138+
}
139+
140+
func (m *BackendRPCClient) IsStable() bool {
141+
var resp bool
142+
err := m.client.Call("Plugin.IsStable", new(interface{}), &resp)
143+
if err != nil {
144+
return false
145+
}
146+
return resp
147+
}
148+
149+
func (m *BackendRPCClient) SupportsOffline() bool {
150+
var resp bool
151+
err := m.client.Call("Plugin.SupportsOffline", new(interface{}), &resp)
152+
if err != nil {
153+
return false
154+
}
155+
return resp
156+
}
157+
158+
func (m *BackendRPCClient) Dependencies() []string {
159+
var resp []string
160+
err := m.client.Call("Plugin.Dependencies", new(interface{}), &resp)
161+
if err != nil {
162+
return nil
163+
}
164+
return resp
165+
}
166+
104167
// BackendRPCServer is the RPC server that BackendRPCClient talks to, conforming to
105168
// the requirements of net/rpc.
106169
type BackendRPCServer struct {
@@ -145,6 +208,41 @@ func (s *BackendRPCServer) SupportsGPG(args interface{}, resp *bool) error {
145208
return nil
146209
}
147210

211+
func (s *BackendRPCServer) AttestationType(args interface{}, resp *string) error {
212+
*resp = s.Impl.AttestationType()
213+
return nil
214+
}
215+
216+
func (s *BackendRPCServer) IsRecommended(args interface{}, resp *bool) error {
217+
*resp = s.Impl.IsRecommended()
218+
return nil
219+
}
220+
221+
func (s *BackendRPCServer) IsScriptless(args interface{}, resp *bool) error {
222+
*resp = s.Impl.IsScriptless()
223+
return nil
224+
}
225+
226+
func (s *BackendRPCServer) GetReach(args interface{}, resp *string) error {
227+
*resp = s.Impl.GetReach()
228+
return nil
229+
}
230+
231+
func (s *BackendRPCServer) IsStable(args interface{}, resp *bool) error {
232+
*resp = s.Impl.IsStable()
233+
return nil
234+
}
235+
236+
func (s *BackendRPCServer) SupportsOffline(args interface{}, resp *bool) error {
237+
*resp = s.Impl.SupportsOffline()
238+
return nil
239+
}
240+
241+
func (s *BackendRPCServer) Dependencies(args interface{}, resp *[]string) error {
242+
*resp = s.Impl.Dependencies()
243+
return nil
244+
}
245+
148246
// BackendPlugin is the implementation of plugin.Plugin so we can serve/consume this
149247
//
150248
// This has two methods: Server must return an RPC server for this plugin
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package plugin
2+
3+
import (
4+
"context"
5+
"net"
6+
"net/rpc"
7+
"testing"
8+
9+
"github.com/snowdreamtech/unirtm/internal/backend"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/mock"
12+
)
13+
14+
type mockBackend struct {
15+
mock.Mock
16+
}
17+
18+
func (m *mockBackend) Name() string {
19+
args := m.Called()
20+
return args.String(0)
21+
}
22+
23+
func (m *mockBackend) ListVersions(ctx context.Context, tool string, platform backend.Platform) ([]backend.VersionInfo, error) {
24+
args := m.Called(ctx, tool, platform)
25+
if args.Get(0) != nil {
26+
return args.Get(0).([]backend.VersionInfo), args.Error(1)
27+
}
28+
return nil, args.Error(1)
29+
}
30+
31+
func (m *mockBackend) ResolveVersion(ctx context.Context, tool string, versionRequest string, platform backend.Platform) (*backend.VersionInfo, error) {
32+
args := m.Called(ctx, tool, versionRequest, platform)
33+
if args.Get(0) != nil {
34+
return args.Get(0).(*backend.VersionInfo), args.Error(1)
35+
}
36+
return nil, args.Error(1)
37+
}
38+
39+
func (m *mockBackend) GetDownloadInfo(ctx context.Context, tool string, version string, platform backend.Platform) (*backend.VersionInfo, error) {
40+
args := m.Called(ctx, tool, version, platform)
41+
if args.Get(0) != nil {
42+
return args.Get(0).(*backend.VersionInfo), args.Error(1)
43+
}
44+
return nil, args.Error(1)
45+
}
46+
47+
func (m *mockBackend) SupportsChecksum() bool {
48+
args := m.Called()
49+
return args.Bool(0)
50+
}
51+
52+
func (m *mockBackend) SupportsGPG() bool {
53+
args := m.Called()
54+
return args.Bool(0)
55+
}
56+
57+
func (m *mockBackend) AttestationType() string {
58+
args := m.Called()
59+
return args.String(0)
60+
}
61+
62+
func (m *mockBackend) IsRecommended() bool {
63+
args := m.Called()
64+
return args.Bool(0)
65+
}
66+
67+
func (m *mockBackend) IsScriptless() bool {
68+
args := m.Called()
69+
return args.Bool(0)
70+
}
71+
72+
func (m *mockBackend) GetReach() string {
73+
args := m.Called()
74+
return args.String(0)
75+
}
76+
77+
func (m *mockBackend) IsStable() bool {
78+
args := m.Called()
79+
return args.Bool(0)
80+
}
81+
82+
func (m *mockBackend) SupportsOffline() bool {
83+
args := m.Called()
84+
return args.Bool(0)
85+
}
86+
87+
func (m *mockBackend) Dependencies() []string {
88+
args := m.Called()
89+
if args.Get(0) != nil {
90+
return args.Get(0).([]string)
91+
}
92+
return nil
93+
}
94+
95+
// Add dummy interface check
96+
var _ backend.Backend = (*mockBackend)(nil)
97+
98+
func setupBackendRPCClientServer(t *testing.T, impl backend.Backend) *BackendRPCClient {
99+
server := rpc.NewServer()
100+
err := server.RegisterName("Plugin", &BackendRPCServer{Impl: impl})
101+
assert.NoError(t, err)
102+
103+
clientConn, serverConn := net.Pipe()
104+
go server.ServeConn(serverConn)
105+
106+
client := rpc.NewClient(clientConn)
107+
t.Cleanup(func() {
108+
client.Close()
109+
clientConn.Close()
110+
serverConn.Close()
111+
})
112+
113+
return &BackendRPCClient{client: client}
114+
}
115+
116+
func TestBackendRPC_Name(t *testing.T) {
117+
mockB := new(mockBackend)
118+
mockB.On("Name").Return("mock-backend")
119+
client := setupBackendRPCClientServer(t, mockB)
120+
121+
name := client.Name()
122+
assert.Equal(t, "mock-backend", name)
123+
mockB.AssertExpectations(t)
124+
}
125+
126+
func TestBackendRPC_ResolveVersion(t *testing.T) {
127+
mockB := new(mockBackend)
128+
expectedInfo := &backend.VersionInfo{
129+
Version: "1.20",
130+
DownloadURL: "https://example.com/go1.20",
131+
}
132+
mockB.On("ResolveVersion", mock.Anything, "go", "latest", backend.Platform{OS: "linux", Arch: "amd64"}).
133+
Return(expectedInfo, nil)
134+
135+
client := setupBackendRPCClientServer(t, mockB)
136+
137+
info, err := client.ResolveVersion(context.Background(), "go", "latest", backend.Platform{OS: "linux", Arch: "amd64"})
138+
assert.NoError(t, err)
139+
assert.Equal(t, expectedInfo, info)
140+
mockB.AssertExpectations(t)
141+
}
142+
143+
func TestBackendRPC_SupportsGPG(t *testing.T) {
144+
mockB := new(mockBackend)
145+
mockB.On("SupportsGPG").Return(true)
146+
147+
client := setupBackendRPCClientServer(t, mockB)
148+
149+
assert.True(t, client.SupportsGPG())
150+
mockB.AssertExpectations(t)
151+
}

0 commit comments

Comments
 (0)