Skip to content

Commit e0e362a

Browse files
authored
Merge pull request #71 from neo4j/mjfwebb/integration-tests
Add integration tests
2 parents 9a6e129 + 78bde80 commit e0e362a

File tree

11 files changed

+1027
-17
lines changed

11 files changed

+1027
-17
lines changed

go.mod

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,73 @@
11
module github.com/neo4j/mcp
22

3-
go 1.25.1
3+
go 1.25.3
44

55
require (
6-
github.com/mark3labs/mcp-go v0.41.1
6+
github.com/google/uuid v1.6.0
7+
github.com/mark3labs/mcp-go v0.42.0
78
github.com/neo4j/neo4j-go-driver/v5 v5.28.4
9+
github.com/testcontainers/testcontainers-go v0.39.0
810
go.uber.org/mock v0.6.0
911
)
1012

1113
require (
14+
dario.cat/mergo v1.0.2 // indirect
15+
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
16+
github.com/Microsoft/go-winio v0.6.2 // indirect
1217
github.com/bahlo/generic-list-go v0.2.0 // indirect
1318
github.com/buger/jsonparser v1.1.1 // indirect
14-
github.com/google/uuid v1.6.0 // indirect
19+
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
20+
github.com/containerd/errdefs v1.0.0 // indirect
21+
github.com/containerd/errdefs/pkg v0.3.0 // indirect
22+
github.com/containerd/log v0.1.0 // indirect
23+
github.com/containerd/platforms v0.2.1 // indirect
24+
github.com/cpuguy83/dockercfg v0.3.2 // indirect
25+
github.com/davecgh/go-spew v1.1.1 // indirect
26+
github.com/distribution/reference v0.6.0 // indirect
27+
github.com/docker/docker v28.5.1+incompatible // indirect
28+
github.com/docker/go-connections v0.6.0 // indirect
29+
github.com/docker/go-units v0.5.0 // indirect
30+
github.com/ebitengine/purego v0.9.0 // indirect
31+
github.com/felixge/httpsnoop v1.0.4 // indirect
32+
github.com/go-logr/logr v1.4.3 // indirect
33+
github.com/go-logr/stdr v1.2.2 // indirect
34+
github.com/go-ole/go-ole v1.3.0 // indirect
1535
github.com/invopop/jsonschema v0.13.0 // indirect
36+
github.com/klauspost/compress v1.18.1 // indirect
37+
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
38+
github.com/magiconair/properties v1.8.10 // indirect
1639
github.com/mailru/easyjson v0.9.1 // indirect
40+
github.com/moby/docker-image-spec v1.3.1 // indirect
41+
github.com/moby/go-archive v0.1.0 // indirect
42+
github.com/moby/patternmatcher v0.6.0 // indirect
43+
github.com/moby/sys/sequential v0.6.0 // indirect
44+
github.com/moby/sys/user v0.4.0 // indirect
45+
github.com/moby/sys/userns v0.1.0 // indirect
46+
github.com/moby/term v0.5.2 // indirect
47+
github.com/morikuni/aec v1.0.0 // indirect
48+
github.com/opencontainers/go-digest v1.0.0 // indirect
49+
github.com/opencontainers/image-spec v1.1.1 // indirect
50+
github.com/pkg/errors v0.9.1 // indirect
51+
github.com/pmezard/go-difflib v1.0.0 // indirect
52+
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
53+
github.com/shirou/gopsutil/v4 v4.25.9 // indirect
54+
github.com/sirupsen/logrus v1.9.3 // indirect
1755
github.com/spf13/cast v1.10.0 // indirect
56+
github.com/stretchr/testify v1.11.1 // indirect
57+
github.com/tklauser/go-sysconf v0.3.15 // indirect
58+
github.com/tklauser/numcpus v0.10.0 // indirect
1859
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
1960
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
61+
github.com/yusufpapurcu/wmi v1.2.4 // indirect
62+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
63+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
64+
go.opentelemetry.io/otel v1.38.0 // indirect
65+
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
66+
go.opentelemetry.io/otel/metric v1.38.0 // indirect
67+
go.opentelemetry.io/otel/trace v1.38.0 // indirect
68+
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
69+
golang.org/x/crypto v0.43.0 // indirect
70+
golang.org/x/sys v0.37.0 // indirect
71+
google.golang.org/protobuf v1.36.10 // indirect
2072
gopkg.in/yaml.v3 v3.0.1 // indirect
2173
)

