Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
name: Smoke Tests

on:
push:
branches:
- main
pull_request:
types:
- opened
- reopened
- synchronize

jobs:
smoke:
name: Run Smoke Tests
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Download dependencies
run: go mod download

- name: Create kind cluster
uses: helm/kind-action@v1
with:
cluster_name: stackrox-mcp-smoke

- name: Checkout StackRox repository
uses: actions/checkout@v4
with:
repository: stackrox/stackrox
path: stackrox-repo

- name: Deploy StackRox Central
env:
MAIN_IMAGE_TAG: latest
SENSOR_HELM_DEPLOY: "true"
ROX_SCANNER_V4: "false"
ADMISSION_CONTROLLER: "false"
SCANNER_REPLICAS: "0"
COLLECTION_METHOD: "no_collection"
run: |
cd stackrox-repo
./deploy/k8s/deploy-local.sh

- name: Wait for Central pods ready
run: kubectl wait --for=condition=ready --timeout=180s pod -l app=central -n stackrox

- name: Remove resource constraints from Sensor
run: |
# Use kubectl set resources to remove all resource constraints
kubectl set resources deployment/sensor -n stackrox \
--requests=cpu=0,memory=0 \
--limits=cpu=0,memory=0

# Delete sensor pods to force recreation with new (empty) resources
kubectl delete pods -n stackrox -l app=sensor

# Wait a bit for pods to be deleted and recreated
sleep 10

# Wait for new pods to be ready
kubectl wait --for=condition=ready --timeout=300s pod -l app=sensor -n stackrox

- name: Extract Central password
id: extract-password
run: |
PASSWORD="$(cat stackrox-repo/deploy/k8s/central-deploy/password)"
echo "::add-mask::${PASSWORD}"
echo "password=${PASSWORD}" >> "$GITHUB_OUTPUT"

- name: Setup port-forward to Central
run: |
# Create logs directory
mkdir -p logs

# Kill any existing port-forward on port 8000
pkill -f "port-forward.*8000" || true
sleep 2

# Start port-forward in background
kubectl port-forward -n stackrox svc/central 8000:443 > logs/port-forward.log 2>&1 &
sleep 5

# Verify port-forward is working
if ! curl -k -s https://localhost:8000/v1/ping > /dev/null 2>&1; then
echo "Port-forward failed to start. Log:"
cat logs/port-forward.log || true
exit 1
fi
echo "Port-forward established successfully"

- name: Install go-junit-report
run: go install github.com/jstemmer/go-junit-report/v2@v2.1.0

- name: Run smoke tests with JUnit output
env:
ROX_ENDPOINT: localhost:8000
ROX_PASSWORD: ${{ steps.extract-password.outputs.password }}
run: |
go test -v -tags=smoke -cover -race -coverprofile=coverage-smoke.out -timeout=20m ./smoke 2>&1 | \
tee /dev/stderr | \
go-junit-report -set-exit-code -out junit-smoke.xml

- name: Upload JUnit test results
if: always()
uses: actions/upload-artifact@v4
with:
name: junit-smoke-results
path: junit-smoke.xml
if-no-files-found: error

- name: Upload test results to Codecov
if: always()
uses: codecov/test-results-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: junit-smoke.xml

- name: Upload coverage to Codecov
if: always()
uses: codecov/codecov-action@v5
with:
files: ./coverage-smoke.out
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
flags: smoke
name: smoke-tests

- name: Collect logs
if: always()
run: |
mkdir -p logs
kubectl get pods -A > logs/pods.txt || true
kubectl get events -A --sort-by='.lastTimestamp' > logs/events.txt || true
kubectl logs -n stackrox deployment/central > logs/central.log || true

- name: Upload logs
if: always()
uses: actions/upload-artifact@v4
with:
name: smoke-test-logs
path: logs/
if-no-files-found: ignore
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@
# Test output
/*.out
/*junit.xml
/coverage-report.html

# Build output
/stackrox-mcp
/bin/

# Virtual environments
/ENV_DIR/

# Lint output
/report.xml
Expand All @@ -24,6 +29,7 @@
/e2e-tests/.env
/e2e-tests/mcp-reports/
/e2e-tests/bin/
/e2e-tests/**/mcpchecker
/e2e-tests/**/*-out.json

# WireMock
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ e2e-test: ## Run E2E tests (uses WireMock)
.PHONY: test-coverage-and-junit
test-coverage-and-junit: ## Run unit tests with coverage and junit output
go install github.com/jstemmer/go-junit-report/v2@v2.1.0
$(GOTEST) -v -cover -race -coverprofile=$(COVERAGE_OUT) ./... 2>&1 | go-junit-report -set-exit-code -iocopy -out $(JUNIT_OUT)
$(GOTEST) -v -cover -race -coverprofile=$(COVERAGE_OUT) $(shell go list ./... | grep -v '/smoke$$') 2>&1 | go-junit-report -set-exit-code -iocopy -out $(JUNIT_OUT)

.PHONY: test-integration-coverage
test-integration-coverage: ## Run integration tests with coverage
Expand Down
36 changes: 0 additions & 36 deletions internal/testutil/integration_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import (
"testing"
"time"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stackrox/stackrox-mcp/internal/app"
"github.com/stackrox/stackrox-mcp/internal/config"
"github.com/stretchr/testify/require"
)

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

return NewMCPTestClient(t, runFunc)
}

// SetupInitializedClient creates an initialized MCP client for testing with automatic cleanup.
func SetupInitializedClient(t *testing.T, createClient func(*testing.T) (*MCPTestClient, error)) *MCPTestClient {
t.Helper()

client, err := createClient(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 *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)
RequireNoError(t, result)

return result
}

// GetTextContent extracts text from the first content item.
func GetTextContent(t *testing.T, result *mcp.CallToolResult) string {
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
}
48 changes: 48 additions & 0 deletions internal/testutil/test_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package testutil

import (
"context"
"testing"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/require"
)

// SetupInitializedClient creates an initialized MCP client with automatic cleanup.
func SetupInitializedClient(t *testing.T, createClient func(*testing.T) (*MCPTestClient, error)) *MCPTestClient {
t.Helper()

client, err := createClient(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 *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)
RequireNoError(t, result)

return result
}

// GetTextContent extracts text from the first content item.
func GetTextContent(t *testing.T, result *mcp.CallToolResult) string {
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
}
Loading
Loading