Files
threshold/internal/db/schema/manager_test.go
T

353 lines
9.2 KiB
Go

package schema
import (
"context"
"database/sql"
"sort"
"strings"
"testing"
"github.com/hashicorp/boundary/internal/db/schema/postgres"
"github.com/hashicorp/boundary/internal/docker"
"github.com/hashicorp/boundary/internal/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewManager(t *testing.T) {
dialect := "postgres"
c, u, _, err := docker.StartDbInDocker(dialect)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, c())
})
d, err := sql.Open(dialect, u)
require.NoError(t, err)
ctx := context.Background()
_, err = NewManager(ctx, dialect, d)
require.NoError(t, err)
_, err = NewManager(ctx, "unknown", d)
assert.True(t, errors.Match(errors.T(errors.InvalidParameter), err))
d.Close()
_, err = NewManager(ctx, dialect, d)
assert.True(t, errors.Match(errors.T(errors.Op("schema.NewManager")), err))
}
func TestCurrentState(t *testing.T) {
dialect := "postgres"
c, u, _, err := docker.StartDbInDocker(dialect)
t.Cleanup(func() {
if err := c(); err != nil {
t.Fatalf("Got error at cleanup: %v", err)
}
})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, c())
})
ctx := context.Background()
d, err := sql.Open(dialect, u)
require.NoError(t, err)
m, err := NewManager(ctx, dialect, d)
require.NoError(t, err)
want := &State{
BinarySchemaVersion: BinarySchemaVersion(dialect),
DatabaseSchemaVersion: nilVersion,
}
s, err := m.CurrentState(ctx)
require.NoError(t, err)
assert.Equal(t, want, s)
testDriver, err := postgres.New(ctx, d)
require.NoError(t, err)
require.NoError(t, testDriver.EnsureVersionTable(ctx))
require.NoError(t, testDriver.Run(ctx, strings.NewReader("select 1"), 2))
want = &State{
InitializationStarted: true,
BinarySchemaVersion: BinarySchemaVersion(dialect),
DatabaseSchemaVersion: 2,
}
s, err = m.CurrentState(ctx)
require.NoError(t, err)
assert.Equal(t, want, s)
}
func TestRollForward(t *testing.T) {
dialect := "postgres"
c, u, _, err := docker.StartDbInDocker(dialect)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, c())
})
d, err := sql.Open(dialect, u)
require.NoError(t, err)
ctx := context.Background()
m, err := NewManager(ctx, dialect, d)
require.NoError(t, err)
assert.NoError(t, m.RollForward(ctx))
// Now set to dirty at an early version
_, err = postgres.New(ctx, d)
require.NoError(t, err)
// TODO: Extract out a way to mock the db to test failing rollforwards.
_, err = d.ExecContext(ctx, "TRUNCATE boundary_schema_version; INSERT INTO boundary_schema_version (version, dirty) VALUES (2, true)")
require.NoError(t, err)
assert.Error(t, m.RollForward(ctx))
}
func TestRollForward_NotFromFresh(t *testing.T) {
dialect := "postgres"
oState := migrationStates[dialect]
nState := createPartialMigrationState(oState, 8)
migrationStates[dialect] = nState
c, u, _, err := docker.StartDbInDocker(dialect)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, c())
})
d, err := sql.Open(dialect, u)
require.NoError(t, err)
// Initialize the DB with only a portion of the current sql scripts.
ctx := context.Background()
m, err := NewManager(ctx, dialect, d)
require.NoError(t, err)
assert.NoError(t, m.RollForward(ctx))
state, err := m.CurrentState(ctx)
require.NoError(t, err)
assert.Equal(t, nState.binarySchemaVersion, state.DatabaseSchemaVersion)
assert.False(t, state.Dirty)
// Restore the full set of sql scripts and roll the rest of the way forward.
migrationStates[dialect] = oState
newM, err := NewManager(ctx, dialect, d)
require.NoError(t, err)
assert.NoError(t, newM.RollForward(ctx))
state, err = newM.CurrentState(ctx)
require.NoError(t, err)
assert.Equal(t, oState.binarySchemaVersion, state.DatabaseSchemaVersion)
assert.False(t, state.Dirty)
}
func TestRunMigration_canceledContext(t *testing.T) {
dialect := "postgres"
c, u, _, err := docker.StartDbInDocker(dialect)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, c())
})
d, err := sql.Open(dialect, u)
require.NoError(t, err)
ctx := context.Background()
m, err := NewManager(ctx, dialect, d)
require.NoError(t, err)
// TODO: Find a way to test different parts of the runMigrations loop.
ctx, cancel := context.WithCancel(ctx)
cancel()
assert.Error(t, m.runMigrations(ctx, newStatementProvider(dialect, 0)))
}
func TestRollForward_BadSQL(t *testing.T) {
dialect := "postgres"
oState := migrationStates[dialect]
defer func() { migrationStates[dialect] = oState }()
nState := createPartialMigrationState(oState, 8)
nState.binarySchemaVersion = 10
nState.upMigrations[10] = []byte("SELECT 1 FROM NonExistantTable;")
migrationStates[dialect] = nState
c, u, _, err := docker.StartDbInDocker(dialect)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, c())
})
d, err := sql.Open(dialect, u)
require.NoError(t, err)
// Initialize the DB with only a portion of the current sql scripts.
ctx := context.Background()
m, err := NewManager(ctx, dialect, d)
require.NoError(t, err)
assert.Error(t, m.RollForward(ctx))
state, err := m.CurrentState(ctx)
require.NoError(t, err)
assert.Equal(t, nilVersion, state.DatabaseSchemaVersion)
assert.False(t, state.Dirty)
}
func TestManager_ExclusiveLock(t *testing.T) {
ctx := context.Background()
dialect := "postgres"
c, u, _, err := docker.StartDbInDocker(dialect)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, c())
})
d1, err := sql.Open(dialect, u)
require.NoError(t, err)
m1, err := NewManager(ctx, dialect, d1)
require.NoError(t, err)
d2, err := sql.Open(dialect, u)
require.NoError(t, err)
m2, err := NewManager(ctx, dialect, d2)
require.NoError(t, err)
assert.NoError(t, m1.ExclusiveLock(ctx))
assert.NoError(t, m1.ExclusiveLock(ctx))
assert.Error(t, m2.ExclusiveLock(ctx))
assert.Error(t, m2.SharedLock(ctx))
}
func TestManager_SharedLock(t *testing.T) {
ctx := context.Background()
dialect := "postgres"
c, u, _, err := docker.StartDbInDocker(dialect)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, c())
})
d1, err := sql.Open("postgres", u)
require.NoError(t, err)
m1, err := NewManager(ctx, "postgres", d1)
require.NoError(t, err)
d2, err := sql.Open("postgres", u)
require.NoError(t, err)
m2, err := NewManager(ctx, "postgres", d2)
require.NoError(t, err)
assert.NoError(t, m1.SharedLock(ctx))
assert.NoError(t, m2.SharedLock(ctx))
assert.NoError(t, m1.SharedLock(ctx))
assert.NoError(t, m2.SharedLock(ctx))
assert.Error(t, m1.ExclusiveLock(ctx))
assert.Error(t, m2.ExclusiveLock(ctx))
}
func Test_GetMigrationLog(t *testing.T) {
t.Parallel()
ctx := context.Background()
dialect := "postgres"
c, u, _, err := docker.StartDbInDocker(dialect)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, c())
})
d, err := sql.Open("postgres", u)
require.NoError(t, err)
m, err := NewManager(ctx, "postgres", d)
require.NoError(t, err)
require.NoError(t, m.RollForward(ctx))
const insert = `insert into log_migration(entry) values ($1)`
createEntries := func(entries ...string) {
for _, e := range entries {
_, err := d.Exec(insert, e)
require.NoError(t, err)
}
}
tests := []struct {
name string
d *sql.DB
setup func()
withDeleteLog bool
wantEntries []string
wantErrMatch *errors.Template
}{
{
name: "simple",
d: d,
setup: func() { createEntries("alice", "eve", "bob") },
wantEntries: []string{"alice", "eve", "bob"},
},
{
name: "with-delete-log",
d: d,
setup: func() { createEntries("alice", "eve", "bob") },
withDeleteLog: true,
wantEntries: []string{"alice", "eve", "bob"},
},
{
name: "missing-sql-DB",
wantErrMatch: errors.T(errors.InvalidParameter),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
// start with no log entries
_, err := d.ExecContext(ctx, "truncate log_migration")
require.NoError(err)
if tt.setup != nil {
tt.setup()
}
gotLog, err := GetMigrationLog(ctx, tt.d, WithDeleteLog(tt.withDeleteLog))
if tt.wantErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tt.wantErrMatch, err), "expected error with code: %s and got err: %q", tt.wantErrMatch.Code, err)
return
}
require.NoError(err)
var got []string
for _, e := range gotLog {
got = append(got, e.Entry)
}
sort.Strings(got)
sort.Strings(tt.wantEntries)
assert.Equal(tt.wantEntries, got)
row := d.QueryRowContext(ctx, "select count(*) from log_migration")
require.NoError(row.Err())
var cnt int
require.NoError(row.Scan(&cnt))
if tt.withDeleteLog {
assert.Equal(0, cnt)
} else {
assert.Equal(len(gotLog), cnt)
}
})
}
}
// Creates a new migrationState only with the versions <= the provided maxVer
func createPartialMigrationState(om migrationState, maxVer int) migrationState {
nState := migrationState{
upMigrations: make(map[int][]byte),
}
for k := range om.upMigrations {
if k > maxVer {
// Don't store any versions past our test version.
continue
}
nState.upMigrations[k] = om.upMigrations[k]
if nState.binarySchemaVersion < k {
nState.binarySchemaVersion = k
}
}
return nState
}