package sqlite import ( "context" "database/sql" "errors" "fmt" "go/ast" "go/parser" "go/token" "os" "path/filepath" "strings" "testing" "code.nochebuena.dev/einherjar/contracts/lifecycle" "code.nochebuena.dev/einherjar/contracts/logging" "code.nochebuena.dev/einherjar/contracts/observability" "code.nochebuena.dev/einherjar/core/xerrors" ) // Compile-time interface checks (CT-5 / I-8). var _ lifecycle.Component = (Component)(nil) var _ observability.Checkable = (Component)(nil) var _ Provider = (Component)(nil) // --- CT-6: at most one exported TypeSpec per non-test, non-doc file --- func TestAtMostOneExportedTypePerFile(t *testing.T) { fset := token.NewFileSet() pkgs, err := parser.ParseDir(fset, ".", func(fi os.FileInfo) bool { name := fi.Name() return !strings.HasSuffix(name, "_test.go") && name != "doc.go" }, 0) if err != nil { t.Fatalf("parse: %v", err) } for _, pkg := range pkgs { for path, file := range pkg.Files { base := filepath.Base(path) count := 0 for _, decl := range file.Decls { gd, ok := decl.(*ast.GenDecl) if !ok { continue } for _, spec := range gd.Specs { ts, ok := spec.(*ast.TypeSpec) if ok && ts.Name.IsExported() { count++ } } } if count > 1 { t.Errorf("%s: %d exported TypeSpecs (max 1)", base, count) } } } } // --- Config defaults (S-4) --- func TestDefaultConfig_OptionalFields(t *testing.T) { cfg := DefaultConfig() if cfg.MaxOpenConns == 0 { t.Error("MaxOpenConns must have a default") } if cfg.MaxIdleConns == 0 { t.Error("MaxIdleConns must have a default") } if cfg.Pragmas == "" { t.Error("Pragmas must have a default") } } // --- Config.DSN --- func TestConfig_DSN(t *testing.T) { cfg := Config{Path: "test.db", Pragmas: "?_journal=WAL"} dsn := cfg.DSN() if !strings.Contains(dsn, "test.db") { t.Errorf("DSN %q missing path", dsn) } if !strings.Contains(dsn, "?_journal=WAL") { t.Errorf("DSN %q missing pragmas", dsn) } } // --- 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) { assertXCode(t, HandleError(sql.ErrNoRows), xerrors.ErrNotFound) } func TestHandleError_UniqueConstraint(t *testing.T) { assertXCode(t, HandleError(&fakeSQLiteErr{code: sqliteConstraintUnique}), xerrors.ErrAlreadyExists) } func TestHandleError_PrimaryKey(t *testing.T) { assertXCode(t, HandleError(&fakeSQLiteErr{code: sqliteConstraintPrimaryKey}), xerrors.ErrAlreadyExists) } func TestHandleError_ForeignKey(t *testing.T) { assertXCode(t, HandleError(&fakeSQLiteErr{code: sqliteConstraintForeignKey}), xerrors.ErrInvalidInput) } func TestHandleError_Generic(t *testing.T) { assertXCode(t, HandleError(errors.New("boom")), xerrors.ErrInternal) } // --- New --- func TestNew_NotNil(t *testing.T) { if New(newLogger(), Config{}) == nil { t.Fatal("New returned nil") } } // --- Component metadata --- func TestComponent_Name(t *testing.T) { c := New(newLogger(), Config{}) if c.Name() != "sqlite" { t.Errorf("Name() = %q, want %q", c.Name(), "sqlite") } } func TestComponent_Priority(t *testing.T) { c := New(newLogger(), Config{}) if c.Priority() != observability.LevelCritical { t.Error("Priority() != LevelCritical") } } func TestComponent_OnStop_NilDB(t *testing.T) { c := &sqliteImpl{logger: newLogger()} if err := c.OnStop(); err != nil { t.Errorf("OnStop with nil db: %v", err) } } func TestComponent_Begin_NilDB(t *testing.T) { c := &sqliteImpl{logger: newLogger()} _, err := c.Begin(context.Background()) if err == nil { t.Error("expected error for nil db") } } func TestComponent_GetExecutor_ReturnsDB(t *testing.T) { c := &sqliteImpl{logger: newLogger()} exec := c.GetExecutor(context.Background()) if exec != nil { t.Error("expected nil executor when db is nil") } } func TestComponent_GetExecutor_ReturnsTx(t *testing.T) { tx := &mockTx{} ctx := context.WithValue(context.Background(), ctxTxKey{}, tx) c := &sqliteImpl{logger: newLogger()} got := c.GetExecutor(ctx) if got != tx { t.Error("GetExecutor should return injected Tx from context") } } // --- UnitOfWork --- func TestUnitOfWork_Commit(t *testing.T) { tx := &mockTx{} uow := NewUnitOfWork(newLogger(), &mockProvider{tx: tx}) if err := uow.Do(context.Background(), func(ctx context.Context) error { return nil }); err != nil { t.Fatalf("unexpected error: %v", err) } if !tx.committed { t.Error("expected Commit to be called") } } func TestUnitOfWork_Rollback(t *testing.T) { tx := &mockTx{} uow := NewUnitOfWork(newLogger(), &mockProvider{tx: tx}) _ = uow.Do(context.Background(), func(ctx context.Context) error { return errors.New("fail") }) if !tx.rolledBack { t.Error("expected Rollback to be called") } } func TestUnitOfWork_InjectsExecutor(t *testing.T) { tx := &mockTx{} client := &mockProvider{tx: tx} uow := NewUnitOfWork(newLogger(), client) var got Executor _ = uow.Do(context.Background(), func(ctx context.Context) error { got = client.GetExecutor(ctx) return nil }) if got != tx { t.Error("GetExecutor should return the injected Tx inside Do") } } func TestUnitOfWork_ReturnsBeginError(t *testing.T) { client := &mockProvider{beginErr: errors.New("connection lost")} uow := NewUnitOfWork(newLogger(), client) if err := uow.Do(context.Background(), func(ctx context.Context) error { return nil }); err == nil { t.Error("expected error when Begin fails") } } // --- helpers --- func assertXCode(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 code %s, got %s", want, xe.Code()) } } // fakeSQLiteErr implements the coder interface for error mapping tests. type fakeSQLiteErr struct{ code int } func (e *fakeSQLiteErr) Error() string { return fmt.Sprintf("sqlite error %d", e.code) } func (e *fakeSQLiteErr) Code() int { return e.code } // --- stubs --- type stubLogger struct{} func newLogger() *stubLogger { return &stubLogger{} } func (s *stubLogger) Debug(msg string, args ...any) {} func (s *stubLogger) Info(msg string, args ...any) {} func (s *stubLogger) Warn(msg string, args ...any) {} func (s *stubLogger) Error(msg string, err error, args ...any) {} func (s *stubLogger) With(args ...any) logging.Logger { return s } func (s *stubLogger) WithContext(ctx context.Context) logging.Logger { return s } type mockTx struct{ committed, rolledBack bool } func (m *mockTx) ExecContext(ctx context.Context, q string, args ...any) (sql.Result, error) { return nil, nil } func (m *mockTx) QueryContext(ctx context.Context, q string, args ...any) (*sql.Rows, error) { return nil, nil } func (m *mockTx) QueryRowContext(ctx context.Context, q string, args ...any) *sql.Row { return nil } func (m *mockTx) Commit() error { m.committed = true; return nil } func (m *mockTx) Rollback() error { m.rolledBack = true; return nil } type mockProvider struct { tx *mockTx beginErr error } func (m *mockProvider) Begin(ctx context.Context) (Tx, error) { if m.beginErr != nil { return nil, m.beginErr } return m.tx, nil } func (m *mockProvider) Ping(ctx context.Context) error { return nil } func (m *mockProvider) HandleError(err error) error { return HandleError(err) } func (m *mockProvider) GetExecutor(ctx context.Context) Executor { if tx, ok := ctx.Value(ctxTxKey{}).(Executor); ok { return tx } return m.tx }