Ctrl K
ComunidadGuías…

internal/store/faultydb_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package store

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"errors"
	"io"
	"testing"
	"time"
)

var errQueryFailed = errors.New("the database went away mid-read")

type faults struct {
	columns    int
	values     []driver.Value
	execsPass  int
	beginFails bool
	commitErr  error
	closeErr   error
	readErr    error
}

type faultyConnector struct {
	arm    faults
	execs  *int
	closes *int
}

func (c faultyConnector) Connect(context.Context) (driver.Conn, error) {
	return faultyConn(c), nil
}

func (c faultyConnector) Driver() driver.Driver {
	return faultyDriver{}
}

type faultyDriver struct{}

func (faultyDriver) Open(string) (driver.Conn, error) {
	return faultyConn{}, nil
}

type faultyConn struct {
	arm    faults
	execs  *int
	closes *int
}

func (c faultyConn) Prepare(string) (driver.Stmt, error) {
	return faultyStmt{arm: c.arm, execs: c.execs}, nil
}

func (c faultyConn) Close() error {
	if c.closes != nil {
		*c.closes++
	}
	return c.arm.closeErr
}

func (c faultyConn) Begin() (driver.Tx, error) {
	if c.arm.beginFails {
		return nil, errQueryFailed
	}
	return faultyTx{arm: c.arm}, nil
}

type faultyTx struct {
	arm faults
}

func (t faultyTx) Commit() error {
	return t.arm.commitErr
}

func (faultyTx) Rollback() error {
	return nil
}

type faultyStmt struct {
	arm   faults
	execs *int
}

func (faultyStmt) Close() error {
	return nil
}

func (faultyStmt) NumInput() int {
	return -1
}

func (s faultyStmt) Exec([]driver.Value) (driver.Result, error) {
	*s.execs++
	if *s.execs <= s.arm.execsPass {
		return driver.RowsAffected(1), nil
	}
	return nil, errQueryFailed
}

func (s faultyStmt) Query([]driver.Value) (driver.Rows, error) {
	return &faultyRows{columns: s.arm.columns, values: s.arm.values, readErr: s.arm.readErr}, nil
}

type faultyRows struct {
	columns int
	values  []driver.Value
	served  bool
	readErr error
}

func (r *faultyRows) Columns() []string {
	names := make([]string, r.columns)
	for at := range names {
		names[at] = "c"
	}
	return names
}

func (*faultyRows) Close() error {
	return nil
}

func (r *faultyRows) Next(dest []driver.Value) error {
	if r.values == nil {
		return errQueryFailed
	}
	if r.served {
		if r.readErr != nil {
			return r.readErr
		}
		return io.EOF
	}
	r.served = true
	copy(dest, r.values)
	return nil
}

func faultyDB(t *testing.T, arm faults) *sql.DB {
	t.Helper()
	execs := 0
	db := sql.OpenDB(faultyConnector{arm: arm, execs: &execs})
	t.Cleanup(func() { _ = db.Close() })
	return db
}

func faultyStore(t *testing.T, columns int, values []driver.Value) *Store {
	t.Helper()
	db := faultyDB(t, faults{columns: columns, values: values})
	return &Store{writes: db, reads: db}
}

func TestAWriteThatFailsIsPassedUp(t *testing.T) {
	store := faultyStore(t, 0, nil)
	tabs := store.Tabs()
	ctx := context.Background()

	cases := map[string]func() error{
		"remember":  func() error { return tabs.Remember(ctx, Tab{ID: "one"}) },
		"forget":    func() error { return tabs.Forget(ctx, "one") },
		"recolor":   func() error { return tabs.Recolor(ctx, "one", 3) },
		"rename":    func() error { return tabs.Rename(ctx, "one", "label", "group") },
		"reopening": func() error { return tabs.Reopening(ctx, "one", true) },
		"recording": func() error { return tabs.Recording(ctx, "one", "pods") },
		"record":    func() error { return store.record(ctx, Entry{Name: "web"}) },
		"forgetAll": func() error { return store.Forget(ctx, "one") },
		"prune":     func() error { return store.Prune(ctx, Retention{Days: 7}, time.Unix(1700000000, 0)) },
	}
	for name, call := range cases {
		t.Run(name, func(t *testing.T) {
			err := call()
			if !errors.Is(err, errQueryFailed) {
				t.Fatalf("err = %v, want the driver failure passed up", err)
			}
		})
	}
}

func TestAReadThatBreaksPartWayThroughIsReported(t *testing.T) {
	ctx := context.Background()

	cases := map[string]struct {
		columns int
		read    func(*Store) error
	}{
		"tabs": {columns: 9, read: func(s *Store) error {
			_, err := s.Tabs().All(ctx)
			return err
		}},
		"entries": {columns: 13, read: func(s *Store) error {
			_, err := s.Recent(ctx, Query{})
			return err
		}},
		"changes": {columns: 12, read: func(s *Store) error {
			_, err := s.Changed(ctx, Query{Cluster: "one"})
			return err
		}},
	}
	for name, one := range cases {
		t.Run(name, func(t *testing.T) {
			err := one.read(faultyStore(t, one.columns, nil))
			if !errors.Is(err, errQueryFailed) {
				t.Fatalf("err = %v, want the read failure reported", err)
			}
		})
	}
}

