Skip to content

Commit 78bde80

Browse files
committed
refactor: update integration tests to use unique labels for nodes
1 parent 556bfa6 commit 78bde80

File tree

6 files changed

+122
-62
lines changed

6 files changed

+122
-62
lines changed

test/integration/README.md

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,47 @@ Integration tests for the Neo4j MCP server using a shared Neo4j container (inclu
77
```go
88
func TestMCPIntegration_MyFeature(t *testing.T) {
99
t.Parallel()
10-
tc := NewTestContext(t)
10+
tc := helpers.NewTestContext(t)
1111

12-
// Seed test data (automatically isolated and cleaned up)
13-
tc.SeedNode("Person", map[string]any{"name": "Alice"})
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+
}
1417

1518
// Call tool
16-
handler := tools.ReadCypherHandler(tc.Deps)
19+
handler := cypher.ReadCypherHandler(tc.Deps)
1720
res := tc.CallTool(handler, map[string]any{
18-
"query": "MATCH (p:Person {test_id: $testID}) RETURN p",
19-
"params": map[string]any{"testID": tc.TestID},
21+
"query": "MATCH (p:" + personLabel + " {name: $name}) RETURN p",
22+
"params": map[string]any{"name": "Alice"},
2023
})
2124

2225
// Parse and assert
2326
var records []map[string]any
2427
tc.ParseJSONResponse(res, &records)
2528

2629
person := records[0]["p"].(map[string]any)
27-
AssertNodeProperties(t, person, map[string]any{"name": "Alice"})
30+
helpers.AssertNodeProperties(t, person, map[string]any{"name": "Alice"})
31+
helpers.AssertNodeHasLabel(t, person, personLabel)
2832
}
2933
```
3034

3135
## Key Helpers
3236

3337
**TestContext:**
3438

35-
- `NewTestContext(t)` - Auto-isolation + cleanup
36-
- `SeedNode(label, props)` - Create test data
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
3742
- `CallTool(handler, args)` - Invoke MCP tool
3843
- `ParseJSONResponse(res, &v)` - Parse response
3944
- `VerifyNodeInDB(label, props)` - Check DB state
4045

4146
**Assertions:**
4247

43-
- `AssertNodeProperties(t, node, props)`
44-
- `AssertNodeHasLabel(t, node, label)`
45-
- `AssertSchemaHasNodeType(t, schema, label, props)`
48+
- `helpers.AssertNodeProperties(t, node, props)`
49+
- `helpers.AssertNodeHasLabel(t, node, label)`
50+
- `helpers.AssertSchemaHasNodeType(t, schema, label, props)`
4651

4752
## Running Tests
4853

@@ -75,5 +80,6 @@ go test -tags=integration ./test/integration/... -v
7580
## Important
7681

7782
- Always use `t.Parallel()` for parallel execution
78-
- Always include `test_id` in queries for isolation
79-
- Test data is automatically tagged with unique IDs and cleaned up
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"`

test/integration/get_schema_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ func TestMCPIntegration_GetSchema(t *testing.T) {
1414

1515
tc := helpers.NewTestContext(t)
1616

17-
if err := tc.SeedNode("Person", map[string]any{"name": "Alice", "age": 30}); err != nil {
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 {
1820
t.Fatalf("failed to seed Person node: %v", err)
1921
}
20-
if err := tc.SeedNode("Company", map[string]any{"name": "Neo4j", "founded": 2007}); err != nil {
22+
companyLabel, err := tc.SeedNode("Company", map[string]any{"name": "Neo4j", "founded": 2007})
23+
if err != nil {
2124
t.Fatalf("failed to seed Company node: %v", err)
2225
}
2326

@@ -44,6 +47,7 @@ func TestMCPIntegration_GetSchema(t *testing.T) {
4447
schemaMap[key] = value
4548
}
4649

47-
helpers.AssertSchemaHasNodeType(t, schemaMap, "Person", []string{"name", "age"})
48-
helpers.AssertSchemaHasNodeType(t, schemaMap, "Company", []string{"name", "founded"})
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"})
4953
}

test/integration/helpers/helpers.go

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,23 @@ import (
2323
"github.com/testcontainers/testcontainers-go/wait"
2424
)
2525

26+
type UniqueLabel string
27+
28+
// String returns the string representation of the UniqueLabel.
29+
// This implements the fmt.Stringer interface, making it work seamlessly with fmt functions.
30+
func (ul UniqueLabel) String() string {
31+
return string(ul)
32+
}
33+
2634
// TestContext holds common test dependencies
2735
type TestContext struct {
28-
Ctx context.Context
29-
T *testing.T
30-
TestID string
31-
Service database.Service
32-
Deps *tools.ToolDependencies
36+
Ctx context.Context
37+
T *testing.T
38+
TestID string
39+
Service database.Service
40+
Deps *tools.ToolDependencies
41+
createdLabels map[string]bool
42+
labelMutex sync.Mutex
3343
}
3444

3545
var (
@@ -92,9 +102,10 @@ func NewTestContext(t *testing.T) *TestContext {
92102
testID := makeTestID()
93103

94104
tc := &TestContext{
95-
Ctx: ctx,
96-
T: t,
97-
TestID: testID,
105+
Ctx: ctx,
106+
T: t,
107+
TestID: testID,
108+
createdLabels: make(map[string]bool),
98109
}
99110

100111
t.Cleanup(func() {
@@ -114,7 +125,7 @@ func NewTestContext(t *testing.T) *TestContext {
114125
return tc
115126
}
116127

117-
// Cleanup removes all test data tagged with this test ID
128+
// Cleanup removes all test data by deleting nodes with labels created during the test
118129
func (tc *TestContext) Cleanup() {
119130
if tc.Service == nil {
120131
return // Service wasn't initialized, nothing to clean up
@@ -123,27 +134,46 @@ func (tc *TestContext) Cleanup() {
123134
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
124135
defer cancel()
125136

126-
if _, err := tc.Service.ExecuteWriteQuery(
127-
ctx,
128-
"MATCH (n) WHERE n.test_id = $testID DETACH DELETE n",
129-
map[string]any{"testID": tc.TestID},
130-
cfg.Database,
131-
); err != nil {
132-
log.Printf("Warning: cleanup failed for test_id=%s: %v", tc.TestID, err)
137+
tc.labelMutex.Lock()
138+
labels := make([]string, 0, len(tc.createdLabels))
139+
for label := range tc.createdLabels {
140+
labels = append(labels, label)
141+
}
142+
tc.labelMutex.Unlock()
143+
144+
// Delete nodes for each unique label
145+
for _, label := range labels {
146+
query := fmt.Sprintf("MATCH (n:%s) DETACH DELETE n", label)
147+
if _, err := tc.Service.ExecuteWriteQuery(
148+
ctx,
149+
query,
150+
map[string]any{},
151+
cfg.Database,
152+
); err != nil {
153+
log.Printf("Warning: cleanup failed for label=%s: %v", label, err)
154+
}
133155
}
134156
}
135157

136-
// SeedNode creates a test node and returns it
137-
func (tc *TestContext) SeedNode(label string, props map[string]any) error {
158+
// SeedNode creates a test node with a unique label and returns it.
159+
func (tc *TestContext) SeedNode(label string, props map[string]any) (UniqueLabel, error) {
138160
tc.T.Helper()
139161

140-
// Always add test_id to props
141-
props["test_id"] = tc.TestID
162+
if tc.TestID == "" {
163+
panic("SeedNode: TestID is not set in TestContext. Did you forget to use NewTestContext?")
164+
}
165+
166+
uniqueLabel := UniqueLabel(fmt.Sprintf("%s_%s", label, tc.TestID))
167+
168+
// Track this label for cleanup
169+
tc.labelMutex.Lock()
170+
tc.createdLabels[string(uniqueLabel)] = true
171+
tc.labelMutex.Unlock()
142172

143173
session := (driver).NewSession(tc.Ctx, neo4j.SessionConfig{})
144174
defer session.Close(tc.Ctx)
145175

146-
query := fmt.Sprintf("CREATE (n:%s $props) RETURN n", label)
176+
query := fmt.Sprintf("CREATE (n:%s $props) RETURN n", uniqueLabel)
147177
_, err := session.ExecuteWrite(tc.Ctx, func(tx neo4j.ManagedTransaction) (any, error) {
148178
result, err := tx.Run(tc.Ctx, query, map[string]any{"props": props})
149179
if err != nil {
@@ -153,7 +183,23 @@ func (tc *TestContext) SeedNode(label string, props map[string]any) error {
153183
return nil, err
154184
})
155185

156-
return err
186+
return uniqueLabel, err
187+
}
188+
189+
// GetUniqueLabel returns a unique label for the given base label and identifier.
190+
func (tc *TestContext) GetUniqueLabel(label string) UniqueLabel {
191+
if tc.TestID == "" {
192+
panic("GetUniqueLabel: TestID is not set in TestContext. Did you forget to use NewTestContext?")
193+
}
194+
195+
uniqueLabel := UniqueLabel(fmt.Sprintf("%s_%s", label, tc.TestID))
196+
197+
// Track this label for cleanup
198+
tc.labelMutex.Lock()
199+
tc.createdLabels[string(uniqueLabel)] = true
200+
tc.labelMutex.Unlock()
201+
202+
return uniqueLabel
157203
}
158204

159205
// CallTool invokes an MCP tool and returns the response
@@ -198,13 +244,11 @@ func (tc *TestContext) ParseJSONResponse(res *mcp.CallToolResult, v any) {
198244
}
199245
}
200246

201-
// VerifyNodeInDB verifies that a node exists in the database with the given properties
202-
func (tc *TestContext) VerifyNodeInDB(label string, props map[string]any) *neo4j.Record {
247+
// VerifyNodeInDB verifies that a node exists in the database with the given properties.
248+
// The label parameter should be the unique label (e.g., "Person_test_abc123").
249+
func (tc *TestContext) VerifyNodeInDB(label UniqueLabel, props map[string]any) *neo4j.Record {
203250
tc.T.Helper()
204251

205-
// Build WHERE clause for each property
206-
props["test_id"] = tc.TestID
207-
208252
session := (driver).NewSession(tc.Ctx, neo4j.SessionConfig{})
209253
defer session.Close(tc.Ctx)
210254

@@ -262,7 +306,7 @@ func AssertNodeProperties(t *testing.T, node map[string]any, expectedProps map[s
262306
}
263307

264308
// AssertNodeHasLabel checks if a node has a specific label
265-
func AssertNodeHasLabel(t *testing.T, node map[string]any, expectedLabel string) {
309+
func AssertNodeHasLabel(t *testing.T, node map[string]any, expectedLabel UniqueLabel) {
266310
t.Helper()
267311

268312
labels, ok := node["Labels"].([]any)
@@ -271,7 +315,7 @@ func AssertNodeHasLabel(t *testing.T, node map[string]any, expectedLabel string)
271315
}
272316

273317
for _, label := range labels {
274-
if labelStr, ok := label.(string); ok && labelStr == expectedLabel {
318+
if labelStr, ok := label.(string); ok && labelStr == string(expectedLabel) {
275319
return
276320
}
277321
}
@@ -280,10 +324,10 @@ func AssertNodeHasLabel(t *testing.T, node map[string]any, expectedLabel string)
280324
}
281325

282326
// AssertSchemaHasNodeType checks if the schema contains a node type with expected properties
283-
func AssertSchemaHasNodeType(t *testing.T, schemaMap map[string]map[string]any, label string, expectedProps []string) {
327+
func AssertSchemaHasNodeType(t *testing.T, schemaMap map[string]map[string]any, label UniqueLabel, expectedProps []string) {
284328
t.Helper()
285329

286-
schema, ok := schemaMap[label]
330+
schema, ok := schemaMap[string(label)]
287331
if !ok {
288332
t.Errorf("expected schema to contain '%s' label", label)
289333
return
@@ -308,7 +352,8 @@ func AssertSchemaHasNodeType(t *testing.T, schemaMap map[string]map[string]any,
308352

309353
// makeTestID returns a unique test id suitable for tagging resources created by tests.
310354
func makeTestID() string {
311-
return fmt.Sprintf("test-%s", uuid.NewString())
355+
id := fmt.Sprintf("test-%s", uuid.NewString())
356+
return strings.ReplaceAll(id, "-", "_")
312357
}
313358

314359
// waitForConnectivity waits for Neo4j connectivity with exponential backoff.

test/integration/read_cypher_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@ func TestMCPIntegration_ReadCypher(t *testing.T) {
1414

1515
tc := helpers.NewTestContext(t)
1616

17-
if err := tc.SeedNode("Person", map[string]any{"name": "Alice"}); err != nil {
17+
personLabel, err := tc.SeedNode("Person", map[string]any{"name": "Alice"})
18+
if err != nil {
1819
t.Fatalf("failed to seed data: %v", err)
1920
}
2021

2122
read := cypher.ReadCypherHandler(tc.Deps)
2223
res := tc.CallTool(read, map[string]any{
23-
"query": "MATCH (p:Person {name: $name, test_id: $testID}) RETURN p",
24-
"params": map[string]any{"name": "Alice", "testID": tc.TestID},
24+
"query": "MATCH (p:" + personLabel + " {name: $name}) RETURN p",
25+
"params": map[string]any{"name": "Alice"},
2526
})
2627

2728
var records []map[string]any
@@ -37,5 +38,5 @@ func TestMCPIntegration_ReadCypher(t *testing.T) {
3738
records[0]["p"])
3839
}
3940
helpers.AssertNodeProperties(t, pNode, map[string]any{"name": "Alice"})
40-
helpers.AssertNodeHasLabel(t, pNode, "Person")
41+
helpers.AssertNodeHasLabel(t, pNode, personLabel)
4142
}

test/integration/workflows_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,18 @@ func TestMCPIntegration_WriteThenRead(t *testing.T) {
1414

1515
tc := helpers.NewTestContext(t)
1616

17+
companyLabel := tc.GetUniqueLabel("Company")
18+
1719
write := cypher.WriteCypherHandler(tc.Deps)
1820
tc.CallTool(write, map[string]any{
19-
"query": "CREATE (c:Company {name: $name, industry: $industry, test_id: $testID}) RETURN c",
20-
"params": map[string]any{"name": "Neo4j", "industry": "Database", "testID": tc.TestID},
21+
"query": "CREATE (c:" + companyLabel + " {name: $name, industry: $industry}) RETURN c",
22+
"params": map[string]any{"name": "Neo4j", "industry": "Database"},
2123
})
2224

2325
read := cypher.ReadCypherHandler(tc.Deps)
2426
res := tc.CallTool(read, map[string]any{
25-
"query": "MATCH (c:Company {test_id: $testID}) RETURN c",
26-
"params": map[string]any{"testID": tc.TestID},
27+
"query": "MATCH (c:" + companyLabel + ") RETURN c",
28+
"params": map[string]any{},
2729
})
2830

2931
var records []map[string]any
@@ -38,5 +40,5 @@ func TestMCPIntegration_WriteThenRead(t *testing.T) {
3840
"name": "Neo4j",
3941
"industry": "Database",
4042
})
41-
helpers.AssertNodeHasLabel(t, company, "Company")
43+
helpers.AssertNodeHasLabel(t, company, companyLabel)
4244
}

test/integration/write_cypher_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ func TestMCPIntegration_WriteCypher(t *testing.T) {
1414

1515
tc := helpers.NewTestContext(t)
1616

17+
personLabel := tc.GetUniqueLabel("Person")
18+
1719
write := cypher.WriteCypherHandler(tc.Deps)
1820
tc.CallTool(write, map[string]any{
19-
"query": "CREATE (p:Person {name: $name, test_id: $testID}) RETURN p",
20-
"params": map[string]any{"name": "Alice", "testID": tc.TestID},
21+
"query": "CREATE (p:" + personLabel + " {name: $name}) RETURN p",
22+
"params": map[string]any{"name": "Alice"},
2123
})
2224

23-
tc.VerifyNodeInDB("Person", map[string]any{"name": "Alice"})
25+
tc.VerifyNodeInDB(personLabel, map[string]any{"name": "Alice"})
2426
}

0 commit comments

Comments
 (0)