271 lines
7.2 KiB
Go
271 lines
7.2 KiB
Go
|
|
package sqlite
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"database/sql"
|
||
|
|
"errors"
|
||
|
|
"sync"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"code.nochebuena.dev/go/health"
|
||
|
|
"code.nochebuena.dev/go/logz"
|
||
|
|
"code.nochebuena.dev/go/xerrors"
|
||
|
|
)
|
||
|
|
|
||
|
|
func newLogger() logz.Logger { return logz.New(logz.Options{}) }
|
||
|
|
|
||
|
|
func newMemDB(t *testing.T) Component {
|
||
|
|
t.Helper()
|
||
|
|
c := New(newLogger(), Config{Path: ":memory:", MaxOpenConns: 1, MaxIdleConns: 1,
|
||
|
|
Pragmas: "?_fk=true"})
|
||
|
|
if err := c.OnInit(); err != nil {
|
||
|
|
t.Fatalf("OnInit: %v", err)
|
||
|
|
}
|
||
|
|
return c
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- New / name / priority ---
|
||
|
|
|
||
|
|
func TestNew(t *testing.T) {
|
||
|
|
if New(newLogger(), Config{Path: ":memory:"}) == nil {
|
||
|
|
t.Fatal("New returned nil")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_Name(t *testing.T) {
|
||
|
|
c := New(newLogger(), Config{Path: ":memory:"}).(health.Checkable)
|
||
|
|
if c.Name() != "sqlite" {
|
||
|
|
t.Errorf("want sqlite, got %s", c.Name())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_Priority(t *testing.T) {
|
||
|
|
c := New(newLogger(), Config{Path: ":memory:"}).(health.Checkable)
|
||
|
|
if c.Priority() != health.LevelCritical {
|
||
|
|
t.Error("Priority() != LevelCritical")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_OnStop_NilDB(t *testing.T) {
|
||
|
|
c := &sqliteComponent{logger: newLogger()}
|
||
|
|
if err := c.OnStop(); err != nil {
|
||
|
|
t.Errorf("OnStop with nil db: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_FullLifecycle(t *testing.T) {
|
||
|
|
c := New(newLogger(), Config{Path: ":memory:", Pragmas: ""})
|
||
|
|
if err := c.OnInit(); err != nil {
|
||
|
|
t.Fatalf("OnInit: %v", err)
|
||
|
|
}
|
||
|
|
if err := c.OnStart(); err != nil {
|
||
|
|
t.Fatalf("OnStart: %v", err)
|
||
|
|
}
|
||
|
|
if err := c.OnStop(); err != nil {
|
||
|
|
t.Fatalf("OnStop: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Exec / Query / QueryRow ---
|
||
|
|
|
||
|
|
func TestComponent_Exec(t *testing.T) {
|
||
|
|
c := newMemDB(t)
|
||
|
|
exec := c.GetExecutor(context.Background())
|
||
|
|
_, err := exec.ExecContext(context.Background(),
|
||
|
|
"CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("create table: %v", err)
|
||
|
|
}
|
||
|
|
res, err := exec.ExecContext(context.Background(), "INSERT INTO t VALUES (1, 'hello')")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("insert: %v", err)
|
||
|
|
}
|
||
|
|
n, _ := res.RowsAffected()
|
||
|
|
if n != 1 {
|
||
|
|
t.Errorf("want 1 row affected, got %d", n)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_Query(t *testing.T) {
|
||
|
|
c := newMemDB(t)
|
||
|
|
exec := c.GetExecutor(context.Background())
|
||
|
|
_, _ = exec.ExecContext(context.Background(), "CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)")
|
||
|
|
_, _ = exec.ExecContext(context.Background(), "INSERT INTO t VALUES (1, 'hello')")
|
||
|
|
|
||
|
|
rows, err := exec.QueryContext(context.Background(), "SELECT name FROM t")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("query: %v", err)
|
||
|
|
}
|
||
|
|
defer rows.Close()
|
||
|
|
if !rows.Next() {
|
||
|
|
t.Fatal("expected a row")
|
||
|
|
}
|
||
|
|
var name string
|
||
|
|
if err := rows.Scan(&name); err != nil {
|
||
|
|
t.Fatalf("scan: %v", err)
|
||
|
|
}
|
||
|
|
if name != "hello" {
|
||
|
|
t.Errorf("want hello, got %s", name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestComponent_QueryRow(t *testing.T) {
|
||
|
|
c := newMemDB(t)
|
||
|
|
exec := c.GetExecutor(context.Background())
|
||
|
|
_, _ = exec.ExecContext(context.Background(), "CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT)")
|
||
|
|
_, _ = exec.ExecContext(context.Background(), "INSERT INTO t VALUES (1, 'world')")
|
||
|
|
var val string
|
||
|
|
if err := exec.QueryRowContext(context.Background(), "SELECT val FROM t WHERE id=1").Scan(&val); err != nil {
|
||
|
|
t.Fatalf("scan: %v", err)
|
||
|
|
}
|
||
|
|
if val != "world" {
|
||
|
|
t.Errorf("want world, got %s", val)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- HandleError ---
|
||
|
|
|
||
|
|
func TestHandleError_Nil(t *testing.T) {
|
||
|
|
if err := HandleError(nil); err != nil {
|
||
|
|
t.Errorf("want nil, got %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandleError_NoRows(t *testing.T) {
|
||
|
|
assertCode(t, HandleError(sql.ErrNoRows), xerrors.ErrNotFound)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandleError_Generic(t *testing.T) {
|
||
|
|
assertCode(t, HandleError(errors.New("oops")), xerrors.ErrInternal)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandleError_UniqueConstraint(t *testing.T) {
|
||
|
|
c := newMemDB(t)
|
||
|
|
exec := c.GetExecutor(context.Background())
|
||
|
|
_, _ = exec.ExecContext(context.Background(), "CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||
|
|
_, _ = exec.ExecContext(context.Background(), "INSERT INTO t VALUES (1)")
|
||
|
|
_, err := exec.ExecContext(context.Background(), "INSERT INTO t VALUES (1)")
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("expected unique constraint error")
|
||
|
|
}
|
||
|
|
mapped := HandleError(err)
|
||
|
|
assertCode(t, mapped, xerrors.ErrAlreadyExists)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandleError_ForeignKey(t *testing.T) {
|
||
|
|
c := newMemDB(t)
|
||
|
|
exec := c.GetExecutor(context.Background())
|
||
|
|
_, _ = exec.ExecContext(context.Background(),
|
||
|
|
"CREATE TABLE parent (id INTEGER PRIMARY KEY)")
|
||
|
|
_, _ = exec.ExecContext(context.Background(),
|
||
|
|
"CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id))")
|
||
|
|
_, err := exec.ExecContext(context.Background(),
|
||
|
|
"INSERT INTO child VALUES (1, 999)")
|
||
|
|
if err == nil {
|
||
|
|
t.Fatal("expected foreign key error")
|
||
|
|
}
|
||
|
|
mapped := HandleError(err)
|
||
|
|
assertCode(t, mapped, xerrors.ErrInvalidInput)
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- UnitOfWork ---
|
||
|
|
|
||
|
|
func TestUnitOfWork_Commit(t *testing.T) {
|
||
|
|
c := newMemDB(t)
|
||
|
|
exec := c.GetExecutor(context.Background())
|
||
|
|
_, _ = exec.ExecContext(context.Background(), "CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||
|
|
|
||
|
|
uow := NewUnitOfWork(newLogger(), c)
|
||
|
|
err := uow.Do(context.Background(), func(ctx context.Context) error {
|
||
|
|
e := c.GetExecutor(ctx)
|
||
|
|
_, err := e.ExecContext(ctx, "INSERT INTO t VALUES (42)")
|
||
|
|
return err
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Do: %v", err)
|
||
|
|
}
|
||
|
|
// verify committed
|
||
|
|
var id int
|
||
|
|
_ = exec.QueryRowContext(context.Background(), "SELECT id FROM t WHERE id=42").Scan(&id)
|
||
|
|
if id != 42 {
|
||
|
|
t.Error("row not committed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUnitOfWork_Rollback(t *testing.T) {
|
||
|
|
c := newMemDB(t)
|
||
|
|
exec := c.GetExecutor(context.Background())
|
||
|
|
_, _ = exec.ExecContext(context.Background(), "CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||
|
|
|
||
|
|
uow := NewUnitOfWork(newLogger(), c)
|
||
|
|
_ = uow.Do(context.Background(), func(ctx context.Context) error {
|
||
|
|
e := c.GetExecutor(ctx)
|
||
|
|
_, _ = e.ExecContext(ctx, "INSERT INTO t VALUES (99)")
|
||
|
|
return errors.New("abort")
|
||
|
|
})
|
||
|
|
var count int
|
||
|
|
_ = exec.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM t").Scan(&count)
|
||
|
|
if count != 0 {
|
||
|
|
t.Error("row should have been rolled back")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUnitOfWork_InjectsExecutor(t *testing.T) {
|
||
|
|
c := newMemDB(t)
|
||
|
|
uow := NewUnitOfWork(newLogger(), c)
|
||
|
|
var got Executor
|
||
|
|
_ = uow.Do(context.Background(), func(ctx context.Context) error {
|
||
|
|
got = c.GetExecutor(ctx)
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
if got == nil {
|
||
|
|
t.Error("GetExecutor should return a Tx inside Do")
|
||
|
|
}
|
||
|
|
// Outside Do, should return the pool
|
||
|
|
pool := c.GetExecutor(context.Background())
|
||
|
|
if got == pool {
|
||
|
|
t.Error("inside Do should be a Tx, not the pool")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUnitOfWork_WriteMutex(t *testing.T) {
|
||
|
|
c := newMemDB(t)
|
||
|
|
exec := c.GetExecutor(context.Background())
|
||
|
|
_, _ = exec.ExecContext(context.Background(), "CREATE TABLE t (id INTEGER PRIMARY KEY)")
|
||
|
|
|
||
|
|
uow := NewUnitOfWork(newLogger(), c)
|
||
|
|
var wg sync.WaitGroup
|
||
|
|
for i := 0; i < 5; i++ {
|
||
|
|
wg.Add(1)
|
||
|
|
i := i
|
||
|
|
go func() {
|
||
|
|
defer wg.Done()
|
||
|
|
_ = uow.Do(context.Background(), func(ctx context.Context) error {
|
||
|
|
e := c.GetExecutor(ctx)
|
||
|
|
_, err := e.ExecContext(ctx, "INSERT INTO t VALUES (?)", i)
|
||
|
|
return err
|
||
|
|
})
|
||
|
|
}()
|
||
|
|
}
|
||
|
|
wg.Wait()
|
||
|
|
|
||
|
|
var count int
|
||
|
|
_ = exec.QueryRowContext(context.Background(), "SELECT COUNT(*) FROM t").Scan(&count)
|
||
|
|
if count != 5 {
|
||
|
|
t.Errorf("expected 5 rows, got %d", count)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- helpers ---
|
||
|
|
|
||
|
|
func assertCode(t *testing.T, err error, want xerrors.Code) {
|
||
|
|
t.Helper()
|
||
|
|
var xe *xerrors.Err
|
||
|
|
if !errors.As(err, &xe) {
|
||
|
|
t.Fatalf("expected *xerrors.Err, got %T: %v", err, err)
|
||
|
|
}
|
||
|
|
if xe.Code() != want {
|
||
|
|
t.Errorf("want %s, got %s", want, xe.Code())
|
||
|
|
}
|
||
|
|
}
|