servertest

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2025 License: MIT Imports: 16 Imported by: 0

README

Server Test Harness

This directory contains a comprehensive test harness for the Forgejo MCP server, providing utilities for testing MCP protocol interactions, tool calls, and server behavior with both real and mock backends.

Overview

The test harness consists of two main components:

  1. TestServer - A wrapper around the MCP server for testing
  2. MockGiteaServer - A mock HTTP server that simulates Gitea API responses

Components

TestServer

The TestServer struct provides a controlled environment for testing MCP server functionality:

type TestServer struct {
    ctx     context.Context
    cancel  context.CancelFunc
    t       *testing.T
    client  *client.Client
    once    *sync.Once
    started bool
}
Key Methods
  • NewTestServer(t, ctx, env) - Creates a new test server instance
  • Start() - Starts the MCP server process
  • Initialize() - Performs MCP protocol initialization handshake
  • Client() - Returns the MCP client for making requests
  • IsRunning() - Checks if the server is running
Usage Example
func TestMyTool(t *testing.T) {
    mock := NewMockGiteaServer(t)
    ts := NewTestServer(t, t.Context(), map[string]string{
        "FORGEJO_REMOTE_URL": mock.URL(),
        "FORGEJO_AUTH_TOKEN": "mock-token",
    })

    // Initialize the MCP connection
    if err := ts.Initialize(); err != nil {
        t.Fatalf("Failed to initialize: %v", err)
    }

    // Call a tool
    result, err := ts.Client().CallTool(context.Background(), mcp.CallToolRequest{
        Params: mcp.CallToolParams{
            Name: "list_issues",
            Arguments: map[string]any{
                "repository": "owner/repo",
                "limit": 10,
            },
        },
    })

    if err != nil {
        t.Fatalf("Tool call failed: %v", err)
    }

    // Assert on result
    if result.Content == nil {
        t.Error("Expected content in result")
    }
}
MockGiteaServer

The MockGiteaServer provides a mock HTTP server that simulates Gitea API endpoints:

type MockGiteaServer struct {
    server *httptest.Server
    issues map[string][]MockIssue
}
Key Methods
  • NewMockGiteaServer(t) - Creates a new mock server
  • URL() - Returns the mock server URL
  • AddIssues(owner, repo, issues) - Adds mock issues for a repository
Mock Issue Structure
type MockIssue struct {
    Index int    `json:"index"`
    Title string `json:"title"`
    State string `json:"state"`
}
Usage Example
func TestWithMockData(t *testing.T) {
    mock := NewMockGiteaServer(t)

    // Add mock issues
    mock.AddIssues("testuser", "testrepo", []MockIssue{
        {Index: 1, Title: "Bug: Login fails", State: "open"},
        {Index: 2, Title: "Feature: Add dark mode", State: "open"},
        {Index: 3, Title: "Fix: Memory leak", State: "closed"},
    })

    // Use in test server
    ts := NewTestServer(t, t.Context(), map[string]string{
        "FORGEJO_REMOTE_URL": mock.URL(),
        "FORGEJO_AUTH_TOKEN": "mock-token",
    })
}

Test Categories

Acceptance Tests (acceptance_test.go)

These tests validate end-to-end functionality with realistic scenarios:

  • TestListIssuesAcceptance - Basic issue listing functionality
  • TestListIssuesPagination - Pagination parameter handling
  • TestListIssuesErrorHandling - Error scenario validation
  • TestListIssuesInputValidation - Input parameter validation
  • TestListIssuesConcurrent - Concurrent request handling
  • TestListIssuesInvalidLimit - Invalid parameter handling
Integration Tests (integration_test.go)

These tests validate MCP protocol interactions and server behavior:

  • TestMCPInitialization - MCP protocol handshake
  • TestToolDiscovery - Tool listing and schema validation
  • TestHelloTool - Basic tool execution
  • TestToolExecution - Tool execution with various scenarios
  • TestErrorHandling - Error handling and edge cases
  • TestConcurrentRequests - Concurrent request processing

Running Tests

