diff --git a/bdd/steps_test.go b/bdd/steps_test.go index 904cc82a..445fbf17 100644 --- a/bdd/steps_test.go +++ b/bdd/steps_test.go @@ -20,14 +20,10 @@ package bdd import ( - "context" - "log" "os" "testing" "github.com/cucumber/godog" - "github.com/specterops/dawgs/graph" - "github.com/specterops/dawgs/integration" ) var ( @@ -35,18 +31,6 @@ var ( bddReportInvalid bool ) -func InitializeTestSuite(ctxtestsuite *godog.TestSuiteContext, ctx context.Context, c *dbContext) { - err := c.db.WriteTransaction(ctx, func(tx graph.Transaction) error { - if err := tx.Nodes().Delete(); err != nil { - return err - } - return nil - }) - if err != nil { - log.Fatalf("Failed to clear database %v", err) - } -} - func InitializeScenario(ctx *godog.ScenarioContext, dbCtx *dbContext) { ctx.Before(dbCtx.resetBeforeScenario) ctx.Step(`^an empty graph$`, dbCtx.anEmptyGraph) @@ -58,38 +42,29 @@ func InitializeScenario(ctx *godog.ScenarioContext, dbCtx *dbContext) { ctx.Step(`^no side effects$`, dbCtx.noSideEffects) ctx.Step(`^the side effects should be:$`, dbCtx.theSideEffectsShouldBe) ctx.Step(`^the result should be empty$`, dbCtx.theResultShouldBeEmpty) + ctx.After(dbCtx.closeDBSession) } func TestFeatures(t *testing.T) { - backgroundCtx := context.Background() if err := os.MkdirAll(bddReportDir, 0o755); err != nil { t.Fatalf("failed to create BDD report directory: %v", err) } - // establish database connection - session := integration.Open(t, integration.Options{ - Schema: &graph.Schema{ - DefaultGraph: graph.Graph{ - Name: "dawgs-bdd", - }, - }, - }) - dbCtx := &dbContext{ - db: session.DB, - testData: []string{"testdata/binary-tree-1.json", "testdata/binary-tree-2.json"}, - } + dbCtx := &dbContext{} + suite := godog.TestSuite{ Name: "OpenCypher-TCK", - TestSuiteInitializer: func(ctx *godog.TestSuiteContext) { - InitializeTestSuite(ctx, backgroundCtx, dbCtx) + TestSuiteInitializer: func(_ *godog.TestSuiteContext) { + dbCtx.testData = []string{"testdata/binary-tree-1.json", "testdata/binary-tree-2.json"} }, ScenarioInitializer: func(ctx *godog.ScenarioContext) { InitializeScenario(ctx, dbCtx) }, Options: &godog.Options{ - Format: "pretty,cucumber:" + bddJSONReportPath, - Paths: []string{"features"}, - TestingT: t, + Format: "pretty,cucumber:" + bddJSONReportPath, + Paths: []string{"features"}, + TestingT: t, + Concurrency: 1, }, } diff --git a/bdd/utils.go b/bdd/utils.go index b7c597ef..c809f55f 100644 --- a/bdd/utils.go +++ b/bdd/utils.go @@ -30,6 +30,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/specterops/dawgs/cypher/frontend" "github.com/specterops/dawgs/graph" + "github.com/specterops/dawgs/integration" "github.com/specterops/dawgs/opengraph" ) @@ -40,6 +41,7 @@ type graphSnapshot struct { type dbContext struct { db graph.Database + openSession func(integration.Options) (*integration.Session, error) testData []string beforeExecution graphSnapshot afterExecution graphSnapshot @@ -50,15 +52,46 @@ type dbContext struct { // resetBeforeScenario clears persistent graph data and scenario-local result state. func (c *dbContext) resetBeforeScenario(ctx context.Context, _ *godog.Scenario) (context.Context, error) { - if err := c.anEmptyGraph(ctx); err != nil { - return ctx, fmt.Errorf("reset graph before scenario: %w", err) - } - + c.db = nil c.beforeExecution = graphSnapshot{} c.actualResult = nil c.rowCount = 0 c.actualRows = nil + openSession := c.openSession + if openSession == nil { + openSession = integration.OpenSession + } + session, err := openSession(integration.Options{ + Schema: &graph.Schema{ + DefaultGraph: graph.Graph{ + Name: "dawgs-bdd", + }, + }, + }) + if err != nil { + return ctx, fmt.Errorf("failed to establish database connection error: %w", err) + } + + c.db = session.DB + + if err := c.anEmptyGraph(ctx); err != nil { + return ctx, fmt.Errorf("reset graph before scenario: %w", err) + } + return ctx, nil +} + +func (c *dbContext) closeDBSession(ctx context.Context, _ *godog.Scenario, _ error) (context.Context, error) { + c.resetBeforeScenario(ctx, nil) + if c.db == nil { + return ctx, nil + } + + db := c.db + c.db = nil + if err := db.Close(context.Background()); err != nil { + return ctx, err + } return ctx, nil } diff --git a/bdd/utils_test.go b/bdd/utils_test.go index 437a10cf..f4563cde 100644 --- a/bdd/utils_test.go +++ b/bdd/utils_test.go @@ -23,6 +23,7 @@ import ( "github.com/cucumber/godog" messages "github.com/cucumber/messages/go/v34" "github.com/specterops/dawgs/graph" + "github.com/specterops/dawgs/integration" "github.com/stretchr/testify/require" ) @@ -214,28 +215,37 @@ func TestDBContextAnEmptyGraph(t *testing.T) { func TestDBContextResetBeforeScenario(t *testing.T) { testContext := context.WithValue(context.Background(), struct{}{}, "scenario") + oldDatabase := &stubDatabase{} nodeQuery := &stubNodeQuery{} database := &stubDatabase{ transaction: &stubTransaction{nodeQuery: nodeQuery}, } databaseContext := &dbContext{ - db: database, + db: oldDatabase, + openSession: func(opts integration.Options) (*integration.Session, error) { + require.Equal(t, &graph.Schema{DefaultGraph: graph.Graph{Name: "dawgs-bdd"}}, opts.Schema) + return &integration.Session{DB: database}, nil + }, beforeExecution: graphSnapshot{ NodesCount: 2, RelationshipsCount: 1, }, actualResult: &stubResult{}, rowCount: 2, - actualRows: [][]string{{"stale"}}, + actualRows: [][]string{{}}, + testData: []string{"testdata/binary-tree-1.json", "testdata/binary-tree-2.json"}, } returnedContext, err := databaseContext.resetBeforeScenario(testContext, nil) require.NoError(t, err) require.Equal(t, testContext, returnedContext) + require.Same(t, database, databaseContext.db) + require.False(t, oldDatabase.writeTransactionCalled) require.True(t, database.writeTransactionCalled) require.Equal(t, testContext, database.writeTransactionContext) require.True(t, nodeQuery.deleteCalled) + require.Equal(t, []string{"testdata/binary-tree-1.json", "testdata/binary-tree-2.json"}, databaseContext.testData) require.Equal(t, graphSnapshot{}, databaseContext.beforeExecution) require.Nil(t, databaseContext.actualResult) require.Zero(t, databaseContext.rowCount) @@ -244,17 +254,87 @@ func TestDBContextResetBeforeScenario(t *testing.T) { func TestDBContextResetBeforeScenarioReturnsCleanupError(t *testing.T) { deleteErr := errors.New("failed to delete nodes") - database := &stubDatabase{ - transaction: &stubTransaction{ - nodeQuery: &stubNodeQuery{deleteErr: deleteErr}, + writeErr := errors.New("failed to open write transaction") + tests := []struct { + name string + deleteErr error + writeErr error + expectedErr error + expectDelete bool + }{ + {name: "node deletion error", deleteErr: deleteErr, expectedErr: deleteErr, expectDelete: true}, + {name: "write transaction error", writeErr: writeErr, expectedErr: writeErr}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + testContext := context.WithValue(context.Background(), struct{}{}, test.name) + oldDatabase := &stubDatabase{} + nodeQuery := &stubNodeQuery{deleteErr: test.deleteErr} + database := &stubDatabase{ + transaction: &stubTransaction{nodeQuery: nodeQuery}, + writeTransactionErr: test.writeErr, + } + result := &stubResult{} + snapshot := graphSnapshot{NodesCount: 2, RelationshipsCount: 1} + databaseContext := &dbContext{ + db: oldDatabase, + openSession: func(integration.Options) (*integration.Session, error) { + return &integration.Session{DB: database}, nil + }, + beforeExecution: snapshot, + actualResult: result, + rowCount: 2, + actualRows: [][]string{{"stale"}}, + } + + returnedContext, err := databaseContext.resetBeforeScenario(testContext, nil) + + require.ErrorIs(t, err, test.expectedErr) + require.ErrorContains(t, err, "reset graph before scenario") + require.Equal(t, testContext, returnedContext) + require.Same(t, database, databaseContext.db) + require.False(t, oldDatabase.writeTransactionCalled) + require.True(t, database.writeTransactionCalled) + require.Equal(t, testContext, database.writeTransactionContext) + require.Equal(t, test.expectDelete, nodeQuery.deleteCalled) + require.Equal(t, graphSnapshot{}, databaseContext.beforeExecution) + require.Nil(t, databaseContext.actualResult) + require.Zero(t, databaseContext.rowCount) + require.Nil(t, databaseContext.actualRows) + }) + } +} + +func TestDBContextResetBeforeScenarioReturnsSessionError(t *testing.T) { + testContext := context.WithValue(context.Background(), struct{}{}, "scenario") + openErr := errors.New("failed to open session") + database := &stubDatabase{} + result := &stubResult{} + snapshot := graphSnapshot{NodesCount: 2, RelationshipsCount: 1} + databaseContext := &dbContext{ + db: database, + openSession: func(integration.Options) (*integration.Session, error) { + return nil, openErr }, + testData: []string{"existing fixture"}, + beforeExecution: snapshot, + actualResult: result, + rowCount: 2, + actualRows: [][]string{{"stale"}}, } - databaseContext := &dbContext{db: database} - _, err := databaseContext.resetBeforeScenario(context.Background(), nil) + returnedContext, err := databaseContext.resetBeforeScenario(testContext, nil) - require.ErrorIs(t, err, deleteErr) - require.ErrorContains(t, err, "reset graph before scenario") + require.ErrorIs(t, err, openErr) + require.ErrorContains(t, err, "failed to establish database connection error") + require.Equal(t, testContext, returnedContext) + require.Nil(t, databaseContext.db) + require.False(t, database.writeTransactionCalled) + require.Equal(t, []string{"existing fixture"}, databaseContext.testData) + require.Equal(t, graphSnapshot{NodesCount: 0, RelationshipsCount: 0}, databaseContext.beforeExecution) + require.Nil(t, databaseContext.actualResult) + require.Equal(t, 0, databaseContext.rowCount) + require.Nil(t, databaseContext.actualRows) } func TestDBContextExecutingQuery(t *testing.T) {