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()) } }