Run All Tests
go test ./server_test/...
Run Specific Test Categories
# Acceptance tests only
go test -run Acceptance ./server_test/

# Integration tests only
go test -run Integration ./server_test/
Run with Verbose Output
go test -v ./server_test/...
Run with Coverage
go test -cover ./server_test/

Environment Variables

The test harness supports configuration through environment variables:

  • FORGEJO_REMOTE_URL - URL of the Gitea/Forgejo instance (defaults to mock server URL)
  • FORGEJO_AUTH_TOKEN - Authentication token (defaults to "test-token")

Best Practices

Test Structure
  1. Setup: Create mock server and test server instances
  2. Initialize: Call ts.Initialize() to establish MCP connection
  3. Execute: Make tool calls using ts.Client().CallTool()
  4. Assert: Validate results and error conditions
  5. Cleanup: Automatic cleanup via t.Cleanup() calls
Mock Data Management
  1. Use descriptive repository names (e.g., "testuser/testrepo")
  2. Add realistic mock data that matches expected API responses
  3. Test both success and error scenarios
  4. Use consistent mock data across related tests
Error Testing
  1. Test invalid parameters and edge cases
  2. Validate error responses contain appropriate error messages
  3. Test network failures and timeout scenarios
  4. Verify proper error propagation through MCP protocol
Concurrent Testing
  1. Use goroutines to simulate concurrent requests
  2. Validate thread safety of server operations
  3. Test resource cleanup under concurrent load
  4. Verify consistent results across concurrent executions

Example Test Patterns

Basic Tool Testing
func TestBasicTool(t *testing.T) {
    mock := NewMockGiteaServer(t)
    ts := NewTestServer(t, t.Context(), map[string]string{
        "FORGEJO_REMOTE_URL": mock.URL(),
    })

    if err := ts.Initialize(); err != nil {
        t.Fatal(err)
    }

    result, err := ts.Client().CallTool(context.Background(), mcp.CallToolRequest{
        Params: mcp.CallToolParams{Name: "hello"},
    })

    if err != nil {
        t.Fatal(err)
    }

    expected := "Hello, World!"
    if text := result.Content[0].(mcp.TextContent).Text; text != expected {
        t.Errorf("Expected %q, got %q", expected, text)
    }
}
Error Scenario Testing
func TestErrorScenario(t *testing.T) {
    mock := NewMockGiteaServer(t)
    ts := NewTestServer(t, t.Context(), map[string]string{
        "FORGEJO_REMOTE_URL": mock.URL(),
    })

    if err := ts.Initialize(); err != nil {
        t.Fatal(err)
    }

    // Test with invalid repository
    result, err := ts.Client().CallTool(context.Background(), mcp.CallToolRequest{
        Params: mcp.CallToolParams{
            Name: "list_issues",
            Arguments: map[string]any{
                "repository": "invalid/repo",
            },
        },
    })

    if err != nil {
        t.Fatal(err)
    }

    if result.Content == nil {
        t.Error("Expected error content")
    }
}
Concurrent Load Testing
func TestConcurrentLoad(t *testing.T) {
    mock := NewMockGiteaServer(t)
    ts := NewTestServer(t, t.Context(), map[string]string{
        "FORGEJO_REMOTE_URL": mock.URL(),
    })

    if err := ts.Initialize(); err != nil {
        t.Fatal(err)
    }

    const numRequests = 10
    results := make(chan error, numRequests)

    for range numRequests {
        go func() {
            _, err := ts.Client().CallTool(context.Background(), mcp.CallToolRequest{
                Params: mcp.CallToolParams{Name: "hello"},
            })
            results <- err
        }()
    }

    for range numRequests {
        if err := <-results; err != nil {
            t.Errorf("Concurrent request failed: %v", err)
        }
    }
}

Troubleshooting

Common Issues
  1. Server not starting: Check that environment variables are properly set
  2. MCP initialization failures: Verify protocol version compatibility
  3. Mock server issues: Ensure mock data is added before making requests
  4. Timeout errors: Increase context timeouts for complex operations
