Skip to main content
Glama
nomad_client_integration_test.go13 kB
package integration import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/kocierik/mcp-nomad/test/testdata" "github.com/kocierik/mcp-nomad/types" "github.com/kocierik/mcp-nomad/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // MockNomadServer creates a mock Nomad API server for integration testing type MockNomadServer struct { server *httptest.Server } func NewMockNomadServer() *MockNomadServer { mux := http.NewServeMux() mux.HandleFunc("/v1/job/", func(w http.ResponseWriter, r *http.Request) { jobID := r.URL.Path[len("/v1/job/"):] if r.Method == "GET" { job := types.Job{ ID: jobID, Name: jobID, Type: "service", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(job) } else if r.Method == "DELETE" { response := map[string]interface{}{ "EvalID": "eval-456", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } }) // Job submission endpoint mux.HandleFunc("/v1/jobs", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { jobs := testdata.SampleJobs w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(jobs) } else if r.Method == "POST" { // Handle job submission (HCL parsing) // For testing, we'll just return a successful response response := map[string]interface{}{ "EvalID": "eval-123", "JobModifyIndex": 1, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } }) // Job parsing endpoint mux.HandleFunc("/v1/jobs/parse", func(w http.ResponseWriter, r *http.Request) { // Mock HCL parsing - return a simple job structure parsedJob := map[string]interface{}{ "ID": "test-job", "Name": "test-job", "Type": "service", "Datacenters": []string{"dc1"}, "TaskGroups": []map[string]interface{}{ { "Name": "web", "Count": 2, "Tasks": []map[string]interface{}{ { "Name": "nginx", "Driver": "docker", "Config": map[string]interface{}{ "image": "nginx:latest", }, }, }, }, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(parsedJob) }) // Nodes endpoints mux.HandleFunc("/v1/nodes", func(w http.ResponseWriter, r *http.Request) { nodes := testdata.SampleNodes w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(nodes) }) mux.HandleFunc("/v1/node/", func(w http.ResponseWriter, r *http.Request) { nodeID := r.URL.Path[len("/v1/node/"):] if r.Method == "GET" { node := types.Node{ ID: nodeID, Name: nodeID, Status: "ready", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(node) } else if r.Method == "POST" { // Handle drain/eligibility operations // Check if it's an eligibility request by path if strings.Contains(r.URL.Path, "/eligibility") { // Extract node ID from path (remove /eligibility suffix) actualNodeID := strings.TrimSuffix(nodeID, "/eligibility") // For eligibility, return a NodeSummary nodeSummary := types.NodeSummary{ ID: actualNodeID, Name: actualNodeID, Status: "ready", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(nodeSummary) } else { // For drain operations response := map[string]interface{}{ "EvalID": "eval-789", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } } }) // Namespaces endpoints mux.HandleFunc("/v1/namespaces", func(w http.ResponseWriter, r *http.Request) { namespaces := testdata.SampleNamespaces w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(namespaces) }) mux.HandleFunc("/v1/namespace/", func(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { response := map[string]interface{}{ "CreateIndex": 1, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } else if r.Method == "DELETE" { w.WriteHeader(http.StatusOK) } }) // Allocations endpoints mux.HandleFunc("/v1/allocations", func(w http.ResponseWriter, r *http.Request) { allocations := testdata.SampleAllocations w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(allocations) }) mux.HandleFunc("/v1/allocation/", func(w http.ResponseWriter, r *http.Request) { allocID := r.URL.Path[len("/v1/allocation/"):] allocation := types.Allocation{ ID: allocID, Name: "test-allocation", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(allocation) }) // Logs endpoint mux.HandleFunc("/v1/client/fs/logs/", func(w http.ResponseWriter, r *http.Request) { logs := testdata.SampleLogs["nginx_stdout"] w.Header().Set("Content-Type", "text/plain") w.Write([]byte(logs)) }) // Variables endpoints mux.HandleFunc("/v1/vars", func(w http.ResponseWriter, r *http.Request) { variables := testdata.SampleVariables w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(variables) }) mux.HandleFunc("/v1/var/", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { variable := testdata.SampleVariables[0] w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(variable) } else if r.Method == "PUT" { w.WriteHeader(http.StatusOK) } else if r.Method == "DELETE" { w.WriteHeader(http.StatusOK) } }) // ACL endpoints mux.HandleFunc("/v1/acl/tokens", func(w http.ResponseWriter, r *http.Request) { tokens := testdata.SampleACLTokens w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(tokens) }) mux.HandleFunc("/v1/acl/token/", func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { token := testdata.SampleACLTokens[0] w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(token) } else if r.Method == "POST" { token := testdata.SampleACLTokens[0] w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(token) } else if r.Method == "DELETE" { w.WriteHeader(http.StatusOK) } }) // Cluster endpoints mux.HandleFunc("/v1/operator/raft/configuration", func(w http.ResponseWriter, r *http.Request) { data := testdata.SampleClusterData["leader"] w.Header().Set("Content-Type", "application/json") w.Write(data) }) mux.HandleFunc("/v1/regions", func(w http.ResponseWriter, r *http.Request) { data := testdata.SampleClusterData["regions"] w.Header().Set("Content-Type", "application/json") w.Write(data) }) // Status endpoint for connection testing mux.HandleFunc("/v1/status/leader", func(w http.ResponseWriter, r *http.Request) { response := map[string]string{ "Leader": "127.0.0.1:4647", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) }) server := httptest.NewServer(mux) return &MockNomadServer{ server: server, } } func (m *MockNomadServer) URL() string { return m.server.URL } func (m *MockNomadServer) Close() { m.server.Close() } func TestNomadClientIntegration(t *testing.T) { // Start mock server mockServer := NewMockNomadServer() defer mockServer.Close() // Create client client, err := utils.NewNomadClient(mockServer.URL(), "") require.NoError(t, err) t.Run("ListJobs", func(t *testing.T) { jobs, err := client.ListJobs("default", "") require.NoError(t, err) assert.Len(t, jobs, 2) assert.Equal(t, "test-job-1", jobs[0].ID) assert.Equal(t, "test-job-2", jobs[1].ID) }) t.Run("GetJob", func(t *testing.T) { job, err := client.GetJob("test-job-1", "default") require.NoError(t, err) assert.Equal(t, "test-job-1", job.ID) assert.Equal(t, "service", job.Type) }) t.Run("RunJob", func(t *testing.T) { result, err := client.RunJob(testdata.SampleJobSpecs["simple"], false) require.NoError(t, err) assert.Contains(t, result, "EvalID") assert.Equal(t, "eval-123", result["EvalID"]) }) t.Run("StopJob", func(t *testing.T) { result, err := client.StopJob("test-job-1", "default", false) require.NoError(t, err) assert.Contains(t, result, "EvalID") assert.Equal(t, "eval-456", result["EvalID"]) }) t.Run("ListNodes", func(t *testing.T) { nodes, err := client.ListNodes("") require.NoError(t, err) assert.Len(t, nodes, 2) assert.Equal(t, "node-1", nodes[0].ID) assert.Equal(t, "node-2", nodes[1].ID) }) t.Run("GetNode", func(t *testing.T) { node, err := client.GetNode("node-1") require.NoError(t, err) assert.Equal(t, "node-1", node.ID) assert.Equal(t, "ready", node.Status) }) t.Run("DrainNode", func(t *testing.T) { result, err := client.DrainNode("node-1", true, 300) require.NoError(t, err) assert.Contains(t, result, "drain enabled") }) t.Run("EligibilityNode", func(t *testing.T) { node, err := client.EligibilityNode("node-1", "eligible") require.NoError(t, err) assert.Equal(t, "node-1", node.ID) }) t.Run("ListNamespaces", func(t *testing.T) { namespaces, err := client.ListNamespaces() require.NoError(t, err) assert.Len(t, namespaces, 2) assert.Equal(t, "default", namespaces[0].Name) assert.Equal(t, "production", namespaces[1].Name) }) t.Run("CreateNamespace", func(t *testing.T) { namespace := types.Namespace{ Name: "test-namespace", Description: "Test namespace", } err := client.CreateNamespace(namespace) require.NoError(t, err) }) t.Run("DeleteNamespace", func(t *testing.T) { err := client.DeleteNamespace("test-namespace") require.NoError(t, err) }) t.Run("ListAllocations", func(t *testing.T) { allocations, err := client.ListAllocations() require.NoError(t, err) assert.Len(t, allocations, 1) assert.Equal(t, "alloc-1", allocations[0].ID) }) t.Run("GetAllocation", func(t *testing.T) { allocation, err := client.GetAllocation("alloc-1") require.NoError(t, err) assert.Equal(t, "alloc-1", allocation.ID) assert.Equal(t, "test-allocation", allocation.Name) }) t.Run("GetAllocationLogs", func(t *testing.T) { logs, err := client.GetAllocationLogs("alloc-1", "nginx", "stdout", false, 0, 0) require.NoError(t, err) assert.Contains(t, logs, "Starting nginx") assert.Contains(t, logs, "Server started on port 80") }) t.Run("ListVariables", func(t *testing.T) { variables, err := client.ListVariables("default", "", "", 0, "") require.NoError(t, err) assert.Len(t, variables, 1) assert.Equal(t, "app/config", variables[0].Path) }) t.Run("GetVariable", func(t *testing.T) { variable, err := client.GetVariable("app/config", "default") require.NoError(t, err) assert.Equal(t, "app/config", variable.Path) assert.Contains(t, variable.Value, "database_url") }) t.Run("CreateVariable", func(t *testing.T) { variable := types.Variable{ Path: "test/config", Value: `{"Items":{"key":"value"}}`, } err := client.CreateVariable(variable, "default", 0, "") require.NoError(t, err) }) t.Run("DeleteVariable", func(t *testing.T) { err := client.DeleteVariable("test/config", "default", 0) require.NoError(t, err) }) t.Run("ListACLTokens", func(t *testing.T) { tokens, err := client.ListACLTokens() require.NoError(t, err) assert.Len(t, tokens, 1) assert.Equal(t, "token-1", tokens[0].AccessorID) }) t.Run("GetACLToken", func(t *testing.T) { token, err := client.GetACLToken("token-1") require.NoError(t, err) assert.Equal(t, "token-1", token.AccessorID) assert.Equal(t, "test-token", token.Name) }) t.Run("CreateACLToken", func(t *testing.T) { token := types.ACLToken{ Name: "test-token", Type: "client", Policies: []string{"read-only"}, } newToken, err := client.CreateACLToken(token) require.NoError(t, err) assert.Equal(t, "test-token", newToken.Name) }) t.Run("DeleteACLToken", func(t *testing.T) { err := client.DeleteACLToken("token-1") require.NoError(t, err) }) t.Run("GetClusterLeader", func(t *testing.T) { data, err := client.GetClusterLeader() require.NoError(t, err) assert.Contains(t, string(data), "server-1") }) t.Run("ListClusterPeers", func(t *testing.T) { data, err := client.ListClusterPeers() require.NoError(t, err) assert.Contains(t, string(data), "server-1") }) t.Run("ListRegions", func(t *testing.T) { data, err := client.ListRegions() require.NoError(t, err) assert.Contains(t, string(data), "global") }) } func TestNomadClientErrorHandling(t *testing.T) { // Test with invalid server URL _, err := utils.NewNomadClient("http://invalid-url:9999", "") require.Error(t, err) assert.Contains(t, err.Error(), "failed to connect to Nomad server") // Test with empty address _, err = utils.NewNomadClient("", "") require.Error(t, err) assert.Contains(t, err.Error(), "nomad address is required") }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kocierik/mcp-nomad'

If you have feedback or need assistance with the MCP directory API, please join our Discord server