Files
ResolveSpec/pkg/security/lookup/backends/backends_test.go
T
Hein c9fa8c60f2 refactor(security): move all database access into pkg/security/lookup
pkg/security no longer contains SQL. Every provider calls a store interface
from lookup, implemented by a procedure backend (Postgres stored procedures,
the default there) and a direct backend (dialect-driven SQL for postgres,
sqlite, mysql and mssql with configurable table and column names).

- add sectypes, lookup, lookup/{dialect,procedure,direct,backends,ddl,conformance}
- split totp and providers sub packages out of the core package
- replace SQLNames/TableNames/QueryMode with lookup.Config (see breaking_changes.md)
- direct backend now covers column/row security and API-key login
- move txsettings SQL to lookup.ApplyTxSettings; remove password.go
- move schema scripts under lookup/, add reference DDL per dialect
- add a shared conformance suite; run it on sqlite, and on Postgres in a
  podman/docker container (RESOLVESPEC_TEST_CONTAINERS=1)
- fix procedure schema bugs found on real Postgres: duplicate p_data
  parameter, JSON null arrays, expires_at timezone casts, passkey list
  GROUP BY, missing resolvespec_passkey_login; accept zone-less timestamps
2026-10-01 13:19:44 +02:00

120 lines
3.5 KiB
Go

package backends
import (
"context"
"database/sql"
"errors"
"testing"
"github.com/DATA-DOG/go-sqlmock"
_ "github.com/glebarez/go-sqlite"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup/ddl"
"github.com/bitechdev/ResolveSpec/pkg/security/sectypes"
)
func sqliteDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
db.SetMaxOpenConns(1)
t.Cleanup(func() { _ = db.Close() })
ddl, err := ddl.SQL("sqlite")
if err != nil {
t.Fatal(err)
}
if _, err := db.Exec(ddl); err != nil {
t.Fatal(err)
}
return db
}
func TestSQLiteDefaultsToDirect(t *testing.T) {
ctx := context.Background()
p, err := New(sqliteDB(t), lookup.Config{}, Options{})
if err != nil {
t.Fatal(err)
}
reg, err := p.Auth.Register(ctx, sectypes.RegisterRequest{Username: "a", Email: "a@x.io", Password: "pw"})
if err != nil {
t.Fatal(err)
}
if _, err := p.Auth.Session(ctx, reg.Token, "authenticate"); err != nil {
t.Fatal(err)
}
if on, err := p.TOTP.Status(ctx, reg.User.UserID); err != nil || on {
t.Fatalf("%v %v", on, err)
}
}
func TestProcedureModeRejectedOnSQLite(t *testing.T) {
_, err := New(sqliteDB(t), lookup.Config{Overrides: map[lookup.Op]lookup.Mode{lookup.OpLogin: lookup.ModeProcedure}}, Options{})
if err == nil {
t.Fatal("expected error")
}
}
func TestCustomSchemaAndUnknownDialect(t *testing.T) {
if _, err := New(sqliteDB(t), lookup.Config{Dialect: "nosuch"}, Options{}); err == nil {
t.Fatal("unknown dialect accepted")
}
bad := lookup.Config{Schema: lookup.Schema{lookup.EntityUsers: {Name: "x; drop"}}}
if _, err := New(sqliteDB(t), bad, Options{}); err == nil {
t.Fatal("unsafe schema accepted")
}
if _, err := New(nil, lookup.Config{}, Options{}); err == nil {
t.Fatal("nil db accepted")
}
}
func TestPostgresDefaultsToProcedure(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
p, err := New(db, lookup.Config{Dialect: lookup.DialectPostgres}, Options{})
if err != nil {
t.Fatal(err)
}
mock.ExpectQuery("resolvespec_totp_get_status").WithArgs(1).
WillReturnRows(sqlmock.NewRows([]string{"p_success", "p_error", "p_enabled"}).AddRow(true, nil, true))
if on, err := p.TOTP.Status(context.Background(), 1); err != nil || !on {
t.Fatalf("%v %v", on, err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestPostgresAutoProbesOnce(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
p, err := New(db, lookup.Config{Dialect: lookup.DialectPostgres, Mode: lookup.ModeAuto}, Options{})
if err != nil {
t.Fatal(err)
}
mock.ExpectQuery("pg_proc").WithArgs("resolvespec_totp_get_status").
WillReturnRows(sqlmock.NewRows([]string{"e"}).AddRow(true))
for i := 0; i < 2; i++ { // second call must reuse the cached probe
mock.ExpectQuery("resolvespec_totp_get_status").WithArgs(1).
WillReturnRows(sqlmock.NewRows([]string{"p_success", "p_error", "p_enabled"}).AddRow(true, nil, true))
if on, err := p.TOTP.Status(context.Background(), 1); err != nil || !on {
t.Fatalf("%v %v", on, err)
}
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestFailed(t *testing.T) {
p := Failed(errors.New("boom"))
if _, err := p.Auth.Login(context.Background(), sectypes.LoginRequest{}); err == nil || err.Error() != "boom" {
t.Fatalf("got %v", err)
}
if _, err := p.Policy.RowSecurity(context.Background(), 1, "s", "t"); err == nil {
t.Fatal("want error")
}
}