go.sum

Lines changed: 151 additions & 9 deletions
Large diffs are not rendered by default.

internal/config/config.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ func (c *Config) Validate() error {
4040
// LoadConfig loads configuration from environment variables with defaults
4141
func LoadConfig() (*Config, error) {
4242
cfg := &Config{
43-
URI: getEnvWithDefault("NEO4J_URI", "bolt://localhost:7687"),
44-
Username: getEnvWithDefault("NEO4J_USERNAME", "neo4j"),
45-
Password: getEnvWithDefault("NEO4J_PASSWORD", "password"),
46-
Database: getEnvWithDefault("NEO4J_DATABASE", "neo4j"),
43+
URI: GetEnvWithDefault("NEO4J_URI", "bolt://localhost:7687"),
44+
Username: GetEnvWithDefault("NEO4J_USERNAME", "neo4j"),
45+
Password: GetEnvWithDefault("NEO4J_PASSWORD", "password"),
46+
Database: GetEnvWithDefault("NEO4J_DATABASE", "neo4j"),
4747
}
4848

4949
if err := cfg.Validate(); err != nil {
@@ -53,7 +53,7 @@ func LoadConfig() (*Config, error) {
5353
return cfg, nil
5454
}
5555

56-
func getEnvWithDefault(key, defaultValue string) string {
56+
func GetEnvWithDefault(key, defaultValue string) string {
5757
if value := os.Getenv(key); value != "" {
5858
return value
5959
}

test/integration/README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Integration Tests
2+
3+
Integration tests for the Neo4j MCP server using a shared Neo4j container (includes APOC + GDS).
4+
5+
## Quick Start
6+
7+
```go
8+
func TestMCPIntegration_MyFeature(t *testing.T) {
9+
t.Parallel()
10+
tc := helpers.NewTestContext(t)
11+
12+
// Seed test data (automatically isolated with unique labels and cleaned up)
13+
personLabel, err := tc.SeedNode("Person", map[string]any{"name": "Alice"})
14+
if err != nil {
15+
t.Fatalf("failed to seed data: %v", err)
16+
}
17+
18+
// Call tool
19+
handler := cypher.ReadCypherHandler(tc.Deps)
20+
res := tc.CallTool(handler, map[string]any{
21+
"query": "MATCH (p:" + personLabel + " {name: $name}) RETURN p",
22+
"params": map[string]any{"name": "Alice"},
23+
})
24+
25+
// Parse and assert
26+
var records []map[string]any
27+
tc.ParseJSONResponse(res, &records)
28+
29+
person := records[0]["p"].(map[string]any)
30+
helpers.AssertNodeProperties(t, person, map[string]any{"name": "Alice"})
31+
helpers.AssertNodeHasLabel(t, person, personLabel)
32+
}
33+
```
34+
35+
## Key Helpers
36+
37+
**TestContext:**
38+
39+
- `helpers.NewTestContext(t)` - Auto-isolation + cleanup
40+
- `SeedNode(label, props)` - Create test data with unique label, returns `(UniqueLabel, error)`
41+
- `GetUniqueLabel(label)` - Get a unique label for creating nodes manually
42+
- `CallTool(handler, args)` - Invoke MCP tool
43+
- `ParseJSONResponse(res, &v)` - Parse response
44+
- `VerifyNodeInDB(label, props)` - Check DB state
45+
46+
**Assertions:**
47+
48+
- `helpers.AssertNodeProperties(t, node, props)`
49+
- `helpers.AssertNodeHasLabel(t, node, label)`
50+
- `helpers.AssertSchemaHasNodeType(t, schema, label, props)`
51+
52+
## Running Tests
53+
54+
```bash
55+
go test -tags=integration ./test/integration/... -v # All tests
56+
go test -tags=integration ./test/integration/... -run MyFeature # Specific test
57+
go test -tags=integration ./test/integration/... -race # With race detection
58+
```
59+
60+
## Configuration
61+
62+
The integration tests use environment variables to configure the Neo4j test container. All variables have sensible defaults:
63+
64+
| Environment Variable | Default | Description |
65+
| -------------------- | ------------------------------- | ------------------------- |
66+
| `NEO4J_IMAGE` | `neo4j:5.24.2-community` | Neo4j Docker image to use |
67+
| `NEO4J_USERNAME` | `neo4j` | Database username |
68+
| `NEO4J_PASSWORD` | `password` | Database password |
69+
| `NEO4JLABS_PLUGINS` | `["apoc","graph-data-science"]` | Plugins to install |
70+
71+
**Example with custom configuration:**
72+
73+
```bash
74+
NEO4J_IMAGE=neo4j:5.25.0-enterprise \
75+
NEO4J_USERNAME=admin \
76+
NEO4J_PASSWORD=secret \
77+
go test -tags=integration ./test/integration/... -v
78+
```
79+
80+
## Important
81+
82+
- Always use `t.Parallel()` for parallel execution
83+
- Always use the `UniqueLabel` returned by `SeedNode()` or `GetUniqueLabel()` in your queries for isolation
84+
- Test data is automatically tagged with unique labels and cleaned up after each test
85+
- Import the helpers package: `"github.com/neo4j/mcp/test/integration/helpers"`
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//go:build integration
2+
3+
package integration
4+
5+
import (
6+
"testing"
7+
8+
"github.com/neo4j/mcp/internal/tools/cypher"
9+
"github.com/neo4j/mcp/test/integration/helpers"
10+
)
11+
12+
func TestMCPIntegration_GetSchema(t *testing.T) {
13+
t.Parallel()
14+
15+
tc := helpers.NewTestContext(t)
16+
17+
// Use TestID as identifier to create unique labels
18+
personLabel, err := tc.SeedNode("Person", map[string]any{"name": "Alice", "age": 30})
19+
if err != nil {
20+
t.Fatalf("failed to seed Person node: %v", err)
21+
}
22+
companyLabel, err := tc.SeedNode("Company", map[string]any{"name": "Neo4j", "founded": 2007})
23+
if err != nil {
24+
t.Fatalf("failed to seed Company node: %v", err)
25+
}
26+
27+
getSchema := cypher.GetSchemaHandler(tc.Deps)
28+
res := tc.CallTool(getSchema, nil)
29+
30+
var schemaArray []map[string]any
31+
tc.ParseJSONResponse(res, &schemaArray)
32+
33+
if len(schemaArray) == 0 {
34+
t.Fatal("expected schema to contain at least one entry")
35+
}
36+
37+
schemaMap := make(map[string]map[string]any)
38+
for _, entry := range schemaArray {
39+
key, ok := entry["key"].(string)
40+
if !ok {
41+
continue
42+
}
43+
value, ok := entry["value"].(map[string]any)
44+
if !ok {
45+
continue
46+
}
47+
schemaMap[key] = value
48+
}
49+
50+
// Check for the unique labels created
51+
helpers.AssertSchemaHasNodeType(t, schemaMap, personLabel, []string{"name", "age"})
52+
helpers.AssertSchemaHasNodeType(t, schemaMap, companyLabel, []string{"name", "founded"})
53+
}

0 commit comments

Comments
 (0)