Debug Tips
  1. Use t.Log() to output debug information
  2. Enable verbose test output with -v flag
  3. Check mock server logs for unexpected requests
  4. Validate MCP message formats in failing tests

Contributing

When adding new tests:

  1. Follow existing naming conventions
  2. Add appropriate mock data for your test scenarios
  3. Include both positive and negative test cases
  4. Document complex test setups with comments
  5. Ensure tests are isolated and don't depend on external state

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertToolResultContains

func AssertToolResultContains(t *testing.T, result *mcp.CallToolResult, expectedText string, expectError bool)

AssertToolResultContains validates that a tool result contains expected text

Parameters:

  • t: *testing.T for test context and error reporting
  • result: *mcp.CallToolResult the result to validate
  • expectedText: string the expected text to contain
  • expectError: bool whether to expect an error result

Example usage:

AssertToolResultContains(t, result, "Comment created successfully", false)

func AssertToolResultEqual

func AssertToolResultEqual(t *testing.T, expected, actual *mcp.CallToolResult)

AssertToolResultEqual compares two tool results for equality with detailed error reporting

Parameters:

  • t: *testing.T for test context and error reporting
  • expected: *mcp.CallToolResult the expected result
  • actual: *mcp.CallToolResult the actual result

Example usage:

AssertToolResultEqual(t, tc.expect, result)

func CreateStandardTestContext

func CreateStandardTestContext(t *testing.T, timeoutSeconds int) (context.Context, context.CancelFunc)

CreateStandardTestContext creates a standardized test context with proper timeout and cleanup

Parameters:

  • t: *testing.T for test context
  • timeoutSeconds: int timeout in seconds (defaults to 5 if 0)

Returns:

  • context.Context: the created context
  • context.CancelFunc: the cancel function for cleanup

Example usage:

ctx, cancel := CreateStandardTestContext(t, 10)
defer cancel()

func CreateTestContext

func CreateTestContext(t *testing.T, timeout time.Duration) (context.Context, context.CancelFunc)

CreateTestContext creates a standardized test context with timeout

Parameters:

  • t: *testing.T for test context
  • timeout: time.Duration for the context timeout (defaults to 5 seconds if 0)

Returns:

  • context.Context: the created context
  • context.CancelFunc: the cancel function for cleanup

Example usage:

ctx, cancel := CreateTestContext(t, 10*time.Second)
defer cancel()

func GetStructuredContent

func GetStructuredContent(result *mcp.CallToolResult) map[string]any

GetStructuredContent extracts structured content from MCP result

Parameters:

  • result: *mcp.CallToolResult the result to extract structured content from

Returns:

  • map[string]any: the structured content, or nil if not found

Example usage:

structured := GetStructuredContent(result)
if comment, ok := structured["comment"].(map[string]any); ok {
    t.Logf("Comment ID: %v", comment["id"])
}

func GetTextContent

func GetTextContent(content []mcp.Content) string

GetTextContent extracts text content from MCP content slice

Parameters:

  • content: []mcp.Content the content slice to extract from

Returns:

  • string: the extracted text content, or empty string if not found

Example usage:

text := GetTextContent(result.Content)
if strings.Contains(text, "success") {
    t.Log("Operation succeeded")
}

func RunConcurrentTest

func RunConcurrentTest(t *testing.T, numGoroutines int, testFunc func(int) error)

RunConcurrentTest executes a function concurrently with proper synchronization and error handling

Parameters:

  • t: *testing.T for test context and error reporting
  • numGoroutines: int number of goroutines to run
  • testFunc: func(int) error function to execute in each goroutine

Example usage:

RunConcurrentTest(t, 3, func(id int) error {
    _, err := ts.Client().CallTool(ctx, &mcp.CallToolParams{
        Name: "tool_name",
        Arguments: map[string]any{
            "id": id,
        },
    })
    return err
})

func ValidateToolCall

func ValidateToolCall(t *testing.T, client *mcp.ClientSession, ctx context.Context, toolName string, arguments map[string]any, expectedError string) *mcp.CallToolResult

