@@ -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
2735type 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
3545var (
@@ -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
118129func (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.
310354func 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.
0 commit comments