932 lines
23 KiB
Go
932 lines
23 KiB
Go
/*
|
|
Copyright 2021 The Dapr Authors
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package mysql
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/dapr/components-contrib/metadata"
|
|
"github.com/dapr/components-contrib/state"
|
|
"github.com/dapr/components-contrib/state/utils"
|
|
"github.com/dapr/kit/logger"
|
|
)
|
|
|
|
const (
|
|
fakeConnectionString = "not a real connection"
|
|
keyTableName = "tableName"
|
|
keyConnectionString = "connectionString"
|
|
keySchemaName = "schemaName"
|
|
)
|
|
|
|
func TestEnsureStateSchemaHandlesShortConnectionString(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
m.mySQL.schemaName = "theSchema"
|
|
m.mySQL.connectionString = "theUser:thePassword@/"
|
|
|
|
rows := sqlmock.NewRows([]string{"exists"}).AddRow(1)
|
|
m.mock1.ExpectQuery("SELECT EXISTS").WillReturnRows(rows)
|
|
|
|
// Act
|
|
m.mySQL.ensureStateSchema(t.Context())
|
|
|
|
// Assert
|
|
assert.Equal(t, "theUser:thePassword@/theSchema", m.mySQL.connectionString)
|
|
}
|
|
|
|
func TestFinishInitHandlesSchemaExistsError(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
expectedErr := errors.New("existsError")
|
|
m.mock1.ExpectQuery("SELECT EXISTS").WillReturnError(expectedErr)
|
|
|
|
// Act
|
|
actualErr := m.mySQL.finishInit(t.Context(), m.mySQL.db)
|
|
|
|
// Assert
|
|
require.Error(t, actualErr, "now error returned")
|
|
assert.Equal(t, "existsError", actualErr.Error(), "wrong error")
|
|
}
|
|
|
|
func TestFinishInitHandlesDatabaseCreateError(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
rows := sqlmock.NewRows([]string{"exists"}).AddRow(0)
|
|
m.mock1.ExpectQuery("SELECT EXISTS").WillReturnRows(rows)
|
|
|
|
expectedErr := errors.New("createDatabaseError")
|
|
m.mock1.ExpectExec("CREATE DATABASE").WillReturnError(expectedErr)
|
|
|
|
// Act
|
|
actualErr := m.mySQL.finishInit(t.Context(), m.mySQL.db)
|
|
|
|
// Assert
|
|
require.Error(t, actualErr, "now error returned")
|
|
assert.Equal(t, "createDatabaseError", actualErr.Error(), "wrong error")
|
|
}
|
|
|
|
func TestFinishInitHandlesPingError(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
m.factory.openCount = 1
|
|
|
|
// See if Schema exists
|
|
rows := sqlmock.NewRows([]string{"exists"}).AddRow(1)
|
|
m.mock1.ExpectQuery("SELECT EXISTS").WillReturnRows(rows)
|
|
|
|
m.mock1.ExpectClose()
|
|
|
|
expectedErr := errors.New("pingError")
|
|
m.mock2.ExpectPing().WillReturnError(expectedErr)
|
|
|
|
// Act
|
|
actualErr := m.mySQL.finishInit(t.Context(), m.mySQL.db)
|
|
|
|
// Assert
|
|
require.Error(t, actualErr, "now error returned")
|
|
assert.Equal(t, "pingError", actualErr.Error(), "wrong error")
|
|
}
|
|
|
|
// Verifies that finishInit can handle an error from its call to
|
|
// ensureStateTable. The code should not attempt to create the table. Because
|
|
// there is no m.mock1.ExpectExec if the code attempts to execute the create
|
|
// table commnad this test will fail.
|
|
func TestFinishInitHandlesTableExistsError(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
m.factory.openCount = 1
|
|
|
|
// See if Schema exists
|
|
rows := sqlmock.NewRows([]string{"exists"}).AddRow(1)
|
|
m.mock1.ExpectQuery("SELECT EXISTS").WillReturnRows(rows)
|
|
m.mock1.ExpectClose()
|
|
|
|
// Execute use command
|
|
m.mock2.ExpectPing()
|
|
m.mock2.ExpectQuery("SELECT EXISTS").WillReturnError(errors.New("tableExistsError"))
|
|
|
|
// Act
|
|
err := m.mySQL.finishInit(t.Context(), m.mySQL.db)
|
|
|
|
// Assert
|
|
require.Error(t, err, "no error returned")
|
|
assert.Equal(t, "tableExistsError", err.Error(), "tableExists did not return err")
|
|
}
|
|
|
|
func TestClosingDatabaseTwiceReturnsNil(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
m.mySQL.Close()
|
|
m.mySQL.db = nil
|
|
|
|
// Act
|
|
err := m.mySQL.Close()
|
|
|
|
// Assert
|
|
require.NoError(t, err, "error returned")
|
|
}
|
|
|
|
func TestMultiCommitSetsAndDeletes(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
m.mock1.ExpectBegin()
|
|
m.mock1.ExpectExec("REPLACE INTO").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
m.mock1.ExpectExec("DELETE FROM").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
m.mock1.ExpectCommit()
|
|
|
|
request := state.TransactionalStateRequest{
|
|
Operations: []state.TransactionalStateOperation{
|
|
createSetRequest(),
|
|
createDeleteRequest(),
|
|
},
|
|
Metadata: map[string]string{},
|
|
}
|
|
|
|
// Act
|
|
err := m.mySQL.Multi(t.Context(), &request)
|
|
|
|
// Assert
|
|
require.NoError(t, err, "error returned")
|
|
}
|
|
|
|
func TestSetHandlesOptionsError(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
request := createSetRequest()
|
|
|
|
request.Options.Consistency = "Invalid"
|
|
|
|
// Act
|
|
err := m.mySQL.Set(t.Context(), &request)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestSetHandlesNoKey(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
request := createSetRequest()
|
|
request.Key = ""
|
|
|
|
// Act
|
|
err := m.mySQL.Set(t.Context(), &request)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
assert.Equal(t, "missing key in set operation", err.Error(), "wrong error returned")
|
|
}
|
|
|
|
func TestSetHandlesUpdate(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
m.mock1.ExpectExec("UPDATE state").WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
eTag := "946af56e"
|
|
|
|
request := createSetRequest()
|
|
request.ETag = &eTag
|
|
|
|
// Act
|
|
err := m.mySQL.Set(t.Context(), &request)
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestSetHandlesErr(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
t.Run("error occurs when insert", func(t *testing.T) {
|
|
m.mock1.ExpectExec("REPLACE INTO state").WillReturnError(errors.New("error"))
|
|
request := createSetRequest()
|
|
|
|
// Act
|
|
err := m.mySQL.Set(t.Context(), &request)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
assert.Equal(t, "error", err.Error())
|
|
})
|
|
|
|
t.Run("insert on conflict", func(t *testing.T) {
|
|
m.mock1.ExpectExec("REPLACE INTO state").WillReturnResult(sqlmock.NewResult(1, 2))
|
|
request := createSetRequest()
|
|
|
|
// Act
|
|
err := m.mySQL.Set(t.Context(), &request)
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("no rows effected error", func(t *testing.T) {
|
|
m.mock1.ExpectExec("UPDATE state").WillReturnResult(sqlmock.NewResult(1, 0))
|
|
|
|
eTag := "illegal etag"
|
|
request := createSetRequest()
|
|
request.ETag = &eTag
|
|
|
|
// Act
|
|
err := m.mySQL.Set(t.Context(), &request)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
assert.IsType(t, &state.ETagError{}, err)
|
|
assert.Equal(t, state.ETagMismatch, err.(*state.ETagError).Kind())
|
|
})
|
|
}
|
|
|
|
// Verifies that MySQL passes through to myDBAccess.
|
|
func TestMySQLDeleteHandlesNoKey(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
request := createDeleteRequest()
|
|
request.Key = ""
|
|
|
|
// Act
|
|
err := m.mySQL.Delete(t.Context(), &request)
|
|
|
|
// Asset
|
|
require.Error(t, err)
|
|
assert.Equal(t, "missing key in delete operation", err.Error(), "wrong error returned")
|
|
}
|
|
|
|
func TestDeleteWithETag(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
m.mock1.ExpectExec("DELETE FROM").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
eTag := "946af562"
|
|
request := createDeleteRequest()
|
|
request.ETag = &eTag
|
|
|
|
// Act
|
|
err := m.mySQL.Delete(t.Context(), &request)
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestDeleteWithErr(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
t.Run("error occurs when delete", func(t *testing.T) {
|
|
m.mock1.ExpectExec("DELETE FROM").WillReturnError(errors.New("error"))
|
|
|
|
request := createDeleteRequest()
|
|
|
|
// Act
|
|
err := m.mySQL.Delete(t.Context(), &request)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
assert.Equal(t, "error", err.Error())
|
|
})
|
|
|
|
t.Run("etag mismatch", func(t *testing.T) {
|
|
m.mock1.ExpectExec("DELETE FROM").WillReturnResult(sqlmock.NewResult(0, 0))
|
|
|
|
eTag := "946af563"
|
|
request := createDeleteRequest()
|
|
request.ETag = &eTag
|
|
|
|
// Act
|
|
err := m.mySQL.Delete(t.Context(), &request)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
assert.IsType(t, &state.ETagError{}, err)
|
|
assert.Equal(t, state.ETagMismatch, err.(*state.ETagError).Kind())
|
|
})
|
|
}
|
|
|
|
func TestGetHandlesNoRows(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
m.mock1.ExpectQuery("SELECT id").WillReturnRows(sqlmock.NewRows([]string{"UnitTest", "value", "eTag"}))
|
|
|
|
request := &state.GetRequest{
|
|
Key: "UnitTest",
|
|
}
|
|
|
|
// Act
|
|
response, err := m.mySQL.Get(t.Context(), request)
|
|
|
|
// Assert
|
|
require.NoError(t, err, "returned error")
|
|
assert.NotNil(t, response, "did not return empty response")
|
|
}
|
|
|
|
func TestGetHandlesNoKey(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
request := &state.GetRequest{
|
|
Key: "",
|
|
}
|
|
|
|
// Act
|
|
response, err := m.mySQL.Get(t.Context(), request)
|
|
|
|
// Assert
|
|
require.Error(t, err, "returned error")
|
|
assert.Equal(t, "missing key in get operation", err.Error(), "wrong error returned")
|
|
assert.Nil(t, response, "returned response")
|
|
}
|
|
|
|
func TestGetHandlesGenericError(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
m.mock1.ExpectQuery("").WillReturnError(errors.New("generic error"))
|
|
|
|
request := &state.GetRequest{
|
|
Key: "UnitTest",
|
|
}
|
|
|
|
// Act
|
|
response, err := m.mySQL.Get(t.Context(), request)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
assert.Nil(t, response)
|
|
}
|
|
|
|
func TestGetSucceeds(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
t.Run("has json type", func(t *testing.T) {
|
|
rows := sqlmock.NewRows([]string{"id", "value", "eTag", "isbinary", "expiredate"}).AddRow("UnitTest", "{}", "946af56e", false, "")
|
|
m.mock1.ExpectQuery(`SELECT id, value, eTag, isbinary, IFNULL\(expiredate, ""\) FROM state WHERE id = ?`).WillReturnRows(rows)
|
|
|
|
request := &state.GetRequest{
|
|
Key: "UnitTest",
|
|
}
|
|
|
|
// Act
|
|
response, err := m.mySQL.Get(t.Context(), request)
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, response)
|
|
assert.Equal(t, "{}", string(response.Data))
|
|
assert.NotContains(t, response.Metadata, state.GetRespMetaKeyTTLExpireTime)
|
|
})
|
|
|
|
t.Run("has binary type and expiredate", func(t *testing.T) {
|
|
now := time.UnixMilli(20001).UTC()
|
|
|
|
value, _ := utils.Marshal(base64.StdEncoding.EncodeToString([]byte("abcdefg")), json.Marshal)
|
|
rows := sqlmock.NewRows([]string{"id", "value", "eTag", "isbinary", "expiredate"}).AddRow("UnitTest", value, "946af56e", true, now.Format(time.DateTime))
|
|
m.mock1.ExpectQuery(`SELECT id, value, eTag, isbinary, IFNULL\(expiredate, ""\) FROM state WHERE id = ?`).WillReturnRows(rows)
|
|
|
|
request := &state.GetRequest{
|
|
Key: "UnitTest",
|
|
}
|
|
|
|
// Act
|
|
response, err := m.mySQL.Get(t.Context(), request)
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, response)
|
|
assert.Equal(t, "abcdefg", string(response.Data))
|
|
assert.Contains(t, response.Metadata, state.GetRespMetaKeyTTLExpireTime)
|
|
assert.Equal(t, "1970-01-01T00:00:20Z", response.Metadata[state.GetRespMetaKeyTTLExpireTime])
|
|
})
|
|
}
|
|
|
|
// Verifies that the correct query is executed to test if the table
|
|
// already exists in the database or not.
|
|
func TestTableExists(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
// Return a result that indicates that the table already exists in the
|
|
// database.
|
|
rows := sqlmock.NewRows([]string{"exists"}).AddRow(1)
|
|
m.mock1.ExpectQuery("SELECT EXISTS").WillReturnRows(rows)
|
|
|
|
// Act
|
|
actual, err := tableExists(t.Context(), m.mySQL.db, "dapr_state_store", "store", 10*time.Second)
|
|
|
|
// Assert
|
|
require.NoError(t, err, `error was returned`)
|
|
assert.True(t, actual, `table does not exists`)
|
|
}
|
|
|
|
// Verifies that the code returns an error if the create table command fails.
|
|
func TestEnsureStateTableHandlesCreateTableError(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
rows := sqlmock.NewRows([]string{"exists"}).AddRow(0)
|
|
m.mock1.ExpectQuery("SELECT EXISTS").WillReturnRows(rows)
|
|
m.mock1.ExpectExec("CREATE TABLE").WillReturnError(errors.New("CreateTableError"))
|
|
|
|
// Act
|
|
err := m.mySQL.ensureStateTable(t.Context(), "dapr_state_store", "state")
|
|
|
|
// Assert
|
|
require.Error(t, err, "no error returned")
|
|
assert.Equal(t, "CreateTableError", err.Error(), "wrong error returned")
|
|
}
|
|
|
|
// Verifies that ensureStateTable creates the table when tableExists returns
|
|
// false.
|
|
func TestEnsureStateTableCreatesTable(t *testing.T) {
|
|
// Arrange
|
|
m, _ := mockDatabase(t)
|
|
defer m.mySQL.Close()
|
|
|
|
// Return exists = 0 when Select Exists is called to indicate the table
|
|
// does not already exist.
|
|
rows := sqlmock.NewRows([]string{"exists"}).AddRow(0)
|
|
m.mock1.ExpectQuery("SELECT EXISTS").WillReturnRows(rows)
|
|
m.mock1.ExpectExec("CREATE TABLE").WillReturnResult(sqlmock.NewResult(1, 1))
|
|
rows = sqlmock.NewRows([]string{"exists"}).AddRow(1)
|
|
m.mock1.ExpectQuery("SELECT count(/*)").WillReturnRows(rows)
|
|
m.mock1.ExpectExec("CREATE PROCEDURE").WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
// Act
|
|
err := m.mySQL.ensureStateTable(t.Context(), "dapr_state_store", "state")
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Verify that the call to MySQL init get passed through
|
|
// to the DbAccess instance.
|
|
func TestInitReturnsErrorOnNoConnectionString(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
metadata := &state.Metadata{
|
|
Base: metadata.Base{Properties: map[string]string{keyConnectionString: ""}},
|
|
}
|
|
|
|
// Act
|
|
err := m.mySQL.Init(t.Context(), *metadata)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
assert.Equal(t, defaultTableName, m.mySQL.tableName, "table name did not default")
|
|
}
|
|
|
|
func TestInitReturnsErrorOnFailOpen(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
metadata := &state.Metadata{
|
|
Base: metadata.Base{Properties: map[string]string{keyConnectionString: fakeConnectionString}},
|
|
}
|
|
m.mock1.ExpectQuery("SELECT EXISTS").WillReturnError(sql.ErrConnDone)
|
|
|
|
// Act
|
|
err := m.mySQL.Init(t.Context(), *metadata)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInitHandlesRegisterTLSConfigError(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
m.factory.registerErr = errors.New("registerTLSConfigError")
|
|
|
|
metadata := &state.Metadata{
|
|
Base: metadata.Base{
|
|
Properties: map[string]string{
|
|
keyPemPath: "./ssl.pem",
|
|
keyTableName: "stateStore",
|
|
keyConnectionString: fakeConnectionString,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Act
|
|
err := m.mySQL.Init(t.Context(), *metadata)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
assert.Equal(t, "registerTLSConfigError", err.Error(), "wrong error")
|
|
}
|
|
|
|
func TestInitSetsTableName(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
metadata := &state.Metadata{
|
|
Base: metadata.Base{Properties: map[string]string{keyConnectionString: "", keyTableName: "stateStore"}},
|
|
}
|
|
|
|
// Act
|
|
err := m.mySQL.Init(t.Context(), *metadata)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
assert.Equal(t, "stateStore", m.mySQL.tableName, "table name did not default")
|
|
}
|
|
|
|
func TestInitInvalidTableName(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
metadata := &state.Metadata{
|
|
Base: metadata.Base{Properties: map[string]string{keyConnectionString: "", keyTableName: "🙃"}},
|
|
}
|
|
|
|
// Act
|
|
err := m.mySQL.Init(t.Context(), *metadata)
|
|
|
|
// Assert
|
|
require.ErrorContains(t, err, "table name '🙃' is not valid")
|
|
}
|
|
|
|
func TestInitSetsSchemaName(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
metadata := &state.Metadata{
|
|
Base: metadata.Base{Properties: map[string]string{keyConnectionString: "", keySchemaName: "stateStoreSchema"}},
|
|
}
|
|
|
|
// Act
|
|
err := m.mySQL.Init(t.Context(), *metadata)
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
assert.Equal(t, "stateStoreSchema", m.mySQL.schemaName, "table name did not default")
|
|
}
|
|
|
|
func TestInitInvalidSchemaName(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
metadata := &state.Metadata{
|
|
Base: metadata.Base{Properties: map[string]string{keyConnectionString: "", keySchemaName: "?"}},
|
|
}
|
|
|
|
// Act
|
|
err := m.mySQL.Init(t.Context(), *metadata)
|
|
|
|
// Assert
|
|
require.ErrorContains(t, err, "schema name '?' is not valid")
|
|
}
|
|
|
|
func TestMultiWithNoRequestsDoesNothing(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
var ops []state.TransactionalStateOperation
|
|
|
|
// no operations expected
|
|
m.mock1.ExpectBegin()
|
|
m.mock1.ExpectCommit()
|
|
|
|
// Act
|
|
err := m.mySQL.Multi(t.Context(), &state.TransactionalStateRequest{
|
|
Operations: ops,
|
|
})
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestClosingMySQLWithNilDba(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
m.mySQL.Close()
|
|
|
|
m.mySQL.db = nil
|
|
|
|
// Act
|
|
err := m.mySQL.Close()
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestValidSetRequest(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
|
|
t.Run("single op", func(t *testing.T) {
|
|
ops := []state.TransactionalStateOperation{
|
|
createSetRequest(),
|
|
}
|
|
|
|
m.mock1.ExpectExec("REPLACE INTO").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
// Act
|
|
err := m.mySQL.Multi(t.Context(), &state.TransactionalStateRequest{
|
|
Operations: ops,
|
|
})
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("multiple ops", func(t *testing.T) {
|
|
ops := []state.TransactionalStateOperation{
|
|
createSetRequest(),
|
|
createSetRequest(),
|
|
}
|
|
|
|
m.mock1.ExpectBegin()
|
|
m.mock1.ExpectExec("REPLACE INTO").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
m.mock1.ExpectExec("REPLACE INTO").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
m.mock1.ExpectCommit()
|
|
|
|
// Act
|
|
err := m.mySQL.Multi(t.Context(), &state.TransactionalStateRequest{
|
|
Operations: ops,
|
|
})
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestInvalidMultiSetRequestNoKey(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
ops := []state.TransactionalStateOperation{
|
|
state.SetRequest{
|
|
// empty key is not valid for Upsert operation
|
|
Key: "",
|
|
Value: "value1",
|
|
},
|
|
}
|
|
|
|
// Act
|
|
err := m.mySQL.Multi(t.Context(), &state.TransactionalStateRequest{
|
|
Operations: ops,
|
|
})
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestValidMultiDeleteRequest(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
|
|
t.Run("single op", func(t *testing.T) {
|
|
ops := []state.TransactionalStateOperation{
|
|
createDeleteRequest(),
|
|
}
|
|
|
|
m.mock1.ExpectExec("DELETE FROM").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
// Act
|
|
err := m.mySQL.Multi(t.Context(), &state.TransactionalStateRequest{
|
|
Operations: ops,
|
|
})
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("multiple ops", func(t *testing.T) {
|
|
ops := []state.TransactionalStateOperation{
|
|
createDeleteRequest(),
|
|
createDeleteRequest(),
|
|
}
|
|
|
|
m.mock1.ExpectBegin()
|
|
m.mock1.ExpectExec("DELETE FROM").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
m.mock1.ExpectExec("DELETE FROM").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
m.mock1.ExpectCommit()
|
|
|
|
// Act
|
|
err := m.mySQL.Multi(t.Context(), &state.TransactionalStateRequest{
|
|
Operations: ops,
|
|
})
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestInvalidMultiDeleteRequestNoKey(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
ops := []state.TransactionalStateOperation{
|
|
state.DeleteRequest{
|
|
// empty key is not valid for Delete operation
|
|
Key: "",
|
|
},
|
|
}
|
|
|
|
// Act
|
|
err := m.mySQL.Multi(t.Context(), &state.TransactionalStateRequest{
|
|
Operations: ops,
|
|
})
|
|
|
|
// Assert
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestMultiOperationOrder(t *testing.T) {
|
|
// Arrange
|
|
t.Parallel()
|
|
m, _ := mockDatabase(t)
|
|
|
|
// In a transaction with multiple operations,
|
|
// the order of operations must be respected.
|
|
ops := []state.TransactionalStateOperation{
|
|
state.SetRequest{Key: "k1", Value: "v1"},
|
|
state.DeleteRequest{Key: "k1"},
|
|
state.SetRequest{Key: "k2", Value: "v2"},
|
|
}
|
|
|
|
// expected to run the operations in sequence
|
|
m.mock1.ExpectBegin()
|
|
m.mock1.ExpectExec("REPLACE INTO").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
m.mock1.ExpectExec("DELETE FROM").WithArgs("k1").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
m.mock1.ExpectExec("REPLACE INTO").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
m.mock1.ExpectCommit()
|
|
|
|
// Act
|
|
err := m.mySQL.Multi(t.Context(), &state.TransactionalStateRequest{
|
|
Operations: ops,
|
|
})
|
|
|
|
// Assert
|
|
require.NoError(t, err)
|
|
|
|
err = m.mock1.ExpectationsWereMet()
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func createSetRequest() state.SetRequest {
|
|
return state.SetRequest{
|
|
Key: randomKey(),
|
|
Value: randomJSON(),
|
|
}
|
|
}
|
|
|
|
func createDeleteRequest() state.DeleteRequest {
|
|
return state.DeleteRequest{
|
|
Key: randomKey(),
|
|
}
|
|
}
|
|
|
|
type mocks struct {
|
|
mySQL *MySQL
|
|
db *sql.DB
|
|
mock1 sqlmock.Sqlmock
|
|
mock2 sqlmock.Sqlmock
|
|
factory *fakeMySQLFactory
|
|
}
|
|
|
|
// Returns a MySQL and an extra sql.DB for test that have to close the first
|
|
// db returned in the MySQL instance.
|
|
func mockDatabase(t *testing.T) (*mocks, error) {
|
|
db1, mock1, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
if err != nil {
|
|
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
|
}
|
|
|
|
db2, mock2, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
|
|
if err != nil {
|
|
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
|
|
}
|
|
|
|
fake := newFakeMySQLFactory(db1, db2, nil, nil)
|
|
logger := logger.NewLogger("test")
|
|
mys := newMySQLStateStore(logger, fake)
|
|
mys.db = db1
|
|
mys.tableName = "state"
|
|
mys.connectionString = "theUser:thePassword@/theDBName"
|
|
|
|
return &mocks{
|
|
db: db2,
|
|
mySQL: mys,
|
|
mock1: mock1,
|
|
mock2: mock2,
|
|
factory: fake,
|
|
}, err
|
|
}
|
|
|
|
// Fake for unit testings the part that uses the factory
|
|
// to open the database and register the pem file.
|
|
type fakeMySQLFactory struct {
|
|
openCount int
|
|
openErr error
|
|
registerErr error
|
|
db1 *sql.DB
|
|
db2 *sql.DB
|
|
}
|
|
|
|
// startCount is used to set openCount. openCount is used to determine
|
|
// which fake to open. In the normal flow of code this would go from 1
|
|
// to 2. However, in some tests they assume the factory open has already
|
|
// been called so for those test set startCount to 1.
|
|
func newFakeMySQLFactory(db1 *sql.DB, db2 *sql.DB, registerErr, openErr error) *fakeMySQLFactory {
|
|
return &fakeMySQLFactory{
|
|
db1: db1,
|
|
db2: db2,
|
|
openErr: openErr,
|
|
registerErr: registerErr,
|
|
}
|
|
}
|
|
|
|
func (f *fakeMySQLFactory) Open(connectionString string) (*sql.DB, error) {
|
|
f.openCount++
|
|
|
|
if f.openCount == 1 {
|
|
return f.db1, f.openErr
|
|
}
|
|
|
|
return f.db2, f.openErr
|
|
}
|
|
|
|
func (f *fakeMySQLFactory) RegisterTLSConfig(pemPath string) error {
|
|
return f.registerErr
|
|
}
|
|
|
|
func TestValidIdentifier(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
arg string
|
|
want bool
|
|
}{
|
|
{name: "empty string", arg: "", want: false},
|
|
{name: "valid characters only", arg: "acz_039_AZS", want: true},
|
|
{name: "invalid ASCII characters 1", arg: "$", want: false},
|
|
{name: "invalid ASCII characters 2", arg: "*", want: false},
|
|
{name: "invalid ASCII characters 3", arg: "hello world", want: false},
|
|
{name: "non-ASCII characters", arg: "🙃", want: false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := validIdentifier(tt.arg); got != tt.want {
|
|
t.Errorf("validIdentifier() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|