From 189c72b036e911a6d67f1bd4125f815fe497a863 Mon Sep 17 00:00:00 2001 From: aman Date: Fri, 17 Jul 2026 12:55:56 +0530 Subject: [PATCH 1/5] fix(store): return transaction errors from rql search repositories WithTxn's return value was discarded in the org billing, org projects, org users, and project users search methods. A failed transaction begin or commit returned empty results with no error. Co-Authored-By: Claude Fable 5 --- internal/store/postgres/org_billing_repository.go | 2 +- internal/store/postgres/org_projects_repository.go | 2 +- internal/store/postgres/org_users_repository.go | 2 +- internal/store/postgres/project_users_repository.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/store/postgres/org_billing_repository.go b/internal/store/postgres/org_billing_repository.go index 26bc6be5c..5a75820ac 100644 --- a/internal/store/postgres/org_billing_repository.go +++ b/internal/store/postgres/org_billing_repository.go @@ -141,7 +141,7 @@ func (r OrgBillingRepository) Search(ctx context.Context, rql *rql.Query) (svc.O ReadOnly: true, } - r.dbc.WithTxn(ctx, txOpts, func(tx *sqlx.Tx) error { + err = r.dbc.WithTxn(ctx, txOpts, func(tx *sqlx.Tx) error { err = r.dbc.WithTimeout(ctx, TABLE_ORGANIZATIONS, "GetOrgBilling", func(ctx context.Context) error { return tx.SelectContext(ctx, &orgBilling, dataQuery, params...) }) diff --git a/internal/store/postgres/org_projects_repository.go b/internal/store/postgres/org_projects_repository.go index 16faa63b8..bd63e922a 100644 --- a/internal/store/postgres/org_projects_repository.go +++ b/internal/store/postgres/org_projects_repository.go @@ -84,7 +84,7 @@ func (r OrgProjectsRepository) Search(ctx context.Context, orgID string, rql *rq ReadOnly: true, } - r.dbc.WithTxn(ctx, txOpts, func(tx *sqlx.Tx) error { + err = r.dbc.WithTxn(ctx, txOpts, func(tx *sqlx.Tx) error { err = r.dbc.WithTimeout(ctx, TABLE_PROJECTS, "CountProjectMembers", func(ctx context.Context) error { return tx.SelectContext(ctx, &orgProjects, dataQuery, params...) }) diff --git a/internal/store/postgres/org_users_repository.go b/internal/store/postgres/org_users_repository.go index 028ea4d2e..8e8838922 100644 --- a/internal/store/postgres/org_users_repository.go +++ b/internal/store/postgres/org_users_repository.go @@ -112,7 +112,7 @@ func (r OrgUsersRepository) Search(ctx context.Context, orgID string, rql *rql.Q ReadOnly: true, } - r.dbc.WithTxn(ctx, txOpts, func(tx *sqlx.Tx) error { + err = r.dbc.WithTxn(ctx, txOpts, func(tx *sqlx.Tx) error { err = r.dbc.WithTimeout(ctx, TABLE_POLICIES, "GetOrgUsers", func(ctx context.Context) error { return tx.SelectContext(ctx, &orgUsers, dataQuery, params...) }) diff --git a/internal/store/postgres/project_users_repository.go b/internal/store/postgres/project_users_repository.go index 5ded89002..a34171b0b 100644 --- a/internal/store/postgres/project_users_repository.go +++ b/internal/store/postgres/project_users_repository.go @@ -73,7 +73,7 @@ func (r ProjectUsersRepository) Search(ctx context.Context, projectID string, rq ReadOnly: true, } - r.dbc.WithTxn(ctx, txOpts, func(tx *sqlx.Tx) error { + err = r.dbc.WithTxn(ctx, txOpts, func(tx *sqlx.Tx) error { err = r.dbc.WithTimeout(ctx, TABLE_POLICIES, "GetProjectUsers", func(ctx context.Context) error { return tx.SelectContext(ctx, &projectUsers, dataQuery, params...) }) From 6b189a804c9b4d4582d75d3aa18856eac92322dc Mon Sep 17 00:00:00 2001 From: aman Date: Fri, 17 Jul 2026 14:59:40 +0530 Subject: [PATCH 2/5] test(store): cover transaction errors in rql search repositories A canceled context makes the transaction begin fail. Each Search method must return that error instead of an empty result. Co-Authored-By: Claude Fable 5 --- .../postgres/rql_search_repository_test.go | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 internal/store/postgres/rql_search_repository_test.go diff --git a/internal/store/postgres/rql_search_repository_test.go b/internal/store/postgres/rql_search_repository_test.go new file mode 100644 index 000000000..7b69d23a0 --- /dev/null +++ b/internal/store/postgres/rql_search_repository_test.go @@ -0,0 +1,70 @@ +package postgres_test + +import ( + "context" + "io" + "log/slog" + "testing" + + "github.com/ory/dockertest" + "github.com/raystack/frontier/internal/store/postgres" + "github.com/raystack/frontier/pkg/db" + "github.com/raystack/salt/rql" + "github.com/stretchr/testify/suite" +) + +// The rql search repositories must surface transaction failures instead of +// returning an empty result with a nil error. A canceled context makes the +// transaction begin fail before any query runs. +type RQLSearchRepositoryTestSuite struct { + suite.Suite + client *db.Client + pool *dockertest.Pool + resource *dockertest.Resource +} + +func (s *RQLSearchRepositoryTestSuite) SetupSuite() { + var err error + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + s.client, s.pool, s.resource, err = newTestClient(logger) + if err != nil { + s.T().Fatal(err) + } +} + +func (s *RQLSearchRepositoryTestSuite) TearDownSuite() { + if err := purgeDocker(s.pool, s.resource); err != nil { + s.T().Fatal(err) + } +} + +func canceledContext() context.Context { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + return ctx +} + +func (s *RQLSearchRepositoryTestSuite) TestOrgBillingSearchReturnsTxnError() { + _, err := postgres.NewOrgBillingRepository(s.client).Search(canceledContext(), &rql.Query{Limit: 10}) + s.Error(err) +} + +func (s *RQLSearchRepositoryTestSuite) TestOrgProjectsSearchReturnsTxnError() { + _, err := postgres.NewOrgProjectsRepository(s.client).Search(canceledContext(), "00000000-0000-0000-0000-000000000000", &rql.Query{Limit: 10}) + s.Error(err) +} + +func (s *RQLSearchRepositoryTestSuite) TestOrgUsersSearchReturnsTxnError() { + _, err := postgres.NewOrgUsersRepository(s.client).Search(canceledContext(), "00000000-0000-0000-0000-000000000000", &rql.Query{Limit: 10}) + s.Error(err) +} + +func (s *RQLSearchRepositoryTestSuite) TestProjectUsersSearchReturnsTxnError() { + _, err := postgres.NewProjectUsersRepository(s.client).Search(canceledContext(), "00000000-0000-0000-0000-000000000000", &rql.Query{Limit: 10}) + s.Error(err) +} + +func TestRQLSearchRepository(t *testing.T) { + suite.Run(t, new(RQLSearchRepositoryTestSuite)) +} From 2dde55c6d2270cec1422fe89067a4c19e7759d40 Mon Sep 17 00:00:00 2001 From: aman Date: Fri, 17 Jul 2026 15:13:53 +0530 Subject: [PATCH 3/5] test(store): move search transaction-error tests into repository test files Each repository test file gets a Search case using a client whose connections always fail, so no database container is needed. Co-Authored-By: Claude Fable 5 --- .../postgres/org_billing_repository_test.go | 28 ++++++++ .../postgres/org_projects_repository_test.go | 9 +++ .../postgres/org_users_repository_test.go | 9 +++ .../postgres/project_users_repository_test.go | 9 +++ .../postgres/rql_search_repository_test.go | 70 ------------------- 5 files changed, 55 insertions(+), 70 deletions(-) delete mode 100644 internal/store/postgres/rql_search_repository_test.go diff --git a/internal/store/postgres/org_billing_repository_test.go b/internal/store/postgres/org_billing_repository_test.go index 96eb6cfdd..374bace95 100644 --- a/internal/store/postgres/org_billing_repository_test.go +++ b/internal/store/postgres/org_billing_repository_test.go @@ -1,8 +1,14 @@ package postgres import ( + "context" + "database/sql" + "database/sql/driver" + "errors" "testing" + "github.com/jmoiron/sqlx" + "github.com/raystack/frontier/pkg/db" "github.com/raystack/salt/rql" "github.com/stretchr/testify/assert" ) @@ -216,3 +222,25 @@ func TestPrepareGroupByQuery(t *testing.T) { }) } } + +// txnFailClient returns a client whose connections always fail, so any +// transaction begin returns an error. +func txnFailClient() *db.Client { + return &db.Client{DB: sqlx.NewDb(sql.OpenDB(failConnector{}), "postgres")} +} + +type failConnector struct{} + +func (failConnector) Connect(context.Context) (driver.Conn, error) { + return nil, errors.New("connection failed") +} + +func (failConnector) Driver() driver.Driver { return nil } + +func TestOrgBillingRepository_Search(t *testing.T) { + t.Run("should return error when the transaction cannot start", func(t *testing.T) { + repo := NewOrgBillingRepository(txnFailClient()) + _, err := repo.Search(context.Background(), &rql.Query{Limit: 10}) + assert.Error(t, err) + }) +} diff --git a/internal/store/postgres/org_projects_repository_test.go b/internal/store/postgres/org_projects_repository_test.go index d3681cac1..35342a377 100644 --- a/internal/store/postgres/org_projects_repository_test.go +++ b/internal/store/postgres/org_projects_repository_test.go @@ -1,6 +1,7 @@ package postgres import ( + "context" "testing" "github.com/raystack/salt/rql" @@ -125,3 +126,11 @@ func TestOrgProjectsRepository_prepareDataQuery(t *testing.T) { }) } } + +func TestOrgProjectsRepository_Search(t *testing.T) { + t.Run("should return error when the transaction cannot start", func(t *testing.T) { + repo := NewOrgProjectsRepository(txnFailClient()) + _, err := repo.Search(context.Background(), "org-id", &rql.Query{Limit: 10}) + assert.Error(t, err) + }) +} diff --git a/internal/store/postgres/org_users_repository_test.go b/internal/store/postgres/org_users_repository_test.go index b9434fbb8..4e5d7a8a4 100644 --- a/internal/store/postgres/org_users_repository_test.go +++ b/internal/store/postgres/org_users_repository_test.go @@ -1,6 +1,7 @@ package postgres import ( + "context" "testing" "strings" @@ -306,3 +307,11 @@ func TestOrgUsersRepository_BuildRoleFilterCondition(t *testing.T) { }) } } + +func TestOrgUsersRepository_Search(t *testing.T) { + t.Run("should return error when the transaction cannot start", func(t *testing.T) { + repo := NewOrgUsersRepository(txnFailClient()) + _, err := repo.Search(context.Background(), "org-id", &rql.Query{Limit: 10}) + assert.Error(t, err) + }) +} diff --git a/internal/store/postgres/project_users_repository_test.go b/internal/store/postgres/project_users_repository_test.go index e53580e95..77ac70083 100644 --- a/internal/store/postgres/project_users_repository_test.go +++ b/internal/store/postgres/project_users_repository_test.go @@ -1,6 +1,7 @@ package postgres import ( + "context" "testing" "github.com/raystack/salt/rql" @@ -58,3 +59,11 @@ func TestProjectUsersRepository_PrepareDataQuery(t *testing.T) { }) } } + +func TestProjectUsersRepository_Search(t *testing.T) { + t.Run("should return error when the transaction cannot start", func(t *testing.T) { + repo := NewProjectUsersRepository(txnFailClient()) + _, err := repo.Search(context.Background(), "project-123", &rql.Query{Limit: 10}) + assert.Error(t, err) + }) +} diff --git a/internal/store/postgres/rql_search_repository_test.go b/internal/store/postgres/rql_search_repository_test.go deleted file mode 100644 index 7b69d23a0..000000000 --- a/internal/store/postgres/rql_search_repository_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package postgres_test - -import ( - "context" - "io" - "log/slog" - "testing" - - "github.com/ory/dockertest" - "github.com/raystack/frontier/internal/store/postgres" - "github.com/raystack/frontier/pkg/db" - "github.com/raystack/salt/rql" - "github.com/stretchr/testify/suite" -) - -// The rql search repositories must surface transaction failures instead of -// returning an empty result with a nil error. A canceled context makes the -// transaction begin fail before any query runs. -type RQLSearchRepositoryTestSuite struct { - suite.Suite - client *db.Client - pool *dockertest.Pool - resource *dockertest.Resource -} - -func (s *RQLSearchRepositoryTestSuite) SetupSuite() { - var err error - - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - s.client, s.pool, s.resource, err = newTestClient(logger) - if err != nil { - s.T().Fatal(err) - } -} - -func (s *RQLSearchRepositoryTestSuite) TearDownSuite() { - if err := purgeDocker(s.pool, s.resource); err != nil { - s.T().Fatal(err) - } -} - -func canceledContext() context.Context { - ctx, cancel := context.WithCancel(context.Background()) - cancel() - return ctx -} - -func (s *RQLSearchRepositoryTestSuite) TestOrgBillingSearchReturnsTxnError() { - _, err := postgres.NewOrgBillingRepository(s.client).Search(canceledContext(), &rql.Query{Limit: 10}) - s.Error(err) -} - -func (s *RQLSearchRepositoryTestSuite) TestOrgProjectsSearchReturnsTxnError() { - _, err := postgres.NewOrgProjectsRepository(s.client).Search(canceledContext(), "00000000-0000-0000-0000-000000000000", &rql.Query{Limit: 10}) - s.Error(err) -} - -func (s *RQLSearchRepositoryTestSuite) TestOrgUsersSearchReturnsTxnError() { - _, err := postgres.NewOrgUsersRepository(s.client).Search(canceledContext(), "00000000-0000-0000-0000-000000000000", &rql.Query{Limit: 10}) - s.Error(err) -} - -func (s *RQLSearchRepositoryTestSuite) TestProjectUsersSearchReturnsTxnError() { - _, err := postgres.NewProjectUsersRepository(s.client).Search(canceledContext(), "00000000-0000-0000-0000-000000000000", &rql.Query{Limit: 10}) - s.Error(err) -} - -func TestRQLSearchRepository(t *testing.T) { - suite.Run(t, new(RQLSearchRepositoryTestSuite)) -} From 9c6bba80b13f390c6c83a7f78c2b06886723e5b5 Mon Sep 17 00:00:00 2001 From: aman Date: Wed, 22 Jul 2026 11:38:44 +0530 Subject: [PATCH 4/5] test(store): move the failing-client fake into a shared fakes file Co-Authored-By: Claude Fable 5 --- internal/store/postgres/fakes_test.go | 25 +++++++++++++++++++ .../postgres/org_billing_repository_test.go | 19 -------------- 2 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 internal/store/postgres/fakes_test.go diff --git a/internal/store/postgres/fakes_test.go b/internal/store/postgres/fakes_test.go new file mode 100644 index 000000000..51e11381a --- /dev/null +++ b/internal/store/postgres/fakes_test.go @@ -0,0 +1,25 @@ +package postgres + +import ( + "context" + "database/sql" + "database/sql/driver" + "errors" + + "github.com/jmoiron/sqlx" + "github.com/raystack/frontier/pkg/db" +) + +// txnFailClient returns a client whose connections always fail, so any +// transaction begin returns an error. +func txnFailClient() *db.Client { + return &db.Client{DB: sqlx.NewDb(sql.OpenDB(failConnector{}), "postgres")} +} + +type failConnector struct{} + +func (failConnector) Connect(context.Context) (driver.Conn, error) { + return nil, errors.New("connection failed") +} + +func (failConnector) Driver() driver.Driver { return nil } diff --git a/internal/store/postgres/org_billing_repository_test.go b/internal/store/postgres/org_billing_repository_test.go index 374bace95..a145fa3a1 100644 --- a/internal/store/postgres/org_billing_repository_test.go +++ b/internal/store/postgres/org_billing_repository_test.go @@ -2,13 +2,8 @@ package postgres import ( "context" - "database/sql" - "database/sql/driver" - "errors" "testing" - "github.com/jmoiron/sqlx" - "github.com/raystack/frontier/pkg/db" "github.com/raystack/salt/rql" "github.com/stretchr/testify/assert" ) @@ -223,20 +218,6 @@ func TestPrepareGroupByQuery(t *testing.T) { } } -// txnFailClient returns a client whose connections always fail, so any -// transaction begin returns an error. -func txnFailClient() *db.Client { - return &db.Client{DB: sqlx.NewDb(sql.OpenDB(failConnector{}), "postgres")} -} - -type failConnector struct{} - -func (failConnector) Connect(context.Context) (driver.Conn, error) { - return nil, errors.New("connection failed") -} - -func (failConnector) Driver() driver.Driver { return nil } - func TestOrgBillingRepository_Search(t *testing.T) { t.Run("should return error when the transaction cannot start", func(t *testing.T) { repo := NewOrgBillingRepository(txnFailClient()) From fc8e0202ba1601732f7b84c9c38b10d625977ec8 Mon Sep 17 00:00:00 2001 From: aman Date: Wed, 22 Jul 2026 14:26:16 +0530 Subject: [PATCH 5/5] test(store): close the fake client after each test Co-Authored-By: Claude Fable 5 --- internal/store/postgres/fakes_test.go | 8 ++++++-- internal/store/postgres/org_billing_repository_test.go | 2 +- internal/store/postgres/org_projects_repository_test.go | 2 +- internal/store/postgres/org_users_repository_test.go | 2 +- internal/store/postgres/project_users_repository_test.go | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/store/postgres/fakes_test.go b/internal/store/postgres/fakes_test.go index 51e11381a..691bf6806 100644 --- a/internal/store/postgres/fakes_test.go +++ b/internal/store/postgres/fakes_test.go @@ -5,6 +5,7 @@ import ( "database/sql" "database/sql/driver" "errors" + "testing" "github.com/jmoiron/sqlx" "github.com/raystack/frontier/pkg/db" @@ -12,8 +13,11 @@ import ( // txnFailClient returns a client whose connections always fail, so any // transaction begin returns an error. -func txnFailClient() *db.Client { - return &db.Client{DB: sqlx.NewDb(sql.OpenDB(failConnector{}), "postgres")} +func txnFailClient(t *testing.T) *db.Client { + t.Helper() + client := &db.Client{DB: sqlx.NewDb(sql.OpenDB(failConnector{}), "postgres")} + t.Cleanup(func() { _ = client.Close() }) + return client } type failConnector struct{} diff --git a/internal/store/postgres/org_billing_repository_test.go b/internal/store/postgres/org_billing_repository_test.go index a145fa3a1..6e7e3ba09 100644 --- a/internal/store/postgres/org_billing_repository_test.go +++ b/internal/store/postgres/org_billing_repository_test.go @@ -220,7 +220,7 @@ func TestPrepareGroupByQuery(t *testing.T) { func TestOrgBillingRepository_Search(t *testing.T) { t.Run("should return error when the transaction cannot start", func(t *testing.T) { - repo := NewOrgBillingRepository(txnFailClient()) + repo := NewOrgBillingRepository(txnFailClient(t)) _, err := repo.Search(context.Background(), &rql.Query{Limit: 10}) assert.Error(t, err) }) diff --git a/internal/store/postgres/org_projects_repository_test.go b/internal/store/postgres/org_projects_repository_test.go index 35342a377..81ac231d9 100644 --- a/internal/store/postgres/org_projects_repository_test.go +++ b/internal/store/postgres/org_projects_repository_test.go @@ -129,7 +129,7 @@ func TestOrgProjectsRepository_prepareDataQuery(t *testing.T) { func TestOrgProjectsRepository_Search(t *testing.T) { t.Run("should return error when the transaction cannot start", func(t *testing.T) { - repo := NewOrgProjectsRepository(txnFailClient()) + repo := NewOrgProjectsRepository(txnFailClient(t)) _, err := repo.Search(context.Background(), "org-id", &rql.Query{Limit: 10}) assert.Error(t, err) }) diff --git a/internal/store/postgres/org_users_repository_test.go b/internal/store/postgres/org_users_repository_test.go index 4e5d7a8a4..acb7c6d08 100644 --- a/internal/store/postgres/org_users_repository_test.go +++ b/internal/store/postgres/org_users_repository_test.go @@ -310,7 +310,7 @@ func TestOrgUsersRepository_BuildRoleFilterCondition(t *testing.T) { func TestOrgUsersRepository_Search(t *testing.T) { t.Run("should return error when the transaction cannot start", func(t *testing.T) { - repo := NewOrgUsersRepository(txnFailClient()) + repo := NewOrgUsersRepository(txnFailClient(t)) _, err := repo.Search(context.Background(), "org-id", &rql.Query{Limit: 10}) assert.Error(t, err) }) diff --git a/internal/store/postgres/project_users_repository_test.go b/internal/store/postgres/project_users_repository_test.go index 77ac70083..928f404e5 100644 --- a/internal/store/postgres/project_users_repository_test.go +++ b/internal/store/postgres/project_users_repository_test.go @@ -62,7 +62,7 @@ func TestProjectUsersRepository_PrepareDataQuery(t *testing.T) { func TestProjectUsersRepository_Search(t *testing.T) { t.Run("should return error when the transaction cannot start", func(t *testing.T) { - repo := NewProjectUsersRepository(txnFailClient()) + repo := NewProjectUsersRepository(txnFailClient(t)) _, err := repo.Search(context.Background(), "project-123", &rql.Query{Limit: 10}) assert.Error(t, err) })