diff --git a/cmd/server/main.go b/cmd/server/main.go index c5fd8e4..2425d0a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -73,6 +73,14 @@ func startHTTPServer(cfg *config.Config, queryClient overlockv1beta1.QueryClient environmentHandler := handler.NewEnvironmentHandler(queryClient, cfg.APITimeout) mcp.AddTool(srv, environmentTool, environmentHandler.Handle) + // Register get-environments tool + environmentsTool := &mcp.Tool{ + Name: "get-environments", + Description: "Get list of all environments in the Overlock Network with optional filtering and pagination", + InputSchema: schema.CreateEnvironmentsToolInputSchema(), + } + mcp.AddTool(srv, environmentsTool, environmentHandler.HandleList) + // Create the HTTP handler for MCP httpHandler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { return srv diff --git a/internal/schema/get_environments_tool.go b/internal/schema/get_environments_tool.go new file mode 100644 index 0000000..8422414 --- /dev/null +++ b/internal/schema/get_environments_tool.go @@ -0,0 +1,34 @@ +package schema + +import ( + "github.com/modelcontextprotocol/go-sdk/jsonschema" +) + +// CreateEnvironmentsToolInputSchema creates the JSON schema for the get-environments tool input +// This schema matches the QueryListEnvironmentRequest from the Overlock API +func CreateEnvironmentsToolInputSchema() *jsonschema.Schema { + zero := 0.0 + thousand := 1000.0 + + return &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "creator": { + Type: "string", + Description: "Filter environments by creator address (optional)", + }, + "limit": { + Type: "integer", + Description: "Maximum number of environments to return (default: 100, max: 1000)", + Minimum: &zero, + Maximum: &thousand, + }, + "offset": { + Type: "integer", + Description: "Number of environments to skip for pagination (default: 0)", + Minimum: &zero, + }, + }, + AdditionalProperties: &jsonschema.Schema{}, + } +} \ No newline at end of file diff --git a/internal/schema/get_environments_tool_test.go b/internal/schema/get_environments_tool_test.go new file mode 100644 index 0000000..40a1113 --- /dev/null +++ b/internal/schema/get_environments_tool_test.go @@ -0,0 +1,47 @@ +package schema + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateEnvironmentsToolInputSchema(t *testing.T) { + schema := CreateEnvironmentsToolInputSchema() + + require.NotNil(t, schema) + assert.Equal(t, "object", schema.Type) + + // Check required properties exist + assert.Contains(t, schema.Properties, "creator") + assert.Contains(t, schema.Properties, "limit") + assert.Contains(t, schema.Properties, "offset") + + // Check creator property + creatorProp := schema.Properties["creator"] + assert.Equal(t, "string", creatorProp.Type) + assert.Equal(t, "Filter environments by creator address (optional)", creatorProp.Description) + + // Check limit property + limitProp := schema.Properties["limit"] + assert.Equal(t, "integer", limitProp.Type) + assert.Equal(t, "Maximum number of environments to return (default: 100, max: 1000)", limitProp.Description) + assert.NotNil(t, limitProp.Minimum) + assert.Equal(t, 0.0, *limitProp.Minimum) + assert.NotNil(t, limitProp.Maximum) + assert.Equal(t, 1000.0, *limitProp.Maximum) + + // Check offset property + offsetProp := schema.Properties["offset"] + assert.Equal(t, "integer", offsetProp.Type) + assert.Equal(t, "Number of environments to skip for pagination (default: 0)", offsetProp.Description) + assert.NotNil(t, offsetProp.Minimum) + assert.Equal(t, 0.0, *offsetProp.Minimum) + + // Check no required fields (all optional) + assert.Empty(t, schema.Required) + + // Check additional properties + assert.NotNil(t, schema.AdditionalProperties) +} \ No newline at end of file diff --git a/internal/schema/environment_tool.go b/internal/schema/show_environment_tool.go similarity index 100% rename from internal/schema/environment_tool.go rename to internal/schema/show_environment_tool.go diff --git a/internal/schema/environment_tool_test.go b/internal/schema/show_environment_tool_test.go similarity index 100% rename from internal/schema/environment_tool_test.go rename to internal/schema/show_environment_tool_test.go diff --git a/pkg/handler/environment.go b/pkg/handler/environment.go index 080350e..ee45089 100644 --- a/pkg/handler/environment.go +++ b/pkg/handler/environment.go @@ -7,6 +7,7 @@ import ( "time" "github.com/Oudwins/zog" + "github.com/cosmos/cosmos-sdk/types/query" "github.com/modelcontextprotocol/go-sdk/mcp" overlockv1beta1 "github.com/overlock-network/api/go/node/overlock/crossplane/v1beta1" "github.com/rs/zerolog/log" @@ -18,7 +19,14 @@ type EnvironmentInput struct { Id int `json:"id,omitempty"` } -// EnvironmentHandler handles the show-environment tool requests +// EnvironmentsListInput represents the input parameters for the get-environments tool +type EnvironmentsListInput struct { + Creator string `json:"creator,omitempty"` + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` +} + +// EnvironmentHandler handles both show-environment and get-environments tool requests type EnvironmentHandler struct { chainClient overlockv1beta1.QueryClient timeout time.Duration @@ -190,3 +198,140 @@ func (h *EnvironmentHandler) Handle(ctx context.Context, session *mcp.ServerSess }, }, nil } + +// HandleList processes the get-environments tool call +func (h *EnvironmentHandler) HandleList(ctx context.Context, session *mcp.ServerSession, params *mcp.CallToolParams) (*mcp.CallToolResult, error) { + // Create a logger with request context + logger := log.With(). + Str("tool", "get-environments"). + Str("request_id", fmt.Sprintf("%p", params)). + Logger() + + start := time.Now() + logger.Info().Msg("Processing get-environments request") + + // Apply timeout to the context + timeoutCtx, cancel := context.WithTimeout(ctx, h.timeout) + defer cancel() + + // Define validation schema using Zog with default values + schema := zog.Struct(zog.Shape{ + "creator": zog.String().Default(""), + "limit": zog.Int().LTE(1000).Default(100), + "offset": zog.Int().Default(0), + }) + + // Validate input parameters (always parse to apply defaults) + var input EnvironmentsListInput + arguments := params.Arguments + if arguments == nil { + arguments = make(map[string]interface{}) + } + + logger.Debug().Interface("arguments", arguments).Msg("Validating input arguments") + // Parse and validate the arguments + errs := schema.Parse(arguments, &input) + if errs != nil { + logger.Error().Interface("errors", errs).Msg("Input validation failed") + return nil, fmt.Errorf("validation failed: %v", errs) + } + logger.Debug().Interface("parsed_input", input).Msg("Input validation successful") + + // Set default pagination + req := &overlockv1beta1.QueryListEnvironmentRequest{ + Pagination: &query.PageRequest{ + Limit: 100, + Offset: 0, + }, + } + + // Apply validated parameters + if input.Creator != "" { + req.Creator = input.Creator + } + if input.Limit > 0 { + req.Pagination.Limit = uint64(input.Limit) + } + req.Pagination.Offset = uint64(input.Offset) + + // Log request parameters + logger.Info(). + Uint64("limit", req.Pagination.Limit). + Uint64("offset", req.Pagination.Offset). + Str("creator", req.Creator). + Msg("Fetching environments from blockchain") + + // Check if chain client is available + if h.chainClient == nil { + logger.Error().Msg("gRPC client is not available") + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: "Error: gRPC connection to blockchain is not available. Please check the connection and try again.", + }, + }, + }, nil + } + + // Fetch environments from the chain using circuit breaker protection + result, err := h.circuitBreaker.Execute(func() (interface{}, error) { + return h.chainClient.ListEnvironment(timeoutCtx, req) + }) + + if err != nil { + logger.Info().Err(err).Msg("Failed to connect to gRPC server - blockchain service unavailable") + // Check if it's a circuit breaker error + if err == gobreaker.ErrOpenState { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: "Blockchain service is currently unavailable (circuit breaker protection active). Please try again later.", + }, + }, + }, nil + } + // Return a user-friendly response instead of propagating the error + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: "Unable to connect to blockchain service. The service may be temporarily unavailable. Please check your connection and try again later.", + }, + }, + }, nil + } + + chainResponse, ok := result.(*overlockv1beta1.QueryListEnvironmentResponse) + if !ok || chainResponse == nil { + logger.Error().Msg("Received invalid response from blockchain service") + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: "Received invalid response from blockchain service. Please try again later.", + }, + }, + }, nil + } + + // Log successful response + environmentCount := len(chainResponse.Environments) + duration := time.Since(start) + logger.Info(). + Int("environment_count", environmentCount). + Dur("duration", duration). + Msg("Successfully fetched environments") + + // Use the official API response directly + responseJSON, err := json.MarshalIndent(chainResponse, "", " ") + if err != nil { + logger.Error().Err(err).Msg("Failed to marshal response") + return nil, fmt.Errorf("failed to marshal environments response: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: string(responseJSON), + }, + }, + }, nil +} diff --git a/pkg/handler/environment_test.go b/pkg/handler/environment_test.go index 05860af..5359cc5 100644 --- a/pkg/handler/environment_test.go +++ b/pkg/handler/environment_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/cosmos/cosmos-sdk/types/query" "github.com/modelcontextprotocol/go-sdk/mcp" overlockv1beta1 "github.com/overlock-network/api/go/node/overlock/crossplane/v1beta1" "github.com/sony/gobreaker" @@ -241,3 +242,185 @@ func TestEnvironmentHandler_Handle_WithValidID(t *testing.T) { mockClient.AssertExpectations(t) } + +// Tests for HandleList method (get-environments tool) +func TestEnvironmentHandler_HandleList_Success(t *testing.T) { + mockClient := &MockQueryClient{} + handler := NewEnvironmentHandler(mockClient, 30*time.Second) + + ctx := context.Background() + session := &mcp.ServerSession{} + + expectedResponse := &overlockv1beta1.QueryListEnvironmentResponse{ + Environments: []overlockv1beta1.Environment{ + { + Id: 1, + Creator: "test-creator-1", + }, + }, + Pagination: &query.PageResponse{ + Total: 1, + }, + } + + mockClient.On("ListEnvironment", mock.AnythingOfType("*context.timerCtx"), mock.MatchedBy(func(req *overlockv1beta1.QueryListEnvironmentRequest) bool { + return req.Pagination.Limit == 10 && req.Pagination.Offset == 0 + })).Return(expectedResponse, nil) + + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{ + "limit": 10, + }, + } + + result, err := handler.HandleList(ctx, session, params) + + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result.Content, 1) + + textContent, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok) + + var response overlockv1beta1.QueryListEnvironmentResponse + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + assert.Len(t, response.Environments, 1) + assert.Equal(t, uint64(1), response.Environments[0].Id) + + mockClient.AssertExpectations(t) +} + +func TestEnvironmentHandler_HandleList_NilClient(t *testing.T) { + handler := NewEnvironmentHandler(nil, 30*time.Second) + + ctx := context.Background() + session := &mcp.ServerSession{} + + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{}, + } + + result, err := handler.HandleList(ctx, session, params) + + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result.Content, 1) + + textContent, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok) + assert.Contains(t, textContent.Text, "gRPC connection to blockchain is not available") +} + +func TestEnvironmentHandler_HandleList_ValidationError(t *testing.T) { + mockClient := &MockQueryClient{} + handler := NewEnvironmentHandler(mockClient, 30*time.Second) + + ctx := context.Background() + session := &mcp.ServerSession{} + + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{ + "limit": 2000, // Exceeds maximum limit + }, + } + + result, err := handler.HandleList(ctx, session, params) + + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "validation failed") +} + +func TestEnvironmentHandler_HandleList_DefaultValues(t *testing.T) { + mockClient := &MockQueryClient{} + handler := NewEnvironmentHandler(mockClient, 30*time.Second) + + ctx := context.Background() + session := &mcp.ServerSession{} + + expectedResponse := &overlockv1beta1.QueryListEnvironmentResponse{ + Environments: []overlockv1beta1.Environment{}, + Pagination: &query.PageResponse{}, + } + + mockClient.On("ListEnvironment", mock.AnythingOfType("*context.timerCtx"), mock.MatchedBy(func(req *overlockv1beta1.QueryListEnvironmentRequest) bool { + return req.Pagination.Limit == 100 && req.Pagination.Offset == 0 && req.Creator == "" + })).Return(expectedResponse, nil) + + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{}, + } + + result, err := handler.HandleList(ctx, session, params) + + require.NoError(t, err) + require.NotNil(t, result) + + mockClient.AssertExpectations(t) +} + +func TestEnvironmentHandler_HandleList_CircuitBreakerOpen(t *testing.T) { + handler := NewEnvironmentHandler(nil, 30*time.Second) + + handler.circuitBreaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{ + Name: "test-breaker", + MaxRequests: 0, + ReadyToTrip: func(counts gobreaker.Counts) bool { + return true + }, + }) + + ctx := context.Background() + session := &mcp.ServerSession{} + + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{}, + } + + result, err := handler.HandleList(ctx, session, params) + + require.NoError(t, err) + require.NotNil(t, result) + require.Len(t, result.Content, 1) + + textContent, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok) + assert.Contains(t, textContent.Text, "gRPC connection to blockchain is not available") +} + +func TestEnvironmentHandler_HandleList_WithCreatorFilter(t *testing.T) { + mockClient := &MockQueryClient{} + handler := NewEnvironmentHandler(mockClient, 30*time.Second) + + ctx := context.Background() + session := &mcp.ServerSession{} + + expectedResponse := &overlockv1beta1.QueryListEnvironmentResponse{ + Environments: []overlockv1beta1.Environment{}, + Pagination: &query.PageResponse{}, + } + + mockClient.On("ListEnvironment", mock.AnythingOfType("*context.timerCtx"), mock.MatchedBy(func(req *overlockv1beta1.QueryListEnvironmentRequest) bool { + return req.Creator == "test-creator-address" && req.Pagination.Limit == 100 && req.Pagination.Offset == 0 + })).Return(expectedResponse, nil) + + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{ + "creator": "test-creator-address", + }, + } + + result, err := handler.HandleList(ctx, session, params) + + require.NoError(t, err) + require.NotNil(t, result) + + mockClient.AssertExpectations(t) +} diff --git a/test/environments_e2e_test.go b/test/environments_e2e_test.go new file mode 100644 index 0000000..0c410bf --- /dev/null +++ b/test/environments_e2e_test.go @@ -0,0 +1,241 @@ +package test + +import ( + "context" + "encoding/json" + "path/filepath" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "overlock-mcp-server/pkg/handler" + "overlock-mcp-server/test/mocks" + + "github.com/modelcontextprotocol/go-sdk/mcp" + overlockv1beta1 "github.com/overlock-network/api/go/node/overlock/crossplane/v1beta1" +) + +var _ = Describe("Environments E2E Test", func() { + var ( + mockClient *mocks.MockQueryClient + environmentHandler *handler.EnvironmentHandler + ctx context.Context + session *mcp.ServerSession + ) + + BeforeEach(func() { + ctx = context.Background() + session = &mcp.ServerSession{} + + // Setup mock client with test data + testDataDir, err := filepath.Abs("testdata") + Expect(err).ToNot(HaveOccurred()) + + mockClient = mocks.NewMockQueryClient(testDataDir) + environmentHandler = handler.NewEnvironmentHandler(mockClient, 30*time.Second) + }) + + Describe("Get environments tool", func() { + Context("when called without arguments", func() { + It("should return all environments", func() { + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: nil, + } + + result, err := environmentHandler.HandleList(ctx, session, params) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + Expect(result.Content).To(HaveLen(1)) + + textContent, ok := result.Content[0].(*mcp.TextContent) + Expect(ok).To(BeTrue()) + + var response overlockv1beta1.QueryListEnvironmentResponse + err = json.Unmarshal([]byte(textContent.Text), &response) + Expect(err).ToNot(HaveOccurred()) + + Expect(response.Environments).To(HaveLen(2)) + Expect(response.Environments[0].Creator).To(Equal("overlock1abc123def456ghi789jkl012mno345pqr678stu901vwx")) + Expect(response.Environments[1].Creator).To(Equal("overlock1xyz789ghi456def123abc012mno345pqr678stu901vwx")) + Expect(response.Environments[0].Metadata.Name).To(Equal("test-environment-1")) + Expect(response.Environments[1].Metadata.Name).To(Equal("test-environment-2")) + }) + }) + + Context("when called with creator filter", func() { + It("should return filtered environments", func() { + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{ + "creator": "overlock1abc123def456ghi789jkl012mno345pqr678stu901vwx", + }, + } + + result, err := environmentHandler.HandleList(ctx, session, params) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + textContent, ok := result.Content[0].(*mcp.TextContent) + Expect(ok).To(BeTrue()) + + var response overlockv1beta1.QueryListEnvironmentResponse + err = json.Unmarshal([]byte(textContent.Text), &response) + Expect(err).ToNot(HaveOccurred()) + + Expect(response.Environments).To(HaveLen(1)) + Expect(response.Environments[0].Creator).To(Equal("overlock1abc123def456ghi789jkl012mno345pqr678stu901vwx")) + Expect(response.Environments[0].Metadata.Name).To(Equal("test-environment-1")) + Expect(response.Environments[0].Metadata.Annotations).To(ContainSubstring("Test environment for E2E testing")) + }) + }) + + Context("when called with pagination", func() { + It("should return paginated results", func() { + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{ + "limit": 1, + "offset": 0, + }, + } + + result, err := environmentHandler.HandleList(ctx, session, params) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + textContent, ok := result.Content[0].(*mcp.TextContent) + Expect(ok).To(BeTrue()) + + var response overlockv1beta1.QueryListEnvironmentResponse + err = json.Unmarshal([]byte(textContent.Text), &response) + Expect(err).ToNot(HaveOccurred()) + + Expect(response.Environments).To(HaveLen(1)) + Expect(response.Environments[0].Id).To(Equal(uint64(1001))) + Expect(response.Environments[0].Provider).To(Equal(uint64(1))) + }) + }) + + Context("when called with empty creator filter", func() { + It("should return all environments", func() { + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{ + "creator": "", + }, + } + + result, err := environmentHandler.HandleList(ctx, session, params) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + textContent, ok := result.Content[0].(*mcp.TextContent) + Expect(ok).To(BeTrue()) + + var response overlockv1beta1.QueryListEnvironmentResponse + err = json.Unmarshal([]byte(textContent.Text), &response) + Expect(err).ToNot(HaveOccurred()) + + Expect(response.Environments).To(HaveLen(2)) + }) + }) + + Context("when called with invalid limit", func() { + It("should return validation error", func() { + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{ + "limit": 2000, // Exceeds maximum limit of 1000 + }, + } + + result, err := environmentHandler.HandleList(ctx, session, params) + Expect(err).To(HaveOccurred()) + Expect(result).To(BeNil()) + Expect(err.Error()).To(ContainSubstring("validation failed")) + }) + }) + + Context("when called with valid environment metadata", func() { + It("should return environment with proper metadata structure", func() { + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{ + "limit": 1, + }, + } + + result, err := environmentHandler.HandleList(ctx, session, params) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + textContent, ok := result.Content[0].(*mcp.TextContent) + Expect(ok).To(BeTrue()) + + var response overlockv1beta1.QueryListEnvironmentResponse + err = json.Unmarshal([]byte(textContent.Text), &response) + Expect(err).ToNot(HaveOccurred()) + + Expect(response.Environments).To(HaveLen(1)) + env := response.Environments[0] + + Expect(env.Metadata).ToNot(BeNil()) + Expect(env.Metadata.Name).To(Equal("test-environment-1")) + Expect(env.Metadata.Annotations).To(ContainSubstring("region")) + Expect(env.Metadata.Annotations).To(ContainSubstring("us-east-1")) + Expect(env.Metadata.Annotations).To(ContainSubstring("development")) + Expect(env.Metadata.Annotations).To(ContainSubstring("Test environment for E2E testing")) + + Expect(env.Provider).To(Equal(uint64(1))) + Expect(env.Creator).To(Equal("overlock1abc123def456ghi789jkl012mno345pqr678stu901vwx")) + }) + }) + + Context("when called with non-existent creator filter", func() { + It("should return empty results", func() { + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{ + "creator": "overlock1nonexistent123456789", + }, + } + + result, err := environmentHandler.HandleList(ctx, session, params) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + + textContent, ok := result.Content[0].(*mcp.TextContent) + Expect(ok).To(BeTrue()) + + var response overlockv1beta1.QueryListEnvironmentResponse + err = json.Unmarshal([]byte(textContent.Text), &response) + Expect(err).ToNot(HaveOccurred()) + + Expect(response.Environments).To(HaveLen(0)) + }) + }) + }) + + Describe("Handler with nil client", func() { + It("should return error message when gRPC client is nil", func() { + // Create handler with nil client + nilHandler := handler.NewEnvironmentHandler(nil, 30*time.Second) + + params := &mcp.CallToolParams{ + Name: "get-environments", + Arguments: map[string]interface{}{}, + } + + result, err := nilHandler.HandleList(ctx, session, params) + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + Expect(result.Content).To(HaveLen(1)) + + textContent, ok := result.Content[0].(*mcp.TextContent) + Expect(ok).To(BeTrue()) + Expect(textContent.Text).To(ContainSubstring("gRPC connection to blockchain is not available")) + }) + }) +}) \ No newline at end of file diff --git a/test/mocks/grpc_client.go b/test/mocks/grpc_client.go index aa71497..6727fbe 100644 --- a/test/mocks/grpc_client.go +++ b/test/mocks/grpc_client.go @@ -123,7 +123,52 @@ func (m *MockQueryClient) ShowEnvironment(ctx context.Context, req *overlockv1be return &response, nil } -// ListEnvironment implements the ListEnvironment method (not used in our test) +// ListEnvironment implements the ListEnvironment method by returning test data func (m *MockQueryClient) ListEnvironment(ctx context.Context, req *overlockv1beta1.QueryListEnvironmentRequest, opts ...grpc.CallOption) (*overlockv1beta1.QueryListEnvironmentResponse, error) { - return nil, nil + // Load test data from JSON file + jsonFile := filepath.Join(m.testDataPath, "environments_response.json") + data, err := os.ReadFile(jsonFile) + if err != nil { + return nil, err + } + + var response overlockv1beta1.QueryListEnvironmentResponse + if err := json.Unmarshal(data, &response); err != nil { + return nil, err + } + + // Apply filtering if creator is specified + if req.Creator != "" { + var filteredEnvironments []overlockv1beta1.Environment + for _, environment := range response.Environments { + if environment.Creator == req.Creator { + filteredEnvironments = append(filteredEnvironments, environment) + } + } + response.Environments = filteredEnvironments + } + + // Apply pagination + if req.Pagination != nil { + offset := int(req.Pagination.Offset) + limit := int(req.Pagination.Limit) + + if offset >= len(response.Environments) { + response.Environments = []overlockv1beta1.Environment{} + } else { + end := offset + limit + if end > len(response.Environments) { + end = len(response.Environments) + } + response.Environments = response.Environments[offset:end] + } + + // Update pagination info + response.Pagination = &query.PageResponse{ + NextKey: []byte{}, + Total: uint64(len(response.Environments)), + } + } + + return &response, nil } diff --git a/test/testdata/environments_response.json b/test/testdata/environments_response.json new file mode 100644 index 0000000..d4e08e6 --- /dev/null +++ b/test/testdata/environments_response.json @@ -0,0 +1,26 @@ +{ + "environments": [ + { + "id": 1001, + "creator": "overlock1abc123def456ghi789jkl012mno345pqr678stu901vwx", + "metadata": { + "name": "test-environment-1", + "annotations": "{\"region\":\"us-east-1\",\"type\":\"development\",\"version\":\"1.0.0\",\"description\":\"Test environment for E2E testing\"}" + }, + "provider": 1 + }, + { + "id": 1002, + "creator": "overlock1xyz789ghi456def123abc012mno345pqr678stu901vwx", + "metadata": { + "name": "test-environment-2", + "annotations": "{\"region\":\"us-west-2\",\"type\":\"production\",\"version\":\"2.1.0\",\"description\":\"Production environment for testing\"}" + }, + "provider": 2 + } + ], + "pagination": { + "next_key": null, + "total": 2 + } +} \ No newline at end of file