Skip to content

Commit 3e6fc88

Browse files
janiszclaudemtodor
authored
Add smoke test infrastructure (#91)
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 f0d2054 commit 3e6fc88

File tree

7 files changed

+448
-37
lines changed

7 files changed

+448
-37
lines changed

.github/workflows/smoke.yml

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: Smoke Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
types:
9+
- opened
10+
- reopened
11+
- synchronize
12+
13+
jobs:
14+
smoke:
15+
name: Run Smoke Tests
16+
runs-on: ubuntu-latest
17+
timeout-minutes: 30
18+
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Go
24+
uses: actions/setup-go@v5
25+
with:
26+
go-version-file: go.mod
27+
28+
- name: Download dependencies
29+
run: go mod download
30+
31+
- name: Create kind cluster
32+
uses: helm/kind-action@v1
33+
with:
34+
cluster_name: stackrox-mcp-smoke
35+
36+
- name: Checkout StackRox repository
37+
uses: actions/checkout@v4
38+
with:
39+
repository: stackrox/stackrox
40+
path: stackrox-repo
41+
42+
- name: Deploy StackRox Central
43+
env:
44+
MAIN_IMAGE_TAG: latest
45+
SENSOR_HELM_DEPLOY: "true"
46+
ROX_SCANNER_V4: "false"
47+
ADMISSION_CONTROLLER: "false"
48+
SCANNER_REPLICAS: "0"
49+
COLLECTION_METHOD: "no_collection"
50+
run: |
51+
cd stackrox-repo
52+
./deploy/k8s/deploy-local.sh
53+
54+
- name: Wait for Central pods ready
55+
run: kubectl wait --for=condition=ready --timeout=180s pod -l app=central -n stackrox
56+
57+
- name: Remove resource constraints from Sensor
58+
run: |
59+
# Use kubectl set resources to remove all resource constraints
60+
kubectl set resources deployment/sensor -n stackrox \
61+
--requests=cpu=0,memory=0 \
62+
--limits=cpu=0,memory=0
63+
64+
# Delete sensor pods to force recreation with new (empty) resources
65+
kubectl delete pods -n stackrox -l app=sensor
66+
67+
# Wait a bit for pods to be deleted and recreated
68+
sleep 10
69+
70+
# Wait for new pods to be ready
71+
kubectl wait --for=condition=ready --timeout=300s pod -l app=sensor -n stackrox
72+
73+
- name: Extract Central password
74+
id: extract-password
75+
run: |
76+
PASSWORD="$(cat stackrox-repo/deploy/k8s/central-deploy/password)"
77+
echo "::add-mask::${PASSWORD}"
78+
echo "password=${PASSWORD}" >> "$GITHUB_OUTPUT"
79+
80+
- name: Setup port-forward to Central
81+
run: stackrox-repo/scale/dev/port-forward.sh 8000
82+
83+
- name: Install go-junit-report
84+
run: go install github.com/jstemmer/go-junit-report/v2@v2.1.0
85+
86+
- name: Run smoke tests with JUnit output
87+
env:
88+
ROX_ENDPOINT: localhost:8000
89+
ROX_PASSWORD: ${{ steps.extract-password.outputs.password }}
90+
run: |
91+
go test -v -tags=smoke -cover -race -coverprofile=coverage-smoke.out -timeout=20m ./smoke 2>&1 | \
92+
tee /dev/stderr | \
93+
go-junit-report -set-exit-code -out junit-smoke.xml
94+
95+
- name: Upload JUnit test results
96+
if: always()
97+
uses: actions/upload-artifact@v4
98+
with:
99+
name: junit-smoke-results
100+
path: junit-smoke.xml
101+
if-no-files-found: error
102+
103+
- name: Upload test results to Codecov
104+
if: always()
105+
uses: codecov/test-results-action@v1
106+
with:
107+
token: ${{ secrets.CODECOV_TOKEN }}
108+
files: junit-smoke.xml
109+
110+
- name: Upload coverage to Codecov
111+
if: always()
112+
uses: codecov/codecov-action@v5
113+
with:
114+
files: ./coverage-smoke.out
115+
token: ${{ secrets.CODECOV_TOKEN }}
116+
fail_ci_if_error: false
117+
flags: smoke
118+
name: smoke-tests
119+
120+
- name: Collect logs
121+
if: always()
122+
run: |
123+
mkdir -p logs
124+
kubectl get pods -A > logs/pods.txt || true
125+
kubectl get events -A --sort-by='.lastTimestamp' > logs/events.txt || true
126+
kubectl logs -n stackrox deployment/central > logs/central.log || true
127+
128+
- name: Upload logs
129+
if: always()
130+
uses: actions/upload-artifact@v4
131+
with:
132+
name: smoke-test-logs
133+
path: logs/
134+
if-no-files-found: ignore

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
# Test output
1111
/*.out
1212
/*junit.xml
13+
/coverage-report.html
1314

1415
# Build output
1516
/stackrox-mcp
17+
/bin/
18+
19+
# Virtual environments
20+
/ENV_DIR/
1621

1722
# Lint output
1823
/report.xml
@@ -24,6 +29,7 @@
2429
/e2e-tests/.env
2530
/e2e-tests/mcp-reports/
2631
/e2e-tests/bin/
32+
/e2e-tests/**/mcpchecker
2733
/e2e-tests/**/*-out.json
2834

2935
# WireMock

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ e2e-test: ## Run E2E tests (uses WireMock)
7878
.PHONY: test-coverage-and-junit
7979
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
81-
$(GOTEST) -v -cover -race -coverprofile=$(COVERAGE_OUT) ./... 2>&1 | go-junit-report -set-exit-code -iocopy -out $(JUNIT_OUT)
81+
$(GOTEST) -v -cover -race -coverprofile=$(COVERAGE_OUT) -tags=!smoke ./... 2>&1 | go-junit-report -set-exit-code -iocopy -out $(JUNIT_OUT)
8282

8383
.PHONY: test-integration-coverage
8484
test-integration-coverage: ## Run integration tests with coverage

internal/testutil/integration_helpers.go

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ import (
88
"testing"
99
"time"
1010

11-
"github.com/modelcontextprotocol/go-sdk/mcp"
1211
"github.com/stackrox/stackrox-mcp/internal/app"
1312
"github.com/stackrox/stackrox-mcp/internal/config"
14-
"github.com/stretchr/testify/require"
1513
)
1614

1715
// CreateIntegrationTestConfig creates a test configuration for integration tests.
@@ -55,37 +53,3 @@ func CreateIntegrationMCPClient(t *testing.T) (*MCPTestClient, error) {
5553

5654
return NewMCPTestClient(t, runFunc)
5755
}
58-
59-
// SetupInitializedClient creates an initialized MCP client for testing with automatic cleanup.
60-
func SetupInitializedClient(t *testing.T, createClient func(*testing.T) (*MCPTestClient, error)) *MCPTestClient {
61-
t.Helper()
62-
63-
client, err := createClient(t)
64-
require.NoError(t, err, "Failed to create MCP client")
65-
t.Cleanup(func() { client.Close() })
66-
67-
return client
68-
}
69-
70-
// CallToolAndGetResult calls a tool and verifies it succeeds.
71-
func CallToolAndGetResult(t *testing.T, client *MCPTestClient, toolName string, args map[string]any) *mcp.CallToolResult {
72-
t.Helper()
73-
74-
ctx := context.Background()
75-
result, err := client.CallTool(ctx, toolName, args)
76-
require.NoError(t, err)
77-
RequireNoError(t, result)
78-
79-
return result
80-
}
81-
82-
// GetTextContent extracts text from the first content item.
83-
func GetTextContent(t *testing.T, result *mcp.CallToolResult) string {
84-
t.Helper()
85-
require.NotEmpty(t, result.Content, "should have content in response")
86-
87-
textContent, ok := result.Content[0].(*mcp.TextContent)
88-
require.True(t, ok, "expected TextContent, got %T", result.Content[0])
89-
90-
return textContent.Text
91-
}

internal/testutil/test_helpers.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package testutil
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/modelcontextprotocol/go-sdk/mcp"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// SetupInitializedClient creates an initialized MCP client with automatic cleanup.
12+
func SetupInitializedClient(t *testing.T, createClient func(*testing.T) (*MCPTestClient, error)) *MCPTestClient {
13+
t.Helper()
14+
15+
client, err := createClient(t)
16+
require.NoError(t, err, "Failed to create MCP client")
17+
t.Cleanup(func() { _ = client.Close() })
18+
19+
return client
20+
}
21+
22+
// CallToolAndGetResult calls a tool and verifies it succeeds.
23+
func CallToolAndGetResult(
24+
t *testing.T,
25+
client *MCPTestClient,
26+
toolName string,
27+
args map[string]any,
28+
) *mcp.CallToolResult {
29+
t.Helper()
30+
31+
ctx := context.Background()
32+
result, err := client.CallTool(ctx, toolName, args)
33+
require.NoError(t, err)
34+
RequireNoError(t, result)
35+
36+
return result
37+
}
38+
39+
// GetTextContent extracts text from the first content item.
40+
func GetTextContent(t *testing.T, result *mcp.CallToolResult) string {
41+
t.Helper()
42+
require.NotEmpty(t, result.Content, "should have content in response")
43+
44+
textContent, ok := result.Content[0].(*mcp.TextContent)
45+
require.True(t, ok, "expected TextContent, got %T", result.Content[0])
46+
47+
return textContent.Text
48+
}

0 commit comments

Comments
 (0)