Files
ResolveSpec/pkg/security/lookup/direct/builder_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

76 lines
2.8 KiB
Go

package direct
import (
"database/sql"
"testing"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup/dialect"
)
type noRun struct{}
func (noRun) Run(func(*sql.DB) error) error { return nil }
func TestBuilderRendersPerDialect(t *testing.T) {
schema := lookup.Schema{lookup.EntityUserSessions: {Schema: "auth", Name: "sessions"}}
cases := map[string]string{
"postgres": `SELECT t0."session_token" FROM "auth"."sessions" t0`,
"mysql": "SELECT t0.`session_token` FROM `auth`.`sessions` t0",
"mssql": `SELECT t0.[session_token] FROM [auth].[sessions] t0`,
}
tails := map[string]string{
"postgres": ` WHERE t0."user_id" = $1 AND t0."session_token" IN ($2, $3)`,
"mysql": " WHERE t0.`user_id` = ? AND t0.`session_token` IN (?, ?)",
"mssql": ` WHERE t0.[user_id] = @p1 AND t0.[session_token] IN (@p2, @p3)`,
}
for name, head := range cases {
d, err := dialect.Get(name)
if err != nil {
t.Fatal(err)
}
b, err := NewBase(noRun{}, d, schema)
if err != nil {
t.Fatal(err)
}
st := b.From(lookup.EntityUserSessions).Cols(lookup.SessionsToken).
Where(Eq(lookup.SessionsUserID, 7), In(lookup.SessionsToken, "a", "b")).Build()
if st.SQL != head+tails[name] || len(st.Args) != 3 {
t.Errorf("%s:\n got %s\n want %s", name, st.SQL, head+tails[name])
}
}
}
func TestBuilderBoolsGoThroughDialect(t *testing.T) {
d, _ := dialect.Get("sqlite")
b, _ := NewBase(noRun{}, d, nil)
st := b.From(lookup.EntityUsers).Cols(lookup.UsersID).Where(Eq(lookup.UsersIsActive, true)).Build()
if st.Args[0] != d.Bool(true) {
t.Fatalf("bool not converted: %#v", st.Args[0])
}
}
func TestBuilderSubselectSharesArguments(t *testing.T) {
d, _ := dialect.Get("postgres")
b, _ := NewBase(noRun{}, d, nil)
sub := b.From(lookup.EntitySecGroupMembers).Cols(lookup.GroupMembersGroupID).Where(Eq(lookup.GroupMembersUserID, 5))
st := b.From(lookup.EntitySecRowRules).Cols(lookup.RowRulesID).
Where(Eq(lookup.RowRulesTableName, "t"), InSelect(lookup.RowRulesGroupID, sub), Eq(lookup.RowRulesSchemaName, "s")).Build()
want := `SELECT t0."id" FROM "sec_row_rules" t0 WHERE t0."table_name" = $1 AND t0."group_id" IN (SELECT t1."group_id" FROM "sec_group_members" t1 WHERE t1."user_id" = $2) AND t0."schema_name" = $3`
if st.SQL != want || len(st.Args) != 3 {
t.Fatalf("got %s\nwant %s", st.SQL, want)
}
}
func TestSchemaRejectsUnsafeIdentifiers(t *testing.T) {
d, _ := dialect.Get("postgres")
bad := lookup.Schema{lookup.EntityUsers: {Name: `users"; DROP TABLE x; --`}}
if _, err := NewBase(noRun{}, d, bad); err == nil {
t.Fatal("unsafe table name accepted")
}
bad = lookup.Schema{lookup.EntityUsers: {Columns: map[string]string{"username": "a b"}}}
if _, err := NewBase(noRun{}, d, bad); err == nil {
t.Fatal("unsafe column name accepted")
}
}