From 9763572330bcc890705b867dad730044f2f5ae68 Mon Sep 17 00:00:00 2001 From: aman Date: Fri, 25 Sep 2026 17:07:48 +0530 Subject: [PATCH 1/3] fix(store): a search value that is not a uuid reports bad input Every aggregate search runs its query error through checkPostgresError and answers ErrBadInput when the database could not read a value as a uuid. The organizations search handler gains the branch the others already had. --- internal/api/v1beta1connect/organization_billing.go | 4 ++++ internal/store/postgres/org_billing_repository.go | 9 ++++++++- internal/store/postgres/org_invoices_repository.go | 9 ++++++++- internal/store/postgres/org_pats_repository.go | 9 ++++++++- internal/store/postgres/org_projects_repository.go | 9 ++++++++- .../postgres/org_serviceuser_credentials_repository.go | 9 ++++++++- internal/store/postgres/org_serviceuser_repository.go | 9 ++++++++- internal/store/postgres/org_tokens_repository.go | 9 ++++++++- internal/store/postgres/org_users_repository.go | 6 ++++-- internal/store/postgres/project_users_repository.go | 10 +++++++++- internal/store/postgres/user_orgs_repository.go | 9 ++++++++- internal/store/postgres/user_projects_repository.go | 9 ++++++++- 12 files changed, 89 insertions(+), 12 deletions(-) diff --git a/internal/api/v1beta1connect/organization_billing.go b/internal/api/v1beta1connect/organization_billing.go index 06a4c7b963..bb16aabf58 100644 --- a/internal/api/v1beta1connect/organization_billing.go +++ b/internal/api/v1beta1connect/organization_billing.go @@ -7,6 +7,7 @@ import ( "connectrpc.com/connect" "github.com/raystack/frontier/core/aggregates/orgbilling" + "github.com/raystack/frontier/internal/store/postgres" frontierv1beta1 "github.com/raystack/frontier/proto/v1beta1" "github.com/raystack/salt/rql" "google.golang.org/genproto/googleapis/api/httpbody" @@ -28,6 +29,9 @@ func (h *ConnectHandler) SearchOrganizations(ctx context.Context, request *conne orgBillingData, err := h.orgBillingService.Search(ctx, rqlQuery) if err != nil { + if errors.Is(err, postgres.ErrBadInput) { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("SearchOrganizations.Search: %w", err)) } diff --git a/internal/store/postgres/org_billing_repository.go b/internal/store/postgres/org_billing_repository.go index f2e1fe8e29..914cc1f45d 100644 --- a/internal/store/postgres/org_billing_repository.go +++ b/internal/store/postgres/org_billing_repository.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "errors" "fmt" "strings" @@ -170,7 +171,13 @@ func (r OrgBillingRepository) Search(ctx context.Context, rql *rql.Query) (svc.O }) if err != nil { - return svc.OrgBilling{}, err + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + return svc.OrgBilling{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.OrgBilling{}, err + } } res := make([]svc.AggregatedOrganization, 0) diff --git a/internal/store/postgres/org_invoices_repository.go b/internal/store/postgres/org_invoices_repository.go index 4754a9d59f..1aa651d78b 100644 --- a/internal/store/postgres/org_invoices_repository.go +++ b/internal/store/postgres/org_invoices_repository.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "errors" "fmt" "github.com/doug-martin/goqu/v9" @@ -123,7 +124,13 @@ func (r OrgInvoicesRepository) Search(ctx context.Context, orgID string, rql *rq }) if err != nil { - return svc.OrganizationInvoices{}, err + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + return svc.OrganizationInvoices{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.OrganizationInvoices{}, err + } } res := make([]svc.AggregatedInvoice, 0) diff --git a/internal/store/postgres/org_pats_repository.go b/internal/store/postgres/org_pats_repository.go index f945376c26..6a813532e4 100644 --- a/internal/store/postgres/org_pats_repository.go +++ b/internal/store/postgres/org_pats_repository.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "errors" "fmt" "time" @@ -118,7 +119,13 @@ func (r OrgPATsRepository) Search(ctx context.Context, orgID string, rqlQuery *r }) }) if err != nil { - return svc.OrganizationPATs{}, fmt.Errorf("querying org PATs: %w", err) + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + return svc.OrganizationPATs{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.OrganizationPATs{}, fmt.Errorf("querying org PATs: %w", err) + } } return svc.OrganizationPATs{ diff --git a/internal/store/postgres/org_projects_repository.go b/internal/store/postgres/org_projects_repository.go index 980f0f3286..62ec3135a3 100644 --- a/internal/store/postgres/org_projects_repository.go +++ b/internal/store/postgres/org_projects_repository.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "errors" "fmt" "strings" @@ -97,7 +98,13 @@ func (r OrgProjectsRepository) Search(ctx context.Context, orgID string, rql *rq }) if err != nil { - return svc.OrgProjects{}, err + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + return svc.OrgProjects{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.OrgProjects{}, err + } } res := make([]svc.AggregatedProject, 0) diff --git a/internal/store/postgres/org_serviceuser_credentials_repository.go b/internal/store/postgres/org_serviceuser_credentials_repository.go index ebb537150a..429ebf704f 100644 --- a/internal/store/postgres/org_serviceuser_credentials_repository.go +++ b/internal/store/postgres/org_serviceuser_credentials_repository.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "errors" "fmt" "github.com/doug-martin/goqu/v9" @@ -65,7 +66,13 @@ func (r OrgServiceUserCredentialsRepository) Search(ctx context.Context, orgID s }) if err != nil { - return svc.OrganizationServiceUserCredentials{}, err + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + return svc.OrganizationServiceUserCredentials{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.OrganizationServiceUserCredentials{}, err + } } res := make([]svc.AggregatedServiceUserCredential, 0) diff --git a/internal/store/postgres/org_serviceuser_repository.go b/internal/store/postgres/org_serviceuser_repository.go index f602c896a8..24ccd1e9c5 100644 --- a/internal/store/postgres/org_serviceuser_repository.go +++ b/internal/store/postgres/org_serviceuser_repository.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" "slices" @@ -83,7 +84,13 @@ func (r OrgServiceUserRepository) Search(ctx context.Context, orgID string, rqlQ }) if err != nil { - return svc.OrganizationServiceUsers{}, err + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + return svc.OrganizationServiceUsers{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.OrganizationServiceUsers{}, err + } } res := make([]svc.AggregatedServiceUser, 0) diff --git a/internal/store/postgres/org_tokens_repository.go b/internal/store/postgres/org_tokens_repository.go index ad750177f1..ca038ade62 100644 --- a/internal/store/postgres/org_tokens_repository.go +++ b/internal/store/postgres/org_tokens_repository.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "errors" "fmt" "strings" @@ -77,7 +78,13 @@ func (r OrgTokensRepository) Search(ctx context.Context, orgID string, rql *rql. }) if err != nil { - return svc.OrganizationTokens{}, err + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + return svc.OrganizationTokens{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.OrganizationTokens{}, err + } } res := make([]svc.AggregatedToken, 0) diff --git a/internal/store/postgres/org_users_repository.go b/internal/store/postgres/org_users_repository.go index 0626b1a298..c96fdcbf7d 100644 --- a/internal/store/postgres/org_users_repository.go +++ b/internal/store/postgres/org_users_repository.go @@ -125,10 +125,12 @@ func (r OrgUsersRepository) Search(ctx context.Context, orgID string, rql *rql.Q if err != nil { err = checkPostgresError(err) - if errors.Is(err, ErrInvalidTextRepresentation) { + switch { + case errors.Is(err, ErrInvalidTextRepresentation): return svc.OrgUsers{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.OrgUsers{}, err } - return svc.OrgUsers{}, err } res := make([]svc.AggregatedUser, 0) diff --git a/internal/store/postgres/project_users_repository.go b/internal/store/postgres/project_users_repository.go index 59f125353a..1452705f6f 100644 --- a/internal/store/postgres/project_users_repository.go +++ b/internal/store/postgres/project_users_repository.go @@ -3,6 +3,8 @@ package postgres import ( "context" "database/sql" + "errors" + "fmt" "strings" "github.com/doug-martin/goqu/v9" @@ -81,7 +83,13 @@ func (r ProjectUsersRepository) Search(ctx context.Context, projectID string, rq }) if err != nil { - return svc.ProjectUsers{}, err + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + return svc.ProjectUsers{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.ProjectUsers{}, err + } } res := make([]svc.AggregatedUser, 0) diff --git a/internal/store/postgres/user_orgs_repository.go b/internal/store/postgres/user_orgs_repository.go index 88df241e51..53d558bbc9 100644 --- a/internal/store/postgres/user_orgs_repository.go +++ b/internal/store/postgres/user_orgs_repository.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "errors" "fmt" "github.com/doug-martin/goqu/v9" @@ -115,7 +116,13 @@ func (r UserOrgsRepository) Search(ctx context.Context, principalID string, rql }) if err != nil { - return svc.UserOrgs{}, err + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + return svc.UserOrgs{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.UserOrgs{}, err + } } // Transform the results diff --git a/internal/store/postgres/user_projects_repository.go b/internal/store/postgres/user_projects_repository.go index 3f136dbff1..fa52d8386b 100644 --- a/internal/store/postgres/user_projects_repository.go +++ b/internal/store/postgres/user_projects_repository.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "errors" "fmt" "github.com/doug-martin/goqu/v9" @@ -97,7 +98,13 @@ func (r UserProjectsRepository) Search(ctx context.Context, userID string, orgID }) if err != nil { - return svc.UserProjects{}, err + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + return svc.UserProjects{}, fmt.Errorf("%w: value is not a valid uuid", ErrBadInput) + default: + return svc.UserProjects{}, err + } } res := make([]svc.AggregatedProject, 0) From e4783a2f341aad0b40b33c14d134c9d62ccb34a0 Mon Sep 17 00:00:00 2001 From: aman Date: Fri, 25 Sep 2026 17:07:48 +0530 Subject: [PATCH 2/3] test: every aggregate search reports a value that is not a uuid as bad input One case per search against postgres, plus handler cases for the organizations search error mapping. --- .../organization_billing_test.go | 63 +++++++++++ .../postgres/search_invalid_uuid_pg_test.go | 107 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 internal/api/v1beta1connect/organization_billing_test.go create mode 100644 internal/store/postgres/search_invalid_uuid_pg_test.go diff --git a/internal/api/v1beta1connect/organization_billing_test.go b/internal/api/v1beta1connect/organization_billing_test.go new file mode 100644 index 0000000000..5e0cc96427 --- /dev/null +++ b/internal/api/v1beta1connect/organization_billing_test.go @@ -0,0 +1,63 @@ +package v1beta1connect + +import ( + "context" + "fmt" + "testing" + + "connectrpc.com/connect" + "github.com/raystack/frontier/core/aggregates/orgbilling" + "github.com/raystack/frontier/internal/api/v1beta1connect/mocks" + "github.com/raystack/frontier/internal/store/postgres" + frontierv1beta1 "github.com/raystack/frontier/proto/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestSearchOrganizations(t *testing.T) { + tests := []struct { + name string + searchErr error + wantCode connect.Code + wantMsg string + }{ + { + name: "bad input from the store maps to invalid argument and keeps the reason", + searchErr: fmt.Errorf("%w: value is not a valid uuid", postgres.ErrBadInput), + wantCode: connect.CodeInvalidArgument, + wantMsg: "value is not a valid uuid", + }, + { + name: "any other store failure is internal", + searchErr: fmt.Errorf("connection refused"), + wantCode: connect.CodeInternal, + }, + { + name: "a query the store accepts answers normally", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + svc := mocks.NewOrgBillingService(t) + svc.EXPECT().Search(mock.Anything, mock.Anything).Return(orgbilling.OrgBilling{}, tt.searchErr) + handler := &ConnectHandler{orgBillingService: svc} + + resp, err := handler.SearchOrganizations(context.Background(), + connect.NewRequest(&frontierv1beta1.SearchOrganizationsRequest{ + Query: &frontierv1beta1.RQLRequest{Limit: 10}, + })) + + if tt.wantCode == 0 { + assert.NoError(t, err) + assert.NotNil(t, resp) + return + } + assert.Error(t, err) + assert.Equal(t, tt.wantCode, connect.CodeOf(err)) + if tt.wantMsg != "" { + assert.Contains(t, err.Error(), tt.wantMsg) + } + }) + } +} diff --git a/internal/store/postgres/search_invalid_uuid_pg_test.go b/internal/store/postgres/search_invalid_uuid_pg_test.go new file mode 100644 index 0000000000..bc8b677bf9 --- /dev/null +++ b/internal/store/postgres/search_invalid_uuid_pg_test.go @@ -0,0 +1,107 @@ +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" +) + +// Every aggregate search binds a caller supplied id to a uuid column. A value +// the database cannot read as a uuid has to come back as bad input, so the +// handlers answer with invalid argument instead of internal. +type SearchInvalidUUIDTestSuite struct { + suite.Suite + ctx context.Context + client *db.Client + pool *dockertest.Pool + resource *dockertest.Resource +} + +func (s *SearchInvalidUUIDTestSuite) 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) + } + s.ctx = context.TODO() +} + +func (s *SearchInvalidUUIDTestSuite) TearDownSuite() { + if err := purgeDocker(s.pool, s.resource); err != nil { + s.T().Fatal(err) + } +} + +func (s *SearchInvalidUUIDTestSuite) TestEverySearchReportsBadInput() { + const bad = "not-a-uuid" + query := func() *rql.Query { return &rql.Query{Limit: 10} } + + searches := map[string]func() error{ + "org users": func() error { + _, err := postgres.NewOrgUsersRepository(s.client).Search(s.ctx, bad, query()) + return err + }, + "org projects": func() error { + _, err := postgres.NewOrgProjectsRepository(s.client).Search(s.ctx, bad, query()) + return err + }, + "org service users": func() error { + _, err := postgres.NewOrgServiceUserRepository(s.client).Search(s.ctx, bad, query()) + return err + }, + "org personal access tokens": func() error { + _, err := postgres.NewOrgPATsRepository(s.client).Search(s.ctx, bad, query()) + return err + }, + "org invoices": func() error { + _, err := postgres.NewOrgInvoicesRepository(s.client).Search(s.ctx, bad, query()) + return err + }, + "org tokens": func() error { + _, err := postgres.NewOrgTokensRepository(s.client).Search(s.ctx, bad, query()) + return err + }, + "org service user credentials": func() error { + _, err := postgres.NewOrgServiceUserCredentialsRepository(s.client).Search(s.ctx, bad, query()) + return err + }, + "project users": func() error { + _, err := postgres.NewProjectUsersRepository(s.client).Search(s.ctx, bad, query()) + return err + }, + "user organizations": func() error { + _, err := postgres.NewUserOrgsRepository(s.client).Search(s.ctx, bad, &rql.Query{}) + return err + }, + "user projects": func() error { + _, err := postgres.NewUserProjectsRepository(s.client).Search(s.ctx, bad, bad, query()) + return err + }, + // this one takes no id, so the bad value arrives through its id filter + "org billing": func() error { + _, err := postgres.NewOrgBillingRepository(s.client).Search(s.ctx, &rql.Query{ + Limit: 10, + Filters: []rql.Filter{{Name: "id", Operator: "eq", Value: bad}}, + }) + return err + }, + } + + for name, search := range searches { + s.Run(name, func() { + s.ErrorIs(search(), postgres.ErrBadInput) + }) + } +} + +func TestSearchInvalidUUID(t *testing.T) { + suite.Run(t, new(SearchInvalidUUIDTestSuite)) +} From 04fdeddcf475a1ebc9f7e83443e4a40bb502ae72 Mon Sep 17 00:00:00 2001 From: aman Date: Mon, 28 Sep 2026 16:05:15 +0530 Subject: [PATCH 3/3] fix: the invoice searches report a value that is not a uuid as bad input The billing invoice search gains the same mapping as the others, and its two wraps use %w so the cause stays in the chain. The org invoices and org projects handlers pass the reason through instead of answering invalid argument with an internal server error message. --- .../v1beta1connect/organization_invoices.go | 2 +- .../v1beta1connect/organization_projects.go | 2 +- .../postgres/billing_invoice_repository.go | 11 +++++++++-- .../postgres/search_invalid_uuid_pg_test.go | 18 ++++++++++++++++-- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/internal/api/v1beta1connect/organization_invoices.go b/internal/api/v1beta1connect/organization_invoices.go index 800a5de5dd..b26e6802cd 100644 --- a/internal/api/v1beta1connect/organization_invoices.go +++ b/internal/api/v1beta1connect/organization_invoices.go @@ -30,7 +30,7 @@ func (h *ConnectHandler) SearchOrganizationInvoices(ctx context.Context, request invoicesData, err := h.orgInvoicesService.Search(ctx, request.Msg.GetId(), rqlQuery) if err != nil { if errors.Is(err, postgres.ErrBadInput) { - return nil, connect.NewError(connect.CodeInvalidArgument, ErrInternalServerError) + return nil, connect.NewError(connect.CodeInvalidArgument, err) } return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("SearchOrganizationInvoices.Search: org_id=%s: %w", request.Msg.GetId(), err)) } diff --git a/internal/api/v1beta1connect/organization_projects.go b/internal/api/v1beta1connect/organization_projects.go index d2397896d5..d0d47284a4 100644 --- a/internal/api/v1beta1connect/organization_projects.go +++ b/internal/api/v1beta1connect/organization_projects.go @@ -31,7 +31,7 @@ func (h *ConnectHandler) SearchOrganizationProjects(ctx context.Context, request orgProjectsData, err := h.orgProjectsService.Search(ctx, request.Msg.GetId(), rqlQuery) if err != nil { if errors.Is(err, postgres.ErrBadInput) { - return nil, connect.NewError(connect.CodeInvalidArgument, ErrInternalServerError) + return nil, connect.NewError(connect.CodeInvalidArgument, err) } return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("SearchOrganizationProjects.Search: org_id=%s: %w", request.Msg.GetId(), err)) } diff --git a/internal/store/postgres/billing_invoice_repository.go b/internal/store/postgres/billing_invoice_repository.go index f80d4f3837..699b3dcdbc 100644 --- a/internal/store/postgres/billing_invoice_repository.go +++ b/internal/store/postgres/billing_invoice_repository.go @@ -259,7 +259,7 @@ func (r BillingInvoiceRepository) List(ctx context.Context, flt invoice.Filter) if err = r.dbc.WithTimeout(ctx, TABLE_BILLING_INVOICES, "List", func(ctx context.Context) error { return r.dbc.SelectContext(ctx, &invoiceModels, query, params...) }); err != nil { - return nil, fmt.Errorf("%w: %s", errDB, err) + return nil, fmt.Errorf("%w: %w", errDB, err) } invoices := make([]invoice.Invoice, 0, len(invoiceModels)) @@ -352,7 +352,14 @@ func (r BillingInvoiceRepository) Search(ctx context.Context, rqlQuery *rql.Quer if err = r.dbc.WithTimeout(ctx, TABLE_BILLING_INVOICES, "Search", func(ctx context.Context) error { return r.dbc.SelectContext(ctx, &invoiceModels, dataQuery, params...) }); err != nil { - return nil, fmt.Errorf("%w: %s", errDB, err) + err = checkPostgresError(err) + switch { + case errors.Is(err, ErrInvalidTextRepresentation): + // this repository's handler matches the billing sentinel, not the postgres one + return nil, fmt.Errorf("%w: value is not a valid uuid", invoice.ErrBadInput) + default: + return nil, fmt.Errorf("%w: %w", errDB, err) + } } // Transform results diff --git a/internal/store/postgres/search_invalid_uuid_pg_test.go b/internal/store/postgres/search_invalid_uuid_pg_test.go index bc8b677bf9..1f739a1554 100644 --- a/internal/store/postgres/search_invalid_uuid_pg_test.go +++ b/internal/store/postgres/search_invalid_uuid_pg_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/ory/dockertest" + "github.com/raystack/frontier/billing/invoice" "github.com/raystack/frontier/internal/store/postgres" "github.com/raystack/frontier/pkg/db" "github.com/raystack/salt/rql" @@ -85,7 +86,14 @@ func (s *SearchInvalidUUIDTestSuite) TestEverySearchReportsBadInput() { _, err := postgres.NewUserProjectsRepository(s.client).Search(s.ctx, bad, bad, query()) return err }, - // this one takes no id, so the bad value arrives through its id filter + // these two take no id, so the bad value arrives through a filter + "billing invoices": func() error { + _, err := postgres.NewBillingInvoiceRepository(s.client).Search(s.ctx, &rql.Query{ + Limit: 10, + Filters: []rql.Filter{{Name: "id", Operator: "eq", Value: bad}}, + }) + return err + }, "org billing": func() error { _, err := postgres.NewOrgBillingRepository(s.client).Search(s.ctx, &rql.Query{ Limit: 10, @@ -97,7 +105,13 @@ func (s *SearchInvalidUUIDTestSuite) TestEverySearchReportsBadInput() { for name, search := range searches { s.Run(name, func() { - s.ErrorIs(search(), postgres.ErrBadInput) + // every search answers the postgres sentinel except the billing + // invoice one, whose handler matches the billing package's own + want := postgres.ErrBadInput + if name == "billing invoices" { + want = invoice.ErrBadInput + } + s.ErrorIs(search(), want) }) } }