Skip to content

Commit bf6db65

Browse files
janiszclaudemtodor
authored
Add unit tests for internal/testutil and internal/toolsets/mock (#17)
Signed-off-by: Tomasz Janiszewski <[email protected]> Co-authored-by: Claude Sonnet 4.5 <[email protected]> Co-authored-by: Mladen Todorovic <[email protected]>
1 parent f8671ea commit bf6db65

File tree

6 files changed

+491
-1
lines changed

6 files changed

+491
-1
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ test: ## Run unit tests
5656
.PHONY: test-coverage-and-junit
5757
test-coverage-and-junit: ## Run unit tests with coverage and junit output
5858
go install github.com/jstemmer/go-junit-report/[email protected]
59-
$(GOTEST) -v -cover -race -coverprofile=$(COVERAGE_OUT) ./... -json 2>&1 | go-junit-report -parser gojson > $(JUNIT_OUT)
59+
$(GOTEST) -v -cover -race -coverprofile=$(COVERAGE_OUT) ./... 2>&1 | go-junit-report -set-exit-code -iocopy -out $(JUNIT_OUT)
6060

6161
.PHONY: coverage-html
6262
coverage-html: test ## Generate and open HTML coverage report

internal/testutil/config_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package testutil
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestWriteYAMLFile(t *testing.T) {
12+
content := "key: value\nfoo: bar"
13+
14+
filePath := WriteYAMLFile(t, content)
15+
16+
fileName := filepath.Base(filePath)
17+
assert.Contains(t, fileName, t.Name(), "File name should contain test name")
18+
19+
//nolint:gosec // Test code reading from known test file
20+
data, err := os.ReadFile(filePath)
21+
if err != nil {
22+
t.Fatalf("Failed to read created file: %v", err)
23+
}
24+
25+
assert.Equal(t, content, string(data))
26+
}

internal/testutil/ports_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package testutil
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestGetPortForTest_ReturnsPortInValidRange(t *testing.T) {
10+
port := GetPortForTest(t)
11+
12+
assert.GreaterOrEqual(t, port, minPort)
13+
assert.Less(t, port, maxPort)
14+
}
15+
16+
func TestGetPortForTest_ReturnsDeterministicPort(t *testing.T) {
17+
port1 := GetPortForTest(t)
18+
port2 := GetPortForTest(t)
19+
20+
assert.Equal(t, port1, port2)
21+
}
22+
23+
func TestGetPortForTest_DifferentTestNamesGetDifferentPorts(t *testing.T) {
24+
ports := make(map[int]bool)
25+
26+
// Create subtests with different names
27+
for range 10 {
28+
t.Run("subtest", func(t *testing.T) {
29+
port := GetPortForTest(t)
30+
ports[port] = true
31+
})
32+
}
33+
34+
assert.Len(t, ports, 10)
35+
}
36+
37+
func TestPortRangeConstants(t *testing.T) {
38+
assert.Greater(t, minPort, 1024)
39+
assert.LessOrEqual(t, maxPort, 65536)
40+
assert.Equal(t, maxPort-minPort, 50000)
41+
}

internal/testutil/server_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package testutil
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/http/httptest"
7+
"sync/atomic"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestWaitForServerReady_Immediate(t *testing.T) {
16+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
17+
w.WriteHeader(http.StatusOK)
18+
}))
19+
defer server.Close()
20+
21+
err := WaitForServerReady(server.URL, 1*time.Second)
22+
assert.NoError(t, err)
23+
}
24+
25+
func TestWaitForServerReady_AfterDelay(t *testing.T) {
26+
var ready atomic.Bool
27+
28+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
29+
if ready.Load() {
30+
w.WriteHeader(http.StatusOK)
31+
} else {
32+
w.WriteHeader(http.StatusServiceUnavailable)
33+
}
34+
}))
35+
defer server.Close()
36+
37+
// Make server ready after a short delay
38+
go func() {
39+
time.Sleep(200 * time.Millisecond)
40+
ready.Store(true)
41+
}()
42+
43+
err := WaitForServerReady(server.URL, 2*time.Second)
44+
assert.NoError(t, err)
45+
}
46+
47+
func TestWaitForServerReady_NeverReady(t *testing.T) {
48+
// Use an address that won't have a server
49+
err := WaitForServerReady("http://localhost:59999", 300*time.Millisecond)
50+
require.Error(t, err)
51+
assert.Contains(t, err.Error(), "did not become ready")
52+
}
53+
54+
func TestWaitForServerReady_RespectsTimeout(t *testing.T) {
55+
start := time.Now()
56+
timeout := 300 * time.Millisecond
57+
58+
err := WaitForServerReady("http://localhost:59998", timeout)
59+
60+
elapsed := time.Since(start)
61+
62+
require.Error(t, err)
63+
64+
// Allow some margin for timing (timeout + 200ms for final attempt)
65+
maxExpected := timeout + 300*time.Millisecond
66+
assert.LessOrEqual(t, elapsed, maxExpected)
67+
assert.GreaterOrEqual(t, elapsed, timeout)
68+
}
69+
70+
func TestWaitForServerReady_ErrorMessage(t *testing.T) {
71+
timeout := 250 * time.Millisecond
72+
73+
err := WaitForServerReady("http://localhost:59997", timeout)
74+
require.Error(t, err)
75+
assert.Contains(t, err.Error(), "250ms")
76+
}
77+
78+
func TestWaitForServerReady_StatusCodes(t *testing.T) {
79+
tests := []struct {
80+
name string
81+
statusCode int
82+
}{
83+
{"200 OK", http.StatusOK},
84+
{"201 Created", http.StatusCreated},
85+
{"404 Not Found", http.StatusNotFound},
86+
{"500 Internal Server Error", http.StatusInternalServerError},
87+
}
88+
89+
for _, testCase := range tests {
90+
t.Run(testCase.name, func(t *testing.T) {
91+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
92+
w.WriteHeader(testCase.statusCode)
93+
}))
94+
defer server.Close()
95+
96+
err := WaitForServerReady(server.URL, 1*time.Second)
97+
assert.NoError(t, err)
98+
})
99+
}
100+
}
101+
102+
func TestWaitForServerReady_InvalidURL(t *testing.T) {
103+
err := WaitForServerReady("http://invalid-host-that-does-not-exist.local:12345", 200*time.Millisecond)
104+
assert.Error(t, err)
105+
}
106+
107+
func TestWaitForServerReadyIntegration(t *testing.T) {
108+
t.Run("simulates real server startup scenario", func(t *testing.T) {
109+
var server *httptest.Server
110+
111+
serverReady := make(chan struct{})
112+
113+
// Simulate server starting up in background
114+
go func() {
115+
time.Sleep(150 * time.Millisecond)
116+
117+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
118+
w.WriteHeader(http.StatusOK)
119+
}))
120+
121+
close(serverReady)
122+
}()
123+
124+
// Wait for server to be created
125+
<-serverReady
126+
127+
defer server.Close()
128+
129+
err := WaitForServerReady(server.URL, 2*time.Second)
130+
assert.NoError(t, err)
131+
})
132+
}
133+
134+
func ExampleWaitForServerReady() {
135+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
136+
w.WriteHeader(http.StatusOK)
137+
}))
138+
defer server.Close()
139+
140+
err := WaitForServerReady(server.URL, 5*time.Second)
141+
if err != nil {
142+
fmt.Printf("Server not ready: %v\n", err)
143+
144+
return
145+
}
146+
147+
fmt.Println("Server is ready!")
148+
// Output: Server is ready!
149+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package mock
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stackrox/stackrox-mcp/internal/toolsets"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestNewTool(t *testing.T) {
12+
t.Run("creates tool with provided values", func(t *testing.T) {
13+
name := "test-tool"
14+
readOnly := true
15+
16+
tool := NewTool(name, readOnly)
17+
18+
assert.Equal(t, name, tool.NameValue)
19+
assert.Equal(t, readOnly, tool.ReadOnlyValue)
20+
assert.False(t, tool.RegisterCalled)
21+
})
22+
}
23+
24+
func TestTool_IsReadOnly(t *testing.T) {
25+
t.Run("returns true when read-only", func(t *testing.T) {
26+
tool := NewTool("readonly", true)
27+
28+
assert.True(t, tool.IsReadOnly())
29+
})
30+
31+
t.Run("returns false when writable", func(t *testing.T) {
32+
tool := NewTool("writable", false)
33+
34+
assert.False(t, tool.IsReadOnly())
35+
})
36+
}
37+
38+
func TestTool_GetTool(t *testing.T) {
39+
t.Run("returns MCP tool definition", func(t *testing.T) {
40+
name := "test-tool"
41+
tool := NewTool(name, true)
42+
43+
mcpTool := tool.GetTool()
44+
45+
require.NotNil(t, mcpTool)
46+
assert.Equal(t, name, mcpTool.Name)
47+
assert.NotEmpty(t, mcpTool.Description)
48+
49+
expectedDesc := "Mock tool for testing"
50+
assert.Equal(t, expectedDesc, mcpTool.Description)
51+
})
52+
53+
t.Run("returns new tool instance each time", func(t *testing.T) {
54+
tool := NewTool("test", true)
55+
56+
mcpTool1 := tool.GetTool()
57+
mcpTool2 := tool.GetTool()
58+
59+
// Should be different instances
60+
assert.NotSame(t, mcpTool1, mcpTool2)
61+
62+
// But with same values
63+
assert.Equal(t, mcpTool1.Name, mcpTool2.Name)
64+
})
65+
}
66+
67+
func TestTool_RegisterWith(t *testing.T) {
68+
t.Run("sets RegisterCalled flag", func(t *testing.T) {
69+
tool := NewTool("register-test", true)
70+
71+
assert.False(t, tool.RegisterCalled)
72+
73+
tool.RegisterWith(nil)
74+
75+
assert.True(t, tool.RegisterCalled)
76+
77+
// Can be called multiple times
78+
tool.RegisterWith(nil)
79+
80+
assert.True(t, tool.RegisterCalled)
81+
})
82+
}
83+
84+
func TestTool_InterfaceCompliance(t *testing.T) {
85+
t.Run("implements toolsets.Tool interface", func(*testing.T) {
86+
var _ toolsets.Tool = (*Tool)(nil)
87+
})
88+
}
89+
90+
func TestTool_AsInterface(t *testing.T) {
91+
var toolInstance toolsets.Tool = NewTool("interface-test", true)
92+
93+
assert.Equal(t, "interface-test", toolInstance.GetName())
94+
assert.True(t, toolInstance.IsReadOnly())
95+
96+
mcpTool := toolInstance.GetTool()
97+
assert.NotNil(t, mcpTool)
98+
99+
toolInstance.RegisterWith(nil)
100+
}
101+
102+
func TestTool_ReadOnlyWorkflow(t *testing.T) {
103+
tool := NewTool("read-tool", true)
104+
105+
assert.True(t, tool.IsReadOnly())
106+
assert.False(t, tool.RegisterCalled)
107+
108+
mcpTool := tool.GetTool()
109+
assert.Equal(t, "read-tool", mcpTool.Name)
110+
111+
tool.RegisterWith(nil)
112+
113+
assert.True(t, tool.RegisterCalled)
114+
}
115+
116+
func TestTool_WritableWorkflow(t *testing.T) {
117+
tool := NewTool("write-tool", false)
118+
119+
assert.False(t, tool.IsReadOnly())
120+
121+
_ = tool.GetTool()
122+
tool.RegisterWith(nil)
123+
124+
assert.True(t, tool.RegisterCalled)
125+
}
126+
127+
func TestTool_InToolset(t *testing.T) {
128+
tool1 := NewTool("tool1", true)
129+
tool2 := NewTool("tool2", false)
130+
131+
tools := []toolsets.Tool{tool1, tool2}
132+
133+
for _, toolInstance := range tools {
134+
assert.NotEmpty(t, toolInstance.GetName())
135+
136+
_ = toolInstance.GetTool()
137+
toolInstance.RegisterWith(nil)
138+
}
139+
140+
assert.True(t, tool1.RegisterCalled)
141+
assert.True(t, tool2.RegisterCalled)
142+
}

0 commit comments

Comments
 (0)