ValidateToolCall executes a tool call with standardized validation and error handling

Parameters:

  • t: *testing.T for test context and error reporting
  • client: *mcp.ClientSession the MCP client session
  • ctx: context.Context for the tool call
  • toolName: string name of the tool to call
  • arguments: map[string]any arguments for the tool call
  • expectedError: string expected error text (empty for success case)

Returns:

  • *mcp.CallToolResult: the result of the tool call (nil on error)

Example usage:

result := ValidateToolCall(t, client, ctx, "issue_list", map[string]any{
    "repository": "testuser/testrepo",
}, "")
if result != nil {
    t.Log("Tool call succeeded")
}

Types

type MockComment

type MockComment struct {
	ID      int    `json:"id"`
	Content string `json:"body"`
	Author  string `json:"user"`
	Created string `json:"created_at"`
	Updated string `json:"updated_at"`
}

MockComment represents a mock comment for testing

type MockCommentUser

type MockCommentUser struct {
	ID       int    `json:"id"`
	Username string `json:"username"`
}

MockCommentUser represents the user who created the comment

type MockGiteaServer

type MockGiteaServer struct {
	// contains filtered or unexported fields
}

MockGiteaServer represents a mock Gitea API server for testing

func NewMockGiteaServer

func NewMockGiteaServer(t *testing.T) *MockGiteaServer

NewMockGiteaServer creates a new mock Gitea server

func (*MockGiteaServer) AddComments

func (m *MockGiteaServer) AddComments(owner, repo string, comments []MockComment)

AddComments adds mock comments for a repository

func (*MockGiteaServer) AddFile

func (m *MockGiteaServer) AddFile(owner, repo, ref, filepath string, content []byte)

AddFile adds mock file content for a repository

func (*MockGiteaServer) AddIssue

func (m *MockGiteaServer) AddIssue(owner, repo string, issue MockIssue)

AddIssue adds a single mock issue for a repository

func (*MockGiteaServer) AddIssues

func (m *MockGiteaServer) AddIssues(owner, repo string, issues []MockIssue)

AddIssues adds mock issues for a repository

func (*MockGiteaServer) AddNotifications added in v0.2.1

func (m *MockGiteaServer) AddNotifications(notifications []MockNotification)

AddNotifications adds mock notifications

func (*MockGiteaServer) AddPullRequests

func (m *MockGiteaServer) AddPullRequests(owner, repo string, pullRequests []MockPullRequest)

AddPullRequests adds mock pull requests for a repository

func (*MockGiteaServer) SetForbiddenCommentEdit

func (m *MockGiteaServer) SetForbiddenCommentEdit(commentID int)

SetForbiddenCommentEdit marks a comment ID as forbidden (will return 403)

func (*MockGiteaServer) SetNotFoundRepo

func (m *MockGiteaServer) SetNotFoundRepo(owner, repo string)

SetNotFoundRepo marks a repository as not found (will return 404)

func (*MockGiteaServer) SetServerErrorCommentEdit

func (m *MockGiteaServer) SetServerErrorCommentEdit(commentID int)

SetServerErrorCommentEdit marks a comment ID as server error (will return 500)

func (*MockGiteaServer) URL

func (m *MockGiteaServer) URL() string

URL returns the mock server URL

type MockIssue

type MockIssue struct {
	Index   int    `json:"index"`
	Title   string `json:"title"`
	Body    string `json:"body"`
	State   string `json:"state"`
	Updated string `json:"updated_at"`
	Created string `json:"created_at"`
}

MockIssue represents a mock issue for testing

type MockNotification added in v0.2.1

type MockNotification struct {
	ID         int    `json:"id"`
	Repository string `json:"repository"`
	Type       string `json:"type"`
	Number     int    `json:"number"`
	Title      string `json:"title"`
	Unread     bool   `json:"unread"`
	Updated    string `json:"updated_at"`
	URL        string `json:"url"`
}

MockNotification represents a mock notification for testing

type MockPullRequest

type MockPullRequest struct {
	ID        int    `json:"id"`
	Number    int    `json:"number"`
	Title     string `json:"title"`
	Body      string `json:"body"`
	State     string `json:"state"`
	BaseRef   string `json:"base_ref"`
	UpdatedAt string `json:"updated_at"`
}

MockPullRequest represents a mock pull request for testing

type TestServer

type TestServer struct {
	// contains filtered or unexported fields
}

TestServer represents a test harness for running the MCP server

func NewTestServer

func NewTestServer(t *testing.T, ctx context.Context, env map[string]string) *TestServer

NewTestServer creates a new TestServer instance with standardized setup This is the primary constructor for most tests, providing a clean API while maintaining backward compatibility.

Example usage:

ts := NewTestServer(t, ctx, map[string]string{
	"FORGEJO_REMOTE_URL": mock.URL(),
	"FORGEJO_AUTH_TOKEN": "mock-token",
})

func NewTestServerWithCompat added in v0.2.0

func NewTestServerWithCompat(t *testing.T, ctx context.Context, env map[string]string, compat bool) *TestServer

NewTestServerWithCompat creates a new test server with optional compatibility mode

func NewTestServerWithCompatAndDebug added in v0.2.0

func NewTestServerWithCompatAndDebug(t *testing.T, ctx context.Context, env map[string]string, debug, compat bool) *TestServer

NewTestServerWithCompatAndDebug creates a new test server with optional debug and compatibility modes

func NewTestServerWithDebug

func NewTestServerWithDebug(t *testing.T, ctx context.Context, env map[string]string, debug bool) *TestServer

NewTestServerWithDebug creates a new test server with optional debug mode

func (*TestServer) CallToolWithValidation

func (ts *TestServer) CallToolWithValidation(ctx context.Context, toolName string, arguments map[string]any) (*mcp.CallToolResult, error)

CallToolWithValidation calls a tool with standardized error handling and validation

Parameters:

  • ctx: context.Context for the tool call
  • toolName: string name of the tool to call
  • arguments: map[string]any arguments for the tool call

Returns:

  • *mcp.CallToolResult: the result of the tool call
  • error: any error that occurred during the call

Example usage:

result, err := ts.CallToolWithValidation(ctx, "issue_list", map[string]any{
    "repository": "testuser/testrepo",
    "limit":      10,
})

func (*TestServer) Client

func (ts *TestServer) Client() *mcp.ClientSession

Client returns the MCP client session for tool calls

func (*TestServer) Initialize

func (ts *TestServer) Initialize() error

Initialize initializes the MCP client for communication with the server

func (*TestServer) IsRunning

func (ts *TestServer) IsRunning() bool

IsRunning checks if the server process is running

func (*TestServer) Start

func (ts *TestServer) Start() error

Start starts the server process with error handling

func (*TestServer) ValidateErrorResult

func (ts *TestServer) ValidateErrorResult(result *mcp.CallToolResult, expectedErrorText string, t *testing.T) bool

ValidateErrorResult validates that a tool result contains an expected error

Parameters:

  • result: *mcp.CallToolResult the result to validate
  • expectedErrorText: string the expected error text (partial match allowed)
  • t: *testing.T for reporting errors

Returns:

  • bool: true if result contains expected error, false otherwise

Example usage:

if !ts.ValidateErrorResult(result, "Invalid request", t) {
    t.Errorf("Expected error result not found")
}

func (*TestServer) ValidateSuccessResult

func (ts *TestServer) ValidateSuccessResult(result *mcp.CallToolResult, expectedSuccessText string, t *testing.T) bool

func (*TestServer) ValidateToolResult

func (ts *TestServer) ValidateToolResult(expected, actual *mcp.CallToolResult, t *testing.T) bool

ValidateToolResult compares an actual tool result with expected result using deep equality

Parameters:

  • expected: *mcp.CallToolResult the expected result
  • actual: *mcp.CallToolResult the actual result
  • t: *testing.T for reporting errors

Returns:

  • bool: true if results match, false otherwise

Example usage:

if !ts.ValidateToolResult(tc.expect, result, t) {
    t.Errorf("Tool result validation failed")
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL