Skip to content

Commit c94cd66

Browse files
manusaclaude
andcommitted
test: add snapshot testing for tool definitions
Add snapshot testing infrastructure following kubernetes-mcp-server patterns: - internal/test/helpers.go with Must and ReadFile utilities - assertJsonSnapshot method on McpServerSuite - Tool definitions snapshot in pkg/mcp/testdata/ - make test-update-snapshots target for updating snapshots Set UPDATE_SNAPSHOTS=1 to regenerate snapshot files. Part of issue #68 - Testing Infrastructure Modernization. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b7417b6 commit c94cd66

4 files changed

Lines changed: 345 additions & 0 deletions

File tree

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ python-publish: ## Publish the python packages
9090
test: ## Run the tests
9191
go test -count=1 -v ./...
9292

93+
.PHONY: test-update-snapshots
94+
test-update-snapshots: ## Update test snapshots
95+
UPDATE_SNAPSHOTS=1 go test -count=1 -v ./...
96+
9397
.PHONY: format
9498
format: ## Format the code
9599
go fmt ./...

internal/test/helpers.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package test
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
)
8+
9+
// Must panics if err is not nil, otherwise returns v.
10+
func Must[T any](v T, err error) T {
11+
if err != nil {
12+
panic(err)
13+
}
14+
return v
15+
}
16+
17+
// ReadFile reads a file relative to the caller's source file location.
18+
func ReadFile(path ...string) string {
19+
_, file, _, _ := runtime.Caller(1)
20+
filePath := filepath.Join(append([]string{filepath.Dir(file)}, path...)...)
21+
fileBytes := Must(os.ReadFile(filePath))
22+
return string(fileBytes)
23+
}

pkg/mcp/mcp_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
11
package mcp_test
22

33
import (
4+
"encoding/json"
5+
"os"
6+
"path/filepath"
7+
"runtime"
8+
"sort"
49
"testing"
510

611
"github.com/stretchr/testify/suite"
712

813
"github.com/manusa/podman-mcp-server/internal/test"
914
)
1015

16+
const updateSnapshotsEnvVar = "UPDATE_SNAPSHOTS"
17+
1118
type McpServerSuite struct {
1219
test.McpSuite
20+
updateSnapshots bool
1321
}
1422

1523
func TestMcpServer(t *testing.T) {
1624
suite.Run(t, new(McpServerSuite))
1725
}
1826

