Skip to content

Commit cb8e0ab

Browse files
janiszclaudemtodor
authored
ROX-31495: Integration tests (#50)
Signed-off-by: Tomasz Janiszewski <tomek@redhat.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Mladen Todorovic <mtodor@gmail.com>
1 parent 8262064 commit cb8e0ab

File tree

12 files changed

+599
-67
lines changed

12 files changed

+599
-67
lines changed

.github/workflows/test.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,39 @@ jobs:
4545
- name: Run E2E smoke test
4646
run: make e2e-smoke-test
4747

48+
- name: Set up Java
49+
uses: actions/setup-java@v4
50+
with:
51+
distribution: 'temurin'
52+
java-version: '11'
53+
54+
- name: Setup proto files
55+
run: ./scripts/setup-proto-files.sh
56+
57+
- name: Generate proto descriptors
58+
run: make proto-generate
59+
60+
- name: Download WireMock
61+
run: make mock-download
62+
63+
- name: Start WireMock
64+
run: make mock-start
65+
66+
- name: Run integration tests
67+
run: make test-integration-coverage
68+
69+
- name: Stop WireMock
70+
if: always()
71+
run: make mock-stop
72+
73+
- name: Upload WireMock logs on failure
74+
if: failure()
75+
uses: actions/upload-artifact@v4
76+
with:
77+
name: wiremock-logs
78+
path: wiremock/wiremock.log
79+
if-no-files-found: ignore
80+
4881
- name: Upload test results to Codecov
4982
uses: codecov/test-results-action@v1
5083
with:
@@ -56,3 +89,14 @@ jobs:
5689
files: ./coverage.out
5790
token: ${{ secrets.CODECOV_TOKEN }}
5891
fail_ci_if_error: false
92+
flags: unit
93+
name: unit-tests
94+
95+
- name: Upload integration test coverage to Codecov
96+
uses: codecov/codecov-action@v5
97+
with:
98+
files: ./coverage-integration.out
99+
token: ${{ secrets.CODECOV_TOKEN }}
100+
fail_ci_if_error: false
101+
flags: integration
102+
name: integration-tests

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ test-coverage-and-junit: ## Run unit tests with coverage and junit output
8080
go install github.com/jstemmer/go-junit-report/v2@v2.1.0
8181
$(GOTEST) -v -cover -race -coverprofile=$(COVERAGE_OUT) ./... 2>&1 | go-junit-report -set-exit-code -iocopy -out $(JUNIT_OUT)
8282

83+
.PHONY: test-integration-coverage
84+
test-integration-coverage: ## Run integration tests with coverage
85+
$(GOTEST) -v -cover -race -tags=integration -coverprofile=coverage-integration.out ./integration
86+
8387
.PHONY: coverage-html
8488
coverage-html: test ## Generate and open HTML coverage report
8589
$(GOCMD) tool cover -html=$(COVERAGE_OUT)

cmd/stackrox-mcp/main.go

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,12 @@ package main
44
import (
55
"context"
66
"flag"
7-
"log/slog"
8-
"os"
9-
"os/signal"
10-
"syscall"
117

12-
"github.com/stackrox/stackrox-mcp/internal/client"
8+
"github.com/stackrox/stackrox-mcp/internal/app"
139
"github.com/stackrox/stackrox-mcp/internal/config"
1410
"github.com/stackrox/stackrox-mcp/internal/logging"
15-
"github.com/stackrox/stackrox-mcp/internal/server"
16-
"github.com/stackrox/stackrox-mcp/internal/toolsets"
17-
toolsetConfig "github.com/stackrox/stackrox-mcp/internal/toolsets/config"
18-
toolsetVulnerability "github.com/stackrox/stackrox-mcp/internal/toolsets/vulnerability"
1911
)
2012

21-
// getToolsets initializes and returns all available toolsets.
22-
func getToolsets(cfg *config.Config, c *client.Client) []toolsets.Toolset {
23-
return []toolsets.Toolset{
24-
toolsetConfig.NewToolset(cfg, c),
25-
toolsetVulnerability.NewToolset(cfg, c),
26-
}
27-
}
28-
2913
func main() {
3014
logging.SetupLogging()
3115

@@ -38,38 +22,9 @@ func main() {
3822
logging.Fatal("Failed to load configuration", err)
3923
}
4024

41-
// Log full configuration with sensitive data redacted.
42-
slog.Info("Configuration loaded successfully", "config", cfg.Redacted())
43-
44-
stackroxClient, err := client.NewClient(&cfg.Central)
45-
if err != nil {
46-
logging.Fatal("Failed to create StackRox client", err)
47-
}
48-
49-
registry := toolsets.NewRegistry(cfg, getToolsets(cfg, stackroxClient))
50-
srv := server.NewServer(cfg, registry)
51-
52-
// Set up context with signal handling for graceful shutdown.
53-
ctx, cancel := context.WithCancel(context.Background())
54-
defer cancel()
55-
56-
err = stackroxClient.Connect(ctx)
57-
if err != nil {
58-
logging.Fatal("Failed to connect to StackRox server", err)
59-
}
60-
61-
sigChan := make(chan os.Signal, 1)
62-
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
63-
64-
go func() {
65-
<-sigChan
66-
slog.Info("Received shutdown signal")
67-
cancel()
68-
}()
69-
70-
slog.Info("Starting StackRox MCP server")
25+
ctx := context.Background()
7126

72-
if err := srv.Start(ctx); err != nil {
27+
if err := app.Run(ctx, cfg, nil, nil); err != nil {
7328
logging.Fatal("Server error", err)
7429
}
7530
}

cmd/stackrox-mcp/main_test.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,15 @@ import (
99
"testing"
1010
"time"
1111

12+
"github.com/stackrox/stackrox-mcp/internal/app"
1213
"github.com/stackrox/stackrox-mcp/internal/client"
1314
"github.com/stackrox/stackrox-mcp/internal/config"
1415
"github.com/stackrox/stackrox-mcp/internal/server"
1516
"github.com/stackrox/stackrox-mcp/internal/testutil"
1617
"github.com/stackrox/stackrox-mcp/internal/toolsets"
17-
"github.com/stretchr/testify/assert"
1818
"github.com/stretchr/testify/require"
1919
)
2020

21-
func TestGetToolsets(t *testing.T) {
22-
allToolsets := getToolsets(&config.Config{}, &client.Client{})
23-
24-
toolsetNames := []string{}
25-
for _, toolset := range allToolsets {
26-
toolsetNames = append(toolsetNames, toolset.GetName())
27-
}
28-
29-
assert.Contains(t, toolsetNames, "config_manager")
30-
assert.Contains(t, toolsetNames, "vulnerability")
31-
}
32-
3321
func TestGracefulShutdown(t *testing.T) {
3422
// Set up minimal valid config. config.LoadConfig() validates configuration.
3523
t.Setenv("STACKROX_MCP__TOOLS__VULNERABILITY__ENABLED", "true")
@@ -39,14 +27,14 @@ func TestGracefulShutdown(t *testing.T) {
3927
require.NotNil(t, cfg)
4028
cfg.Server.Port = testutil.GetPortForTest(t)
4129

42-
registry := toolsets.NewRegistry(cfg, getToolsets(cfg, &client.Client{}))
30+
registry := toolsets.NewRegistry(cfg, app.GetToolsets(cfg, &client.Client{}))
4331
srv := server.NewServer(cfg, registry)
4432
ctx, cancel := context.WithCancel(context.Background())
4533

4634
errChan := make(chan error, 1)
4735

4836
go func() {
49-
errChan <- srv.Start(ctx)
37+
errChan <- srv.Start(ctx, nil, nil)
5038
}()
5139

5240
serverURL := "http://" + net.JoinHostPort(cfg.Server.Address, strconv.Itoa(cfg.Server.Port))

integration/fixtures.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//go:build integration
2+
3+
package integration
4+
5+
// Log4ShellFixture contains expected data from wiremock/fixtures/deployments/log4j_cve.json fixture.
6+
var Log4ShellFixture = struct {
7+
CVEName string
8+
DeploymentCount int
9+
DeploymentNames []string
10+
ExpectedJSON string
11+
}{
12+
CVEName: "CVE-2021-44228",
13+
DeploymentCount: 3,
14+
DeploymentNames: []string{"elasticsearch", "kafka-broker", "spring-boot-app"},
15+
ExpectedJSON: `{
16+
"deployments": [
17+
{
18+
"name": "elasticsearch",
19+
"namespace": "logging",
20+
"clusterId": "cluster-prod-01",
21+
"clusterName": "production-cluster"
22+
},
23+
{
24+
"name": "kafka-broker",
25+
"namespace": "messaging",
26+
"clusterId": "cluster-prod-02",
27+
"clusterName": "production-cluster-eu"
28+
},
29+
{
30+
"name": "spring-boot-app",
31+
"namespace": "applications",
32+
"clusterId": "cluster-staging-01",
33+
"clusterName": "staging-cluster"
34+
}
35+
],
36+
"nextCursor": ""
37+
}`,
38+
}
39+
40+
// AllClustersFixture contains expected data from wiremock/fixtures/clusters/all_clusters.json fixture.
41+
var AllClustersFixture = struct {
42+
TotalCount int
43+
ClusterNames []string
44+
ExpectedJSON string
45+
}{
46+
TotalCount: 5,
47+
ClusterNames: []string{
48+
"production-cluster",
49+
"staging-cluster",
50+
"staging-central-cluster",
51+
"development-cluster",
52+
"production-cluster-eu",
53+
},
54+
ExpectedJSON: `{
55+
"clusters": [
56+
{
57+
"id": "cluster-prod-01",
58+
"name": "production-cluster",
59+
"type": "KUBERNETES_CLUSTER"
60+
},
61+
{
62+
"id": "cluster-staging-01",
63+
"name": "staging-cluster",
64+
"type": "KUBERNETES_CLUSTER"
65+
},
66+
{
67+
"id": "65673bd7-da6a-4cdc-a5fc-95765d1b9724",
68+
"name": "staging-central-cluster",
69+
"type": "OPENSHIFT4_CLUSTER"
70+
},
71+
{
72+
"id": "cluster-dev-01",
73+
"name": "development-cluster",
74+
"type": "KUBERNETES_CLUSTER"
75+
},
76+
{
77+
"id": "cluster-prod-02",
78+
"name": "production-cluster-eu",
79+
"type": "KUBERNETES_CLUSTER"
80+
}
81+
],
82+
"totalCount": 5,
83+
"offset": 0,
84+
"limit": 0
85+
}`,
86+
}
87+
88+
// EmptyDeploymentsJSON represents the expected JSON response when no deployments are found.
89+
const EmptyDeploymentsJSON = `{"deployments": [], "nextCursor": ""}`
90+
91+
// EmptyClustersForCVEJSON represents the expected JSON response from get_clusters_with_orchestrator_cve when no clusters are found.
92+
const EmptyClustersForCVEJSON = `{"clusters": []}`

integration/integration_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//go:build integration
2+
3+
package integration
4+
5+
import (
6+
"context"
7+
"testing"
8+
9+
"github.com/stackrox/stackrox-mcp/internal/testutil"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// TestIntegration_ListTools verifies that all expected tools are registered.
15+
func TestIntegration_ListTools(t *testing.T) {
16+
client := testutil.SetupInitializedClient(t, testutil.CreateIntegrationMCPClient)
17+
18+
ctx := context.Background()
19+
result, err := client.ListTools(ctx)
20+
require.NoError(t, err)
21+
22+
// Verify we have tools registered
23+
assert.NotEmpty(t, result.Tools, "should have tools registered")
24+
25+
// Check for specific tools we expect
26+
toolNames := make([]string, 0, len(result.Tools))
27+
for _, tool := range result.Tools {
28+
toolNames = append(toolNames, tool.Name)
29+
}
30+
31+
assert.Contains(t, toolNames, "get_deployments_for_cve", "should have get_deployments_for_cve tool")
32+
assert.Contains(t, toolNames, "list_clusters", "should have list_clusters tool")
33+
}
34+
35+
// TestIntegration_ToolCalls tests successful tool calls using table-driven tests.
36+
func TestIntegration_ToolCalls(t *testing.T) {
37+
tests := map[string]struct {
38+
toolName string
39+
args map[string]any
40+
expectedJSON string // expected JSON response for exact comparison
41+
}{
42+
"get_deployments_for_cve with Log4Shell": {
43+
toolName: "get_deployments_for_cve",
44+
args: map[string]any{"cveName": Log4ShellFixture.CVEName},
45+
expectedJSON: Log4ShellFixture.ExpectedJSON,
46+
},
47+
"get_deployments_for_cve with non-existent CVE": {
48+
toolName: "get_deployments_for_cve",
49+
args: map[string]any{"cveName": "CVE-9999-99999"},
50+
expectedJSON: EmptyDeploymentsJSON,
51+
},
52+
"list_clusters": {
53+
toolName: "list_clusters",
54+
args: map[string]any{},
55+
expectedJSON: AllClustersFixture.ExpectedJSON,
56+
},
57+
"get_clusters_with_orchestrator_cve": {
58+
toolName: "get_clusters_with_orchestrator_cve",
59+
args: map[string]any{"cveName": "CVE-2099-00001"},
60+
expectedJSON: EmptyClustersForCVEJSON,
61+
},
62+
}
63+
64+
for name, tt := range tests {
65+
t.Run(name, func(t *testing.T) {
66+
client := testutil.SetupInitializedClient(t, testutil.CreateIntegrationMCPClient)
67+
result := testutil.CallToolAndGetResult(t, client, tt.toolName, tt.args)
68+
69+
responseText := testutil.GetTextContent(t, result)
70+
assert.JSONEq(t, tt.expectedJSON, responseText)
71+
})
72+
}
73+
}
74+
75+
// TestIntegration_ToolCallErrors tests error handling using table-driven tests.
76+
func TestIntegration_ToolCallErrors(t *testing.T) {
77+
tests := map[string]struct {
78+
toolName string
79+
args map[string]any
80+
expectedErrorMsg string
81+
}{
82+
"get_deployments_for_cve missing CVE name": {
83+
toolName: "get_deployments_for_cve",
84+
args: map[string]any{},
85+
expectedErrorMsg: "cveName",
86+
},
87+
}
88+
89+
for name, tt := range tests {
90+
t.Run(name, func(t *testing.T) {
91+
client := testutil.SetupInitializedClient(t, testutil.CreateIntegrationMCPClient)
92+
93+
ctx := context.Background()
94+
_, err := client.CallTool(ctx, tt.toolName, tt.args)
95+
96+
// Validation errors are returned as protocol errors, not tool errors
97+
require.Error(t, err, "should receive protocol error for invalid params")
98+
assert.Contains(t, err.Error(), tt.expectedErrorMsg)
99+
})
100+
}
101+
}

0 commit comments

Comments
 (0)