components-contrib/state/postgresql/postgresql_integration_test.go

567 lines
14 KiB
Go

// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
package postgresql
import (
"database/sql"
"encoding/json"
"fmt"
"os"
"testing"
"github.com/dapr/components-contrib/state"
"github.com/dapr/dapr/pkg/logger"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
const (
connectionStringEnvKey = "DAPR_TEST_POSTGRES_CONNSTRING" // Environment variable containing the connection string
)
type fakeItem struct {
Color string
}
func TestPostgreSQLIntegration(t *testing.T) {
connectionString := getConnectionString()
if connectionString == "" {
t.Skipf("PostgreSQL state integration tests skipped. To enable define the connection string using environment variable '%s' (example 'export %s=\"host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test\")", connectionStringEnvKey, connectionStringEnvKey)
}
t.Run("Test init configurations", func(t *testing.T) {
testInitConfiguration(t)
})
metadata := state.Metadata{
Properties: map[string]string{connectionStringKey: connectionString},
}
pgs := NewPostgreSQLStateStore(logger.NewLogger("test"))
t.Cleanup(func() {
defer pgs.Close()
})
error := pgs.Init(metadata)
if error != nil {
t.Fatal(error)
}
t.Run("Create table succeeds", func(t *testing.T) {
t.Parallel()
testCreateTable(t, pgs.dbaccess.(*postgresDBAccess))
})
t.Run("Get Set Delete one item", func(t *testing.T) {
t.Parallel()
setGetUpdateDeleteOneItem(t, pgs)
})
t.Run("Get item that does not exist", func(t *testing.T) {
t.Parallel()
getItemThatDoesNotExist(t, pgs)
})
t.Run("Get item with no key fails", func(t *testing.T) {
t.Parallel()
getItemWithNoKey(t, pgs)
})
t.Run("Set updates the updatedate field", func(t *testing.T) {
t.Parallel()
setUpdatesTheUpdatedateField(t, pgs)
})
t.Run("Set item with no key fails", func(t *testing.T) {
t.Parallel()
setItemWithNoKey(t, pgs)
})
t.Run("Bulk set and bulk delete", func(t *testing.T) {
t.Parallel()
testBulkSetAndBulkDelete(t, pgs)
})
t.Run("Update and delete with etag succeeds", func(t *testing.T) {
t.Parallel()
updateAndDeleteWithEtagSucceeds(t, pgs)
})
t.Run("Update with old etag fails", func(t *testing.T) {
t.Parallel()
updateWithOldEtagFails(t, pgs)
})
t.Run("Insert with etag fails", func(t *testing.T) {
t.Parallel()
newItemWithEtagFails(t, pgs)
})
t.Run("Delete with invalid etag fails", func(t *testing.T) {
t.Parallel()
deleteWithInvalidEtagFails(t, pgs)
})
t.Run("Delete item with no key fails", func(t *testing.T) {
t.Parallel()
deleteWithNoKeyFails(t, pgs)
})
t.Run("Delete an item that does not exist", func(t *testing.T) {
t.Parallel()
deleteItemThatDoesNotExist(t, pgs)
})
t.Run("Multi with delete and set", func(t *testing.T) {
t.Parallel()
multiWithDeleteAndSet(t, pgs)
})
t.Run("Multi with delete only", func(t *testing.T) {
t.Parallel()
multiWithDeleteOnly(t, pgs)
})
t.Run("Multi with set only", func(t *testing.T) {
t.Parallel()
multiWithSetOnly(t, pgs)
})
}
// setGetUpdateDeleteOneItem validates setting one item, getting it, and deleting it.
func setGetUpdateDeleteOneItem(t *testing.T, pgs *PostgreSQL) {
key := randomKey()
//value := `{"something": "DKbLaZwrlCAZ"}`
value := &fakeItem{Color: "yellow"}
setItem(t, pgs, key, value, "")
getResponse, outputObject := getItem(t, pgs, key)
assert.Equal(t, value, outputObject)
newValue := &fakeItem{Color: "green"}
setItem(t, pgs, key, newValue, getResponse.ETag)
getResponse, outputObject = getItem(t, pgs, key)
assert.Equal(t, newValue, outputObject)
deleteItem(t, pgs, key, getResponse.ETag)
}
// testCreateTable tests the ability to create the state table.
func testCreateTable(t *testing.T, dba *postgresDBAccess) {
tableName := "test_state"
// Drop the table if it already exists
exists, err := tableExists(dba.db, tableName)
assert.Nil(t, err)
if exists {
dropTable(t, dba.db, tableName)
}
// Create the state table and test for its existence
err = dba.ensureStateTable(tableName)
assert.Nil(t, err)
exists, err = tableExists(dba.db, tableName)
assert.Nil(t, err)
assert.True(t, exists)
// Drop the state table
dropTable(t, dba.db, tableName)
}
func dropTable(t *testing.T, db *sql.DB, tableName string) {
_, err := db.Exec(fmt.Sprintf("DROP TABLE %s", tableName))
assert.Nil(t, err)
}
func deleteItemThatDoesNotExist(t *testing.T, pgs *PostgreSQL) {
// Delete the item with a fake etag
deleteReq := &state.DeleteRequest{
Key: randomKey(),
}
err := pgs.Delete(deleteReq)
assert.NotNil(t, err)
}
func multiWithSetOnly(t *testing.T, pgs *PostgreSQL) {
var multiRequest []state.TransactionalRequest
var setRequests []state.SetRequest
for i := 0; i < 3; i++ {
req := state.SetRequest{
Key: randomKey(),
Value: randomJSON(),
}
setRequests = append(setRequests, req)
multiRequest = append(multiRequest, state.TransactionalRequest{
Operation: state.Upsert,
Request: req,
})
}
err := pgs.Multi(multiRequest)
assert.Nil(t, err)
for _, set := range setRequests {
assert.True(t, storeItemExists(t, set.Key))
deleteItem(t, pgs, set.Key, "")
}
}
func multiWithDeleteOnly(t *testing.T, pgs *PostgreSQL) {
var multiRequest []state.TransactionalRequest
var deleteRequests []state.DeleteRequest
for i := 0; i < 3; i++ {
req := state.DeleteRequest{Key: randomKey()}
// Add the item to the database
setItem(t, pgs, req.Key, randomJSON(), "") // Add the item to the database
// Add the item to a slice of delete requests
deleteRequests = append(deleteRequests, req)
// Add the item to the multi transaction request
multiRequest = append(multiRequest, state.TransactionalRequest{
Operation: state.Delete,
Request: req,
})
}
err := pgs.Multi(multiRequest)
assert.Nil(t, err)
for _, delete := range deleteRequests {
assert.False(t, storeItemExists(t, delete.Key))
}
}
func multiWithDeleteAndSet(t *testing.T, pgs *PostgreSQL) {
var multiRequest []state.TransactionalRequest
var deleteRequests []state.DeleteRequest
for i := 0; i < 3; i++ {
req := state.DeleteRequest{Key: randomKey()}
// Add the item to the database
setItem(t, pgs, req.Key, randomJSON(), "") // Add the item to the database
// Add the item to a slice of delete requests
deleteRequests = append(deleteRequests, req)
// Add the item to the multi transaction request
multiRequest = append(multiRequest, state.TransactionalRequest{
Operation: state.Delete,
Request: req,
})
}
// Create the set requests
var setRequests []state.SetRequest
for i := 0; i < 3; i++ {
req := state.SetRequest{
Key: randomKey(),
Value: randomJSON(),
}
setRequests = append(setRequests, req)
multiRequest = append(multiRequest, state.TransactionalRequest{
Operation: state.Upsert,
Request: req,
})
}
err := pgs.Multi(multiRequest)
assert.Nil(t, err)
for _, delete := range deleteRequests {
assert.False(t, storeItemExists(t, delete.Key))
}
for _, set := range setRequests {
assert.True(t, storeItemExists(t, set.Key))
deleteItem(t, pgs, set.Key, "")
}
}
func deleteWithInvalidEtagFails(t *testing.T, pgs *PostgreSQL) {
// Create new item
key := randomKey()
value := &fakeItem{Color: "mauve"}
setItem(t, pgs, key, value, "")
// Delete the item with a fake etag
deleteReq := &state.DeleteRequest{
Key: key,
ETag: "1234",
}
err := pgs.Delete(deleteReq)
assert.NotNil(t, err)
}
func deleteWithNoKeyFails(t *testing.T, pgs *PostgreSQL) {
deleteReq := &state.DeleteRequest{
Key: "",
}
err := pgs.Delete(deleteReq)
assert.NotNil(t, err)
}
// newItemWithEtagFails creates a new item and also supplies an ETag, which is invalid - expect failure
func newItemWithEtagFails(t *testing.T, pgs *PostgreSQL) {
value := &fakeItem{Color: "teal"}
invalidEtag := "12345"
setReq := &state.SetRequest{
Key: randomKey(),
ETag: invalidEtag,
Value: value,
}
err := pgs.Set(setReq)
assert.NotNil(t, err)
}
func updateWithOldEtagFails(t *testing.T, pgs *PostgreSQL) {
// Create and retrieve new item
key := randomKey()
value := &fakeItem{Color: "gray"}
setItem(t, pgs, key, value, "")
getResponse, _ := getItem(t, pgs, key)
assert.NotNil(t, getResponse.ETag)
originalEtag := getResponse.ETag
// Change the value and get the updated etag
newValue := &fakeItem{Color: "silver"}
setItem(t, pgs, key, newValue, originalEtag)
_, updatedItem := getItem(t, pgs, key)
assert.Equal(t, newValue, updatedItem)
// Update again with the original etag - expect udpate failure
newValue = &fakeItem{Color: "maroon"}
setReq := &state.SetRequest{
Key: key,
ETag: originalEtag,
Value: newValue,
}
err := pgs.Set(setReq)
assert.NotNil(t, err)
}
func updateAndDeleteWithEtagSucceeds(t *testing.T, pgs *PostgreSQL) {
// Create and retrieve new item
key := randomKey()
value := &fakeItem{Color: "hazel"}
setItem(t, pgs, key, value, "")
getResponse, _ := getItem(t, pgs, key)
assert.NotNil(t, getResponse.ETag)
// Change the value and compare
value.Color = "purple"
setItem(t, pgs, key, value, getResponse.ETag)
updateResponse, updatedItem := getItem(t, pgs, key)
assert.Equal(t, value, updatedItem)
// ETag should change when item is updated
assert.NotEqual(t, getResponse.ETag, updateResponse.ETag)
// Delete
deleteItem(t, pgs, key, updateResponse.ETag)
// Item is not in the data store
assert.False(t, storeItemExists(t, key))
}
// getItemThatDoesNotExist validates the behavior of retrieving an item that does not exist.
func getItemThatDoesNotExist(t *testing.T, pgs *PostgreSQL) {
key := randomKey()
response, outputObject := getItem(t, pgs, key)
assert.Nil(t, response.Data)
assert.Equal(t, "", outputObject.Color)
}
// getItemWithNoKey validates that attempting a Get operation without providing a key will return an error.
func getItemWithNoKey(t *testing.T, pgs *PostgreSQL) {
getReq := &state.GetRequest{
Key: "",
}
response, getErr := pgs.Get(getReq)
assert.NotNil(t, getErr)
assert.Nil(t, response)
}
// setUpdatesTheUpdatedateField proves that the updateddate is set for an update, and not set upon insert.
func setUpdatesTheUpdatedateField(t *testing.T, pgs *PostgreSQL) {
key := randomKey()
value := &fakeItem{Color: "orange"}
setItem(t, pgs, key, value, "")
// insertdate should have a value and updatedate should be nil
_, insertdate, updatedate := getRowData(t, key)
assert.NotNil(t, insertdate)
assert.Equal(t, "", updatedate.String)
// insertdate should not change, updatedate should have a value
value = &fakeItem{Color: "aqua"}
setItem(t, pgs, key, value, "")
_, newinsertdate, updatedate := getRowData(t, key)
assert.Equal(t, insertdate, newinsertdate) // The insertdate should not change.
assert.NotEqual(t, "", updatedate.String)
deleteItem(t, pgs, key, "")
}
func setItemWithNoKey(t *testing.T, pgs *PostgreSQL) {
setReq := &state.SetRequest{
Key: "",
}
err := pgs.Set(setReq)
assert.NotNil(t, err)
}
// Tests valid bulk sets and deletes
func testBulkSetAndBulkDelete(t *testing.T, pgs *PostgreSQL) {
setReq := []state.SetRequest{
{
Key: randomKey(),
Value: &fakeItem{Color: "blue"},
},
{
Key: randomKey(),
Value: &fakeItem{Color: "red"},
},
}
err := pgs.BulkSet(setReq)
assert.Nil(t, err)
assert.True(t, storeItemExists(t, setReq[0].Key))
assert.True(t, storeItemExists(t, setReq[1].Key))
deleteReq := []state.DeleteRequest{
{
Key: setReq[0].Key,
},
{
Key: setReq[1].Key,
},
}
err = pgs.BulkDelete(deleteReq)
assert.Nil(t, err)
assert.False(t, storeItemExists(t, setReq[0].Key))
assert.False(t, storeItemExists(t, setReq[1].Key))
}
// testInitConfiguration tests valid and invalid config settings
func testInitConfiguration(t *testing.T) {
logger := logger.NewLogger("test")
tests := []struct {
name string
props map[string]string
expectedErr string
}{
{
name: "Empty",
props: map[string]string{},
expectedErr: errMissingConnectionString,
},
{
name: "Valid connection string",
props: map[string]string{connectionStringKey: getConnectionString()},
expectedErr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewPostgreSQLStateStore(logger)
defer p.Close()
metadata := state.Metadata{
Properties: tt.props,
}
err := p.Init(metadata)
if tt.expectedErr == "" {
assert.Nil(t, err)
} else {
assert.NotNil(t, err)
assert.Equal(t, err.Error(), tt.expectedErr)
}
})
}
}
func getConnectionString() string {
return os.Getenv(connectionStringEnvKey)
}
func setItem(t *testing.T, pgs *PostgreSQL, key string, value interface{}, etag string) {
setReq := &state.SetRequest{
Key: key,
ETag: etag,
Value: value,
}
err := pgs.Set(setReq)
assert.Nil(t, err)
itemExists := storeItemExists(t, key)
assert.True(t, itemExists)
}
func getItem(t *testing.T, pgs *PostgreSQL, key string) (*state.GetResponse, *fakeItem) {
getReq := &state.GetRequest{
Key: key,
Options: state.GetStateOption{},
}
response, getErr := pgs.Get(getReq)
assert.Nil(t, getErr)
assert.NotNil(t, response)
outputObject := &fakeItem{}
_ = json.Unmarshal(response.Data, outputObject)
return response, outputObject
}
func deleteItem(t *testing.T, pgs *PostgreSQL, key string, etag string) {
deleteReq := &state.DeleteRequest{
Key: key,
ETag: etag,
Options: state.DeleteStateOption{},
}
deleteErr := pgs.Delete(deleteReq)
assert.Nil(t, deleteErr)
assert.False(t, storeItemExists(t, key))
}
func storeItemExists(t *testing.T, key string) bool {
db, err := sql.Open("pgx", getConnectionString())
assert.Nil(t, err)
defer db.Close()
var exists bool = false
statement := fmt.Sprintf(`SELECT EXISTS (SELECT FROM %s WHERE key = $1)`, tableName)
err = db.QueryRow(statement, key).Scan(&exists)
assert.Nil(t, err)
return exists
}
func getRowData(t *testing.T, key string) (returnValue string, insertdate sql.NullString, updatedate sql.NullString) {
db, err := sql.Open("pgx", getConnectionString())
assert.Nil(t, err)
defer db.Close()
err = db.QueryRow(fmt.Sprintf("SELECT value, insertdate, updatedate FROM %s WHERE key = $1", tableName), key).Scan(&returnValue, &insertdate, &updatedate)
assert.Nil(t, err)
return returnValue, insertdate, updatedate
}
func randomKey() string {
return uuid.New().String()
}
func randomJSON() *fakeItem {
return &fakeItem{Color: randomKey()}
}