func TestARowThatWillNotScanIsReported(t *testing.T) {
	ctx := context.Background()

	cases := map[string]struct {
		columns int
		values  []driver.Value
		read    func(*Store) error
	}{
		"tabs": {
			columns: 9, values: []driver.Value{"id", "ctx", "path", "not a number", int64(0), "", "", false, ""},
			read: func(s *Store) error {
				_, err := s.Tabs().All(ctx)
				return err
			},
		},
		"entries": {
			columns: 13, values: []driver.Value{int64(1), "one", "not a number", "get", "", "", "", "", "", "web", "", "done", ""},
			read: func(s *Store) error {
				_, err := s.Recent(ctx, Query{})
				return err
			},
		},
		"changes": {
			columns: 12, values: []driver.Value{int64(1), "one", "not a number", "get", "", "", "", "", "", "web", "uid", ""},
			read: func(s *Store) error {
				_, err := s.Changed(ctx, Query{Cluster: "one"})
				return err
			},
		},
	}
	for name, one := range cases {
		t.Run(name, func(t *testing.T) {
			err := one.read(faultyStore(t, one.columns, one.values))
			if err == nil {
				t.Fatal("a row that would not scan was reported as read")
			}
		})
	}
}

func TestForgettingAClusterReportsTheSecondWriteFailing(t *testing.T) {
	store := &Store{writes: faultyDB(t, faults{execsPass: 1}), reads: nil}

	err := store.Forget(context.Background(), "one")

	if !errors.Is(err, errQueryFailed) {
		t.Fatalf("err = %v, want the second delete's failure", err)
	}
}

func TestForgettingAClusterReportsTheCommitFailing(t *testing.T) {
	store := &Store{writes: faultyDB(t, faults{execsPass: 2, commitErr: errQueryFailed})}

	err := store.Forget(t.Context(), "one")

	if !errors.Is(err, errQueryFailed) {
		t.Fatalf("err = %v, want the commit failure", err)
	}
}

func TestCappingRowsReportsTheDeleteFailing(t *testing.T) {
	db := faultyDB(t, faults{columns: 1, values: []driver.Value{int64(42)}})
	store := &Store{writes: db, reads: db}

	err := store.Prune(context.Background(), Retention{Rows: 10}, time.Unix(1700000000, 0))

	if !errors.Is(err, errQueryFailed) {
		t.Fatalf("err = %v, want the cut-off delete's failure", err)
	}
}

func TestNotingChangesReportsWhatTheTransactionDid(t *testing.T) {
	change := Change{Cluster: "one", Verb: "update", Name: "web", Cells: []string{"web", "1/1"}}
	ctx := context.Background()

	cases := map[string]faults{
		"the transaction would not start": {beginFails: true},
		"a row would not insert":          {},
		"the commit failed":               {execsPass: 1, commitErr: errQueryFailed},
	}
	for name, arm := range cases {
		t.Run(name, func(t *testing.T) {
			store := &Store{writes: faultyDB(t, arm)}

			err := store.note(ctx, []Change{change})

			if !errors.Is(err, errQueryFailed) {
				t.Fatalf("err = %v, want %s reported", err, name)
			}
		})
	}
}

func TestApplyingAMigrationReportsWhatTheTransactionDid(t *testing.T) {
	ctx := context.Background()

	cases := map[string]faults{
		"the transaction would not start": {beginFails: true},
		"the statements would not run":    {},
		"the version would not stick":     {execsPass: 1},
		"the commit failed":               {execsPass: 2, commitErr: errQueryFailed},
	}
	for name, arm := range cases {
		t.Run(name, func(t *testing.T) {
			err := apply(ctx, faultyDB(t, arm), "CREATE TABLE t (a)", 1)

			if !errors.Is(err, errQueryFailed) {
				t.Fatalf("err = %v, want %s reported", err, name)
			}
		})
	}
}

func TestClosingReportsADatabaseThatWouldNotClose(t *testing.T) {
	readCloses := 0
	writeCloses := 0
	reads := sql.OpenDB(faultyConnector{
		arm: faults{closeErr: errQueryFailed}, execs: new(int), closes: &readCloses,
	})
	writes := sql.OpenDB(faultyConnector{execs: new(int), closes: &writeCloses})
	if err := reads.Ping(); err != nil {
		t.Fatalf("open reads: %v", err)
	}
	if err := writes.Ping(); err != nil {
		t.Fatalf("open writes: %v", err)
	}
	store := &Store{writes: writes, reads: reads}

	err := store.Close()

	if !errors.Is(err, errQueryFailed) {
		t.Fatalf("err = %v, want the close failure passed up", err)
	}
	if readCloses != 1 || writeCloses != 1 {
		t.Fatalf("close calls = reads %d, writes %d, want both pools closed", readCloses, writeCloses)
	}
}