From 189c72b036e911a6d67f1bd4125f815fe497a863 Mon Sep 17 00:00:00 2001 From: aman Date: Fri, 17 Jul 2026 12:55:56 +0530 Subject: [PATCH 1/3] 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/3] 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/3] 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)) -}