-
Notifications
You must be signed in to change notification settings - Fork 0
ROX-31495: Integration tests #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
1d35a3d
ROX-31495: wiremock for central (#32)
janisz 3c617df
ci: fix golangci-lint style issues
janisz b3fcf5c
refactor: extract helper methods to fix funlen linter issue
janisz 630b3da
fix
janisz e911b97
fix
janisz 8550a0c
fix
janisz 07f2802
fix: export GetToolsets function for reusability
janisz 6424c85
fix: remove duplicate getToolsets and use app.GetToolsets
janisz c727323
test: move comprehensive TestGetToolsets to app package
janisz f3c3f8e
docs: remove irrelevant stdin/stdout comment from app.go
janisz 4aadc98
docs: remove '(production mode)' annotation from server.go
janisz 8e73225
docs: remove '(for testing)' annotation from server.go
janisz 46bb0fb
docs: remove '(production)' annotation from server.go
janisz 25d1d91
docs: add full paths to fixture comments
janisz c79028d
refactor: move helper functions to top of integration_test.go
janisz 8b90a9b
refactor: simplify RequireNoError with early return pattern
janisz afc96aa
chore: delete unused RunCommand function
janisz 6708e6b
fix: create cancellable context before client connection
janisz 46d7709
refactor: consolidate integration test helpers in testutil
janisz b6c5b1d
test: replace substring matching with strict JSON equality in integra…
janisz f40166c
Update internal/app/app.go
janisz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| //go:build integration | ||
|
|
||
| package integration | ||
|
|
||
| // Log4ShellFixture contains expected data from log4j_cve.json fixture. | ||
|
mtodor marked this conversation as resolved.
Outdated
|
||
| var Log4ShellFixture = struct { | ||
| CVEName string | ||
| DeploymentCount int | ||
| DeploymentNames []string | ||
| }{ | ||
| CVEName: "CVE-2021-44228", | ||
| DeploymentCount: 3, | ||
| DeploymentNames: []string{"elasticsearch", "kafka-broker", "spring-boot-app"}, | ||
| } | ||
|
|
||
| // AllClustersFixture contains expected data from all_clusters.json fixture. | ||
| var AllClustersFixture = struct { | ||
| TotalCount int | ||
| ClusterNames []string | ||
| }{ | ||
| TotalCount: 5, | ||
| ClusterNames: []string{ | ||
| "production-cluster", | ||
| "staging-cluster", | ||
| "staging-central-cluster", | ||
| "development-cluster", | ||
| "production-cluster-eu", | ||
| }, | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| //go:build integration | ||
|
|
||
| package integration | ||
|
|
||
| import ( | ||
| "context" | ||
| "io" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/modelcontextprotocol/go-sdk/mcp" | ||
| "github.com/stackrox/stackrox-mcp/internal/app" | ||
| "github.com/stackrox/stackrox-mcp/internal/config" | ||
| "github.com/stackrox/stackrox-mcp/internal/testutil" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // setupInitializedClient creates an initialized MCP client for testing. | ||
| func setupInitializedClient(t *testing.T) *testutil.MCPTestClient { | ||
| t.Helper() | ||
|
|
||
| client, err := createMCPClient(t) | ||
| require.NoError(t, err, "Failed to create MCP client") | ||
| t.Cleanup(func() { client.Close() }) | ||
|
|
||
| return client | ||
| } | ||
|
|
||
| // callToolAndGetResult calls a tool and verifies it succeeds. | ||
| func callToolAndGetResult(t *testing.T, client *testutil.MCPTestClient, toolName string, args map[string]any) *mcp.CallToolResult { | ||
| t.Helper() | ||
|
|
||
| ctx := context.Background() | ||
| result, err := client.CallTool(ctx, toolName, args) | ||
| require.NoError(t, err) | ||
| testutil.RequireNoError(t, result) | ||
|
|
||
| return result | ||
| } | ||
|
|
||
| // getTextContent extracts text from the first content item. | ||
| func getTextContent(t *testing.T, result *mcp.CallToolResult) string { | ||
|
mtodor marked this conversation as resolved.
Outdated
|
||
| t.Helper() | ||
| require.NotEmpty(t, result.Content, "should have content in response") | ||
|
|
||
| textContent, ok := result.Content[0].(*mcp.TextContent) | ||
| require.True(t, ok, "expected TextContent, got %T", result.Content[0]) | ||
|
|
||
| return textContent.Text | ||
| } | ||
|
|
||
| // TestIntegration_ListTools verifies that all expected tools are registered. | ||
| func TestIntegration_ListTools(t *testing.T) { | ||
| client := setupInitializedClient(t) | ||
|
|
||
| ctx := context.Background() | ||
| result, err := client.ListTools(ctx) | ||
| require.NoError(t, err) | ||
|
|
||
| // Verify we have tools registered | ||
| assert.NotEmpty(t, result.Tools, "should have tools registered") | ||
|
|
||
| // Check for specific tools we expect | ||
| toolNames := make([]string, 0, len(result.Tools)) | ||
| for _, tool := range result.Tools { | ||
| toolNames = append(toolNames, tool.Name) | ||
| } | ||
|
|
||
| assert.Contains(t, toolNames, "get_deployments_for_cve", "should have get_deployments_for_cve tool") | ||
| assert.Contains(t, toolNames, "list_clusters", "should have list_clusters tool") | ||
| } | ||
|
|
||
| // TestIntegration_ToolCalls tests successful tool calls using table-driven tests. | ||
| func TestIntegration_ToolCalls(t *testing.T) { | ||
| tests := map[string]struct { | ||
| toolName string | ||
| args map[string]any | ||
| expectedInText []string // strings that must appear in response | ||
| }{ | ||
| "get_deployments_for_cve with Log4Shell": { | ||
| toolName: "get_deployments_for_cve", | ||
| args: map[string]any{"cveName": Log4ShellFixture.CVEName}, | ||
| expectedInText: Log4ShellFixture.DeploymentNames, | ||
| }, | ||
| "get_deployments_for_cve with non-existent CVE": { | ||
| toolName: "get_deployments_for_cve", | ||
| args: map[string]any{"cveName": "CVE-9999-99999"}, | ||
| expectedInText: []string{`"deployments":[]`}, | ||
| }, | ||
| "list_clusters": { | ||
| toolName: "list_clusters", | ||
| args: map[string]any{}, | ||
| expectedInText: AllClustersFixture.ClusterNames, | ||
| }, | ||
| "get_clusters_with_orchestrator_cve": { | ||
| toolName: "get_clusters_with_orchestrator_cve", | ||
| args: map[string]any{"cveName": "CVE-2099-00001"}, | ||
| expectedInText: []string{`"clusters":`}, | ||
|
janisz marked this conversation as resolved.
Outdated
|
||
| }, | ||
| } | ||
|
|
||
| for name, tt := range tests { | ||
| t.Run(name, func(t *testing.T) { | ||
| client := setupInitializedClient(t) | ||
| result := callToolAndGetResult(t, client, tt.toolName, tt.args) | ||
|
|
||
| responseText := getTextContent(t, result) | ||
| for _, expected := range tt.expectedInText { | ||
| assert.Contains(t, responseText, expected) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // TestIntegration_ToolCallErrors tests error handling using table-driven tests. | ||
| func TestIntegration_ToolCallErrors(t *testing.T) { | ||
| tests := map[string]struct { | ||
| toolName string | ||
| args map[string]any | ||
| expectedErrorMsg string | ||
| }{ | ||
| "get_deployments_for_cve missing CVE name": { | ||
| toolName: "get_deployments_for_cve", | ||
| args: map[string]any{}, | ||
| expectedErrorMsg: "cveName", | ||
| }, | ||
| } | ||
|
|
||
| for name, tt := range tests { | ||
| t.Run(name, func(t *testing.T) { | ||
| client := setupInitializedClient(t) | ||
|
|
||
| ctx := context.Background() | ||
| _, err := client.CallTool(ctx, tt.toolName, tt.args) | ||
|
|
||
| // Validation errors are returned as protocol errors, not tool errors | ||
| require.Error(t, err, "should receive protocol error for invalid params") | ||
| assert.Contains(t, err.Error(), tt.expectedErrorMsg) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // createTestConfig creates a test configuration for the MCP server. | ||
| func createTestConfig() *config.Config { | ||
| return &config.Config{ | ||
| Central: config.CentralConfig{ | ||
| URL: "localhost:8081", | ||
| AuthType: "static", | ||
| APIToken: "test-token-admin", | ||
| InsecureSkipTLSVerify: true, | ||
| RequestTimeout: 30 * time.Second, | ||
| MaxRetries: 3, | ||
| InitialBackoff: time.Second, | ||
| MaxBackoff: 10 * time.Second, | ||
| }, | ||
| Server: config.ServerConfig{ | ||
| Type: config.ServerTypeStdio, | ||
| }, | ||
| Tools: config.ToolsConfig{ | ||
| Vulnerability: config.ToolsetVulnerabilityConfig{ | ||
| Enabled: true, | ||
| }, | ||
| ConfigManager: config.ToolConfigManagerConfig{ | ||
| Enabled: true, | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // createMCPClient is a helper function that creates an MCP client with the test configuration. | ||
| func createMCPClient(t *testing.T) (*testutil.MCPTestClient, error) { | ||
|
mtodor marked this conversation as resolved.
Outdated
|
||
| t.Helper() | ||
|
|
||
| cfg := createTestConfig() | ||
|
|
||
| // Create a run function that wraps app.Run with the config | ||
| runFunc := func(ctx context.Context, stdin io.ReadCloser, stdout io.WriteCloser) error { | ||
| return app.Run(ctx, cfg, stdin, stdout) | ||
| } | ||
|
|
||
| return testutil.NewMCPTestClient(t, runFunc) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.