diff --git a/tests/integration/events/simple/utils.go b/tests/integration/events/simple/utils.go new file mode 100644 index 0000000000..88a1ec543e --- /dev/null +++ b/tests/integration/events/simple/utils.go @@ -0,0 +1,27 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package simple + +import ( + "testing" + + testUtils "github.com/sourcenetwork/defradb/tests/integration/events" +) + +var schema = ` + type users { + Name: String + } +` + +func executeTestCase(t *testing.T, test testUtils.TestCase) { + testUtils.ExecuteQueryTestCase(t, schema, test) +} diff --git a/tests/integration/events/simple/with_create_test.go b/tests/integration/events/simple/with_create_test.go new file mode 100644 index 0000000000..67615a1392 --- /dev/null +++ b/tests/integration/events/simple/with_create_test.go @@ -0,0 +1,69 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package simple + +import ( + "context" + "testing" + + "github.com/sourcenetwork/immutable" + "github.com/stretchr/testify/assert" + + "github.com/sourcenetwork/defradb/client" + testUtils "github.com/sourcenetwork/defradb/tests/integration/events" +) + +func TestEventsSimpleWithCreate(t *testing.T) { + doc1, err := client.NewDocFromJSON( + []byte( + `{ + "Name": "John" + }`, + ), + ) + assert.Nil(t, err) + docKey1 := doc1.Key().String() + + doc2, err := client.NewDocFromJSON( + []byte( + `{ + "Name": "Shahzad" + }`, + ), + ) + assert.Nil(t, err) + docKey2 := doc2.Key().String() + + test := testUtils.TestCase{ + CollectionCalls: map[string][]func(client.Collection){ + "users": []func(c client.Collection){ + func(c client.Collection) { + err = c.Save(context.Background(), doc1) + assert.Nil(t, err) + }, + func(c client.Collection) { + err = c.Save(context.Background(), doc2) + assert.Nil(t, err) + }, + }, + }, + ExpectedUpdates: []testUtils.ExpectedUpdate{ + { + DocKey: immutable.Some(docKey1), + }, + { + DocKey: immutable.Some(docKey2), + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/events/simple/with_create_txn_test.go b/tests/integration/events/simple/with_create_txn_test.go new file mode 100644 index 0000000000..ea0aa21caa --- /dev/null +++ b/tests/integration/events/simple/with_create_txn_test.go @@ -0,0 +1,67 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package simple + +import ( + "context" + "testing" + + "github.com/sourcenetwork/immutable" + "github.com/stretchr/testify/assert" + + "github.com/sourcenetwork/defradb/client" + testUtils "github.com/sourcenetwork/defradb/tests/integration/events" +) + +func TestEventsSimpleWithCreateWithTxnDiscarded(t *testing.T) { + test := testUtils.TestCase{ + DatabaseCalls: []func(context.Context, client.DB){ + func(ctx context.Context, d client.DB) { + r := d.ExecQuery( + ctx, + `mutation { + create_users(data: "{\"Name\": \"John\"}") { + _key + } + }`, + ) + for _, err := range r.GQL.Errors { + assert.Nil(t, err) + } + }, + func(ctx context.Context, d client.DB) { + txn, err := d.NewTxn(ctx, false) + assert.Nil(t, err) + r := d.ExecTransactionalQuery( + ctx, + `mutation { + create_users(data: "{\"Name\": \"Shahzad\"}") { + _key + } + }`, + txn, + ) + for _, err := range r.GQL.Errors { + assert.Nil(t, err) + } + txn.Discard(ctx) + }, + }, + ExpectedUpdates: []testUtils.ExpectedUpdate{ + { + DocKey: immutable.Some("bae-43deba43-f2bc-59f4-9056-fef661b22832"), + }, + // No event should be recieved for Shahzad, as the transaction was discarded. + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/events/simple/with_delete_test.go b/tests/integration/events/simple/with_delete_test.go new file mode 100644 index 0000000000..4f2d5661db --- /dev/null +++ b/tests/integration/events/simple/with_delete_test.go @@ -0,0 +1,60 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package simple + +import ( + "context" + "testing" + + "github.com/sourcenetwork/immutable" + "github.com/stretchr/testify/assert" + + "github.com/sourcenetwork/defradb/client" + testUtils "github.com/sourcenetwork/defradb/tests/integration/events" +) + +// This test documents undesirable behaviour which should be corrected in +// https://github.com/sourcenetwork/defradb/issues/867 +func TestEventsSimpleWithDelete(t *testing.T) { + doc1, err := client.NewDocFromJSON( + []byte( + `{ + "Name": "John" + }`, + ), + ) + assert.Nil(t, err) + docKey1 := doc1.Key().String() + + test := testUtils.TestCase{ + CollectionCalls: map[string][]func(client.Collection){ + "users": []func(c client.Collection){ + func(c client.Collection) { + err = c.Save(context.Background(), doc1) + assert.Nil(t, err) + }, + func(c client.Collection) { + wasDeleted, err := c.Delete(context.Background(), doc1.Key()) + assert.Nil(t, err) + assert.True(t, wasDeleted) + }, + }, + }, + ExpectedUpdates: []testUtils.ExpectedUpdate{ + { + DocKey: immutable.Some(docKey1), + }, + // No update to reflect the delete + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/events/simple/with_update_test.go b/tests/integration/events/simple/with_update_test.go new file mode 100644 index 0000000000..3426151748 --- /dev/null +++ b/tests/integration/events/simple/with_update_test.go @@ -0,0 +1,80 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package simple + +import ( + "context" + "testing" + + "github.com/sourcenetwork/immutable" + "github.com/stretchr/testify/assert" + + "github.com/sourcenetwork/defradb/client" + testUtils "github.com/sourcenetwork/defradb/tests/integration/events" +) + +func TestEventsSimpleWithUpdate(t *testing.T) { + doc1, err := client.NewDocFromJSON( + []byte( + `{ + "Name": "John" + }`, + ), + ) + assert.Nil(t, err) + docKey1 := doc1.Key().String() + + doc2, err := client.NewDocFromJSON( + []byte( + `{ + "Name": "Shahzad" + }`, + ), + ) + assert.Nil(t, err) + docKey2 := doc2.Key().String() + + test := testUtils.TestCase{ + CollectionCalls: map[string][]func(client.Collection){ + "users": []func(c client.Collection){ + func(c client.Collection) { + err = c.Save(context.Background(), doc1) + assert.Nil(t, err) + }, + func(c client.Collection) { + err = c.Save(context.Background(), doc2) + assert.Nil(t, err) + }, + func(c client.Collection) { + // Update John + doc1.Set("Name", "Johnnnnn") + err = c.Save(context.Background(), doc1) + assert.Nil(t, err) + }, + }, + }, + ExpectedUpdates: []testUtils.ExpectedUpdate{ + { + DocKey: immutable.Some(docKey1), + Cid: immutable.Some("bafybeihm4gewwhzsyqs7tuazdsbusp6pm3oinhnj2ae6funcwshwpxuxri"), + }, + { + DocKey: immutable.Some(docKey2), + }, + { + DocKey: immutable.Some(docKey1), + Cid: immutable.Some("bafybeicxo5sqjzi2mjputtbzwnkliickvowwfigyzziibtwlj7xe3ca7ly"), + }, + }, + } + + executeTestCase(t, test) +} diff --git a/tests/integration/events/utils.go b/tests/integration/events/utils.go new file mode 100644 index 0000000000..6e4282c74f --- /dev/null +++ b/tests/integration/events/utils.go @@ -0,0 +1,175 @@ +// Copyright 2022 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package events + +import ( + "context" + "testing" + "time" + + "github.com/sourcenetwork/immutable" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/db" + testUtils "github.com/sourcenetwork/defradb/tests/integration" +) + +type TestCase struct { + Description string + + // docs is a map from Collection name, to a list + // of docs in stringified JSON format + Docs map[string][]string + + // The collection calls to make after subscribing to the events channel. + // + // Any errors generated within these calls are of no interest to this test + // framework and should be handled as desired by the caller. + CollectionCalls map[string][]func(client.Collection) + + // The daabase calls to make after subscribing to the events channel. + // + // Any errors generated within these calls are of no interest to this test + // framework and should be handled as desired by the caller. + DatabaseCalls []func(context.Context, client.DB) + + // The update events expected during the specified calls. The length will + // be asserted (within timeout). + ExpectedUpdates []ExpectedUpdate +} + +// A struct holding properties that can be asserted upon. +// +// Properties with a `None` value will not be asserted. If all properties +// are `None` the Update event will still be expected and will contribute +// to the asserted count. +type ExpectedUpdate struct { + DocKey immutable.Option[string] + // The expected Cid, as a string (results in much more readable errors) + Cid immutable.Option[string] + SchemaID immutable.Option[string] + Priority immutable.Option[uint64] +} + +type dbInfo interface { + DB() client.DB +} + +const eventTimeout = 100 * time.Millisecond + +func ExecuteQueryTestCase( + t *testing.T, + schema string, + testCase TestCase, +) { + var err error + ctx := context.Background() + + var dbi dbInfo + dbi, err = testUtils.NewBadgerMemoryDB(ctx, db.WithUpdateEvents()) + require.NoError(t, err) + + db := dbi.DB() + + err = db.AddSchema(ctx, schema) + require.NoError(t, err) + + setupDatabase(ctx, t, db, testCase) + + testRoutineClosedChan := make(chan struct{}) + closeTestRoutineChan := make(chan struct{}) + eventsChan, err := db.Events().Updates.Value().Subscribe() + require.NoError(t, err) + + indexOfNextExpectedUpdate := 0 + go func() { + for { + select { + case update := <-eventsChan: + if indexOfNextExpectedUpdate >= len(testCase.ExpectedUpdates) { + assert.Fail(t, "More events recieved than were expected", update) + testRoutineClosedChan <- struct{}{} + return + } + + expectedEvent := testCase.ExpectedUpdates[indexOfNextExpectedUpdate] + assertIfExpected(t, expectedEvent.Cid, update.Cid.String()) + assertIfExpected(t, expectedEvent.DocKey, update.DocKey) + assertIfExpected(t, expectedEvent.Priority, update.Priority) + assertIfExpected(t, expectedEvent.SchemaID, update.SchemaID) + + indexOfNextExpectedUpdate++ + case <-closeTestRoutineChan: + return + } + } + }() + + for collectionName, collectionCallSet := range testCase.CollectionCalls { + col, err := db.GetCollectionByName(ctx, collectionName) + require.NoError(t, err) + + for _, collectionCall := range collectionCallSet { + collectionCall(col) + } + } + + for _, databaseCall := range testCase.DatabaseCalls { + databaseCall(ctx, db) + } + + select { + case <-time.After(eventTimeout): + // Trigger an exit from the go routine monitoring the eventsChan. + // As well as being a bit cleaner it stops the `--race` flag from + // (rightly) seeing the assert of indexOfNextExpectedUpdate as a + // data race. + closeTestRoutineChan <- struct{}{} + case <-testRoutineClosedChan: + // no-op - just allow the host func to continue + } + + // This is expressed verbosely, as `len(testCase.ExpectedUpdates) == indexOfNextExpectedUpdate` + // is less easy to understand than the below + indexOfLastExpectedUpdate := len(testCase.ExpectedUpdates) - 1 + indexOfLastAssertedUpdate := indexOfNextExpectedUpdate - 1 + assert.Equal(t, indexOfLastExpectedUpdate, indexOfLastAssertedUpdate) +} + +func setupDatabase( + ctx context.Context, + t *testing.T, + db client.DB, + testCase TestCase, +) { + for collectionName, docs := range testCase.Docs { + col, err := db.GetCollectionByName(ctx, collectionName) + require.NoError(t, err) + + for _, docStr := range docs { + doc, err := client.NewDocFromJSON([]byte(docStr)) + require.NoError(t, err) + + err = col.Save(ctx, doc) + require.NoError(t, err) + } + } +} + +// assertIfExpected asserts that the given values are Equal, if the expected parameter +// has a value. Otherwise this function will do nothing. +func assertIfExpected[T any](t *testing.T, expected immutable.Option[T], actual T) { + if expected.HasValue() { + assert.Equal(t, expected.Value(), actual) + } +}