27+
func (s *McpServerSuite) SetupTest() {
28+
s.McpSuite.SetupTest()
29+
s.updateSnapshots = os.Getenv(updateSnapshotsEnvVar) != ""
30+
}
31+
1932
func (s *McpServerSuite) TestListTools() {
2033
expectedTools := []string{
2134
"container_inspect",
@@ -47,3 +60,38 @@ func (s *McpServerSuite) TestListTools() {
4760
})
4861
}
4962
}
63+
64+
func (s *McpServerSuite) TestToolDefinitionsSnapshot() {
65+
tools, err := s.ListTools()
66+
s.Require().NoError(err)
67+
68+
// Sort tools by name for consistent snapshots
69+
sort.Slice(tools.Tools, func(i, j int) bool {
70+
return tools.Tools[i].Name < tools.Tools[j].Name
71+
})
72+
73+
s.assertJsonSnapshot("tool_definitions.json", tools.Tools)
74+
}
75+
76+
// assertJsonSnapshot compares actual data against a JSON snapshot file.
77+
// Set UPDATE_SNAPSHOTS=1 environment variable to regenerate snapshot files.
78+
func (s *McpServerSuite) assertJsonSnapshot(snapshotFile string, actual any) {
79+
_, file, _, _ := runtime.Caller(1)
80+
snapshotPath := filepath.Join(filepath.Dir(file), "testdata", snapshotFile)
81+
actualJson, err := json.MarshalIndent(actual, "", " ")
82+
s.Require().NoErrorf(err, "failed to marshal actual data: %v", err)
83+
if s.updateSnapshots {
84+
err := os.WriteFile(snapshotPath, append(actualJson, '\n'), 0644)
85+
s.Require().NoErrorf(err, "failed to write snapshot file %s: %v", snapshotFile, err)
86+
s.T().Logf("Updated snapshot: %s", snapshotFile)
87+
return
88+
}
89+
expectedJson := test.ReadFile("testdata", snapshotFile)
90+
s.JSONEq(
91+
expectedJson,
92+
string(actualJson),
93+
"snapshot %s does not match - to update snapshots re-run the tests with %s=1",
94+
snapshotFile,
95+
updateSnapshotsEnvVar,
96+
)
97+
}
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
[
2+
{
3+
"annotations": {
4+
"readOnlyHint": false,
5+
"destructiveHint": true,
6+
"idempotentHint": false,
7+
"openWorldHint": true
8+
},
9+
"description": "Displays the low-level information and configuration of a Docker or Podman container with the specified container ID or name",
10+
"inputSchema": {
11+
"type": "object",
12+
"properties": {
13+
"name": {
14+
"description": "Docker or Podman container ID or name to displays the information",
15+
"type": "string"
16+
}
17+
},
18+
"required": [
19+
"name"
20+
]
21+
},
22+
"name": "container_inspect"
23+
},
24+
{
25+
"annotations": {
26+
"readOnlyHint": false,
27+
"destructiveHint": true,
28+
"idempotentHint": false,
29+
"openWorldHint": true
30+
},
31+
"description": "Prints out information about the running Docker or Podman containers",
32+
"inputSchema": {
33+
"type": "object"
34+
},
35+
"name": "container_list"
36+
},
37+
{
38+
"annotations": {
39+
"readOnlyHint": false,
40+
"destructiveHint": true,
41+
"idempotentHint": false,
42+
"openWorldHint": true
43+
},
44+
"description": "Displays the logs of a Docker or Podman container with the specified container ID or name",
45+
"inputSchema": {
46+
"type": "object",
47+
"properties": {
48+
"name": {
49+
"description": "Docker or Podman container ID or name to displays the logs",
50+
"type": "string"
51+
}
52+
},
53+
"required": [
54+
"name"
55+
]
56+
},
57+
"name": "container_logs"
58+
},
59+
{
60+
"annotations": {
61+
"readOnlyHint": false,
62+
"destructiveHint": true,
63+
"idempotentHint": false,
64+
"openWorldHint": true
65+
},
66+
"description": "Removes a Docker or Podman container with the specified container ID or name (rm)",
67+
"inputSchema": {
68+
"type": "object",
69+
"properties": {
70+
"name": {
71+
"description": "Docker or Podman container ID or name to remove",
72+
"type": "string"
73+
}
74+
},
75+
"required": [
76+
"name"
77+
]
78+
},
79+
"name": "container_remove"
80+
},
81+
{
82+
"annotations": {
83+
"readOnlyHint": false,
84+
"destructiveHint": true,
85+
"idempotentHint": false,
86+
"openWorldHint": true
87+
},
88+
"description": "Runs a Docker or Podman container with the specified image name",
89+
"inputSchema": {
90+
"type": "object",
91+
"properties": {
92+
"environment": {
93+
"description": "Environment variables to set in the container. Format: \u003ckey\u003e=\u003cvalue\u003e. Example: FOO=bar. (Optional, add only to set environment variables)",
94+
"items": {
95+
"type": "string"
96+
},
97+
"type": "array"
98+
},
99+
"imageName": {
100+
"description": "Docker or Podman container image name to pull",
101+
"type": "string"
102+
},
103+
"ports": {
104+
"description": "Port mappings to expose on the host. Format: \u003chostPort\u003e:\u003ccontainerPort\u003e. Example: 8080:80. (Optional, add only to expose ports)",
105+
"items": {
106+
"type": "string"
107+
},
108+
"type": "array"
109+
}
110+
},
111+
"required": [
112+
"imageName"
113+
]
114+
},
115+
"name": "container_run"
116+
},
117+
{
118+
"annotations": {
119+
"readOnlyHint": false,
120+
"destructiveHint": true,
121+
"idempotentHint": false,
122+
"openWorldHint": true
123+
},
124+
"description": "Stops a Docker or Podman running container with the specified container ID or name",
125+
"inputSchema": {
126+
"type": "object",
127+
"properties": {
128+
"name": {
129+
"description": "Docker or Podman container ID or name to stop",
130+
"type": "string"
131+
}
132+
},
133+
"required": [
134+
"name"
135+
]
136+
},
137+
"name": "container_stop"
138+
},
139+
{
140+
"annotations": {
141+
"readOnlyHint": false,
142+
"destructiveHint": true,
143+
"idempotentHint": false,
144+
"openWorldHint": true
145+
},
146+
"description": "Build a Docker or Podman image from a Dockerfile, Podmanfile, or Containerfile",
147+
"inputSchema": {
148+
"type": "object",
149+
"properties": {
150+
"containerFile": {
151+
"description": "The absolute path to the Dockerfile, Podmanfile, or Containerfile to build the image from",
152+
"type": "string"
153+
},
154+
"imageName": {
155+
"description": "Specifies the name which is assigned to the resulting image if the build process completes successfully (--tag, -t)",
156+
"type": "string"
157+
}
158+
},
159+
"required": [
160+
"containerFile"
161+
]
162+
},
163+
"name": "image_build"
164+
},
165+
{
166+
"annotations": {
167+
"readOnlyHint": false,
168+
"destructiveHint": true,
169+
"idempotentHint": false,
170+
"openWorldHint": true
171+
},
172+
"description": "List the Docker or Podman images on the local machine",
173+
"inputSchema": {
174+
"type": "object"
175+
},
176+
"name": "image_list"
177+
},
178+
{
179+
"annotations": {
180+
"readOnlyHint": false,
181+
"destructiveHint": true,
182+
"idempotentHint": false,
183+
"openWorldHint": true
184+
},
185+
"description": "Copies (pulls) a Docker or Podman container image from a registry onto the local machine storage",
186+
"inputSchema": {
187+
"type": "object",
188+
"properties": {
189+
"imageName": {
190+
"description": "Docker or Podman container image name to pull",
191+
"type": "string"
192+
}
193+
},
194+
"required": [
195+
"imageName"
196+
]
197+
},
198+
"name": "image_pull"
199+
},
200+
{
201+
"annotations": {
202+
"readOnlyHint": false,
203+
"destructiveHint": true,
204+
"idempotentHint": false,
205+
"openWorldHint": true
206+
},
207+
"description": "Pushes a Docker or Podman container image, manifest list or image index from local machine storage to a registry",
208+
"inputSchema": {
209+
"type": "object",
210+
"properties": {
211+
"imageName": {
212+
"description": "Docker or Podman container image name to push",
213+
"type": "string"
214+
}
215+
},
216+
"required": [
217+
"imageName"
218+
]
219+
},
220+
"name": "image_push"
221+
},
222+
{
223+
"annotations": {
224+
"readOnlyHint": false,
225+
"destructiveHint": true,
226+
"idempotentHint": false,
227+
"openWorldHint": true
228+
},
229+
"description": "Removes a Docker or Podman image from the local machine storage",
230+
"inputSchema": {
231+
"type": "object",
232+
"properties": {
233+
"imageName": {
234+
"description": "Docker or Podman container image name to remove",
235+
"type": "string"
236+
}
237+
},
238+
"required": [
239+
"imageName"
240+
]
241+
},
242+
"name": "image_remove"
243+
},
244+
{
245+
"annotations": {
246+
"readOnlyHint": false,
247+
"destructiveHint": true,
248+
"idempotentHint": false,
249+
"openWorldHint": true
250+
},
251+
"description": "List all the available Docker or Podman networks",
252+
"inputSchema": {
253+
"type": "object"
254+
},
255+
"name": "network_list"
256+
},
257+
{
258+
"annotations": {
259+
"readOnlyHint": false,
260+
"destructiveHint": true,
261+
"idempotentHint": false,
262+
"openWorldHint": true
263+
},
264+
"description": "List all the available Docker or Podman volumes",
265+
"inputSchema": {
266+
"type": "object"
267+
},
268+
"name": "volume_list"
269+
}
270+
]

0 commit comments

Comments
 (0)