From ef76a8d2e13009d152ffb2c53881d43aec5717bc Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Fri, 13 Mar 2026 16:50:35 +0100 Subject: [PATCH] Remove deadcode Signed-off-by: Bryan Frimin --- GNUmakefile | 6 + go.mod | 21 +- go.sum | 38 +-- pkg/bootstrap/builder.go | 5 - pkg/bootstrap/builder_test.go | 21 +- pkg/certmanager/cache_store.go | 29 -- pkg/certmanager/selector.go | 8 - pkg/coredata/applicability_statement.go | 43 --- pkg/coredata/audit.go | 38 --- pkg/coredata/cached_certificate.go | 9 - pkg/coredata/compliance_framework.go | 29 -- pkg/coredata/connector.go | 38 --- pkg/coredata/control.go | 132 --------- pkg/coredata/control_audit.go | 71 ----- pkg/coredata/control_mesure.go | 37 --- pkg/coredata/control_obligation.go | 72 ----- pkg/coredata/control_snapshot.go | 71 ----- .../custom_domain_verification_status.go | 43 --- pkg/coredata/framework.go | 47 --- pkg/coredata/identity.go | 79 ----- pkg/coredata/meeting_attendee.go | 41 --- pkg/coredata/organization.go | 209 ------------- pkg/coredata/saml_request.go | 57 ---- pkg/crypto/cipher/cipher.go | 11 - pkg/gid/tenant_id.go | 16 - pkg/iam/auth_service.go | 13 - pkg/iam/errors.go | 13 - pkg/iam/policy/action.go | 126 -------- pkg/iam/policy/action_test.go | 219 -------------- pkg/iam/policy/authorizer.go | 159 ---------- pkg/iam/policy/authorizer_test.go | 277 ------------------ pkg/iam/policy/example_test.go | 54 ---- pkg/iam/policy/policy.go | 24 -- pkg/iam/policy/statement_test.go | 13 +- pkg/iam/scim/bridge/bridge.go | 49 +--- pkg/iam/session_service.go | 13 - pkg/probo/custom_domain_service.go | 14 - pkg/probo/organization_service.go | 12 - pkg/probo/snapshot_service.go | 32 +- pkg/probo/trust_center_access_service.go | 8 - pkg/saferedirect/saferedirect.go | 4 - pkg/saferedirect/saferedirect_test.go | 93 ------ pkg/securecookie/securecookie.go | 18 -- pkg/server/api/compliancepage/context.go | 6 - pkg/server/api/console/v1/types/meeting.go | 10 - .../v1/types/trust_center_document_access.go | 10 - pkg/server/api/trust/v1/resolver.go | 18 -- pkg/server/gqlutils/errors.go | 8 - pkg/server/gqlutils/types/bigint/bigint.go | 1 + pkg/server/statichandler/statichandler.go | 7 - pkg/validator/checkeach_slice_test.go | 18 +- pkg/validator/double_pointer_test.go | 12 +- pkg/validator/oneof_custom_type_test.go | 4 +- pkg/validator/optional_pointer_test.go | 10 +- pkg/validator/validation.go | 24 -- pkg/validator/validation_bench_test.go | 225 +------------- pkg/validator/validation_test.go | 183 +----------- pkg/validator/validator_collection.go | 109 ------- pkg/validator/validator_collection_test.go | 215 -------------- pkg/validator/validator_common_test.go | 22 +- pkg/validator/validator_conditional.go | 71 ----- pkg/validator/validator_conditional_test.go | 182 ------------ pkg/validator/validator_custom.go | 26 -- pkg/validator/validator_custom_test.go | 47 --- pkg/validator/validator_email.go | 46 --- pkg/validator/validator_format.go | 60 ---- pkg/validator/validator_format_test.go | 69 ----- pkg/validator/validator_numeric.go | 123 -------- pkg/validator/validator_numeric_test.go | 25 -- pkg/validator/validator_security_test.go | 28 +- pkg/validator/validator_string.go | 209 ------------- pkg/validator/validator_string_test.go | 209 ------------- pkg/validator/validator_time.go | 90 ------ pkg/validator/validator_time_test.go | 138 --------- pkg/version/version.go | 26 -- pkg/version/version_test.go | 18 -- 76 files changed, 137 insertions(+), 4424 deletions(-) delete mode 100644 pkg/iam/policy/action.go delete mode 100644 pkg/iam/policy/action_test.go delete mode 100644 pkg/iam/policy/authorizer.go delete mode 100644 pkg/iam/policy/authorizer_test.go delete mode 100644 pkg/validator/validator_collection.go delete mode 100644 pkg/validator/validator_collection_test.go delete mode 100644 pkg/validator/validator_conditional.go delete mode 100644 pkg/validator/validator_conditional_test.go delete mode 100644 pkg/validator/validator_custom.go delete mode 100644 pkg/validator/validator_custom_test.go delete mode 100644 pkg/validator/validator_email.go diff --git a/GNUmakefile b/GNUmakefile index 020043613..ee34c96b9 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -32,6 +32,7 @@ GO_BUILD= $(GO_BASE) build $(LDFLAGS) $(GCFLAGS) GO_GENERATE= $(GO_BASE) generate GO_TEST= $(GO_BASE) tool gotestsum -- $(TEST_FLAGS) GO_VET= $(GO_BASE) vet +GO_TOOL= $(GO_BASE) tool TEST_FLAGS?= -race -cover -coverprofile=coverage.out @@ -281,3 +282,8 @@ compose/keycloak/probo-realm.json: compose/keycloak/probo-realm.json.tmpl compos apps/console/dist/index.html apps/trust/dist/index.html: $(MKDIR) $(dir $@) $(ECHO) dev-server > $@ + + +.PHONY: deadcode +deadcode: + $(GO_TOOL) deadcode ./... | grep -v "With" | grep -v "UnmarshalBigIntScalar" | grep -v "^e2e/" \ No newline at end of file diff --git a/go.mod b/go.mod index 7e182291e..b7552b034 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.19.10 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 - github.com/brianvoe/gofakeit/v7 v7.14.0 + github.com/brianvoe/gofakeit/v7 v7.14.1 github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d github.com/chromedp/chromedp v0.14.2 github.com/crewjam/saml v0.5.1 @@ -34,14 +34,16 @@ require ( go.opentelemetry.io/otel v1.40.0 go.opentelemetry.io/otel/trace v1.40.0 go.probo.inc/mcpgen v0.0.0-20260223192226-386b1fbe6184 - golang.org/x/crypto v0.48.0 + golang.org/x/crypto v0.49.0 golang.org/x/image v0.36.0 golang.org/x/oauth2 v0.35.0 - golang.org/x/sync v0.19.0 + golang.org/x/sync v0.20.0 google.golang.org/api v0.269.0 gopkg.in/yaml.v3 v3.0.1 ) +require golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect + require ( cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect @@ -134,12 +136,12 @@ require ( go.opentelemetry.io/otel/sdk v1.40.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.51.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.43.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect google.golang.org/grpc v1.79.1 // indirect @@ -152,6 +154,7 @@ require ( tool ( github.com/99designs/gqlgen go.probo.inc/mcpgen + golang.org/x/tools/cmd/deadcode gotest.tools/gotestsum ) diff --git a/go.sum b/go.sum index f4fb094a4..0f0b998e9 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE= github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= -github.com/brianvoe/gofakeit/v7 v7.14.0 h1:R8tmT/rTDJmD2ngpqBL9rAKydiL7Qr2u3CXPqRt59pk= -github.com/brianvoe/gofakeit/v7 v7.14.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= +github.com/brianvoe/gofakeit/v7 v7.14.1 h1:a7fe3fonbj0cW3wgl5VwIKfZtiH9C3cLnwcIXWT7sow= +github.com/brianvoe/gofakeit/v7 v7.14.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= @@ -296,27 +296,29 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= diff --git a/pkg/bootstrap/builder.go b/pkg/bootstrap/builder.go index da9d3d2e6..8659e2153 100644 --- a/pkg/bootstrap/builder.go +++ b/pkg/bootstrap/builder.go @@ -38,11 +38,6 @@ func NewBuilder(getEnv EnvGetter) *Builder { return &Builder{getEnv: getEnv} } -func (b *Builder) SetSAMLCredentials(certificate, privateKey string) { - b.samlCertificate = certificate - b.samlPrivateKey = privateKey -} - func (b *Builder) Build() (*probod.FullConfig, error) { if err := b.validateRequired(); err != nil { return nil, err diff --git a/pkg/bootstrap/builder_test.go b/pkg/bootstrap/builder_test.go index 4636d701c..ee1f9914e 100644 --- a/pkg/bootstrap/builder_test.go +++ b/pkg/bootstrap/builder_test.go @@ -92,7 +92,8 @@ func TestBuilder_Build_MissingRequiredEnvVars(t *testing.T) { func TestBuilder_Build_Defaults(t *testing.T) { b := NewBuilder(mockEnv(requiredEnv())) - b.SetSAMLCredentials("test-cert", "test-key") + b.samlCertificate = "test-cert" + b.samlPrivateKey = "test-key" cfg, err := b.Build() require.NoError(t, err) @@ -243,7 +244,8 @@ func TestBuilder_Build_CustomValues(t *testing.T) { env["ESIGN_TSA_URL"] = "http://custom.tsa.example.com" b := NewBuilder(mockEnv(env)) - b.SetSAMLCredentials("test-cert", "test-key") + b.samlCertificate = "test-cert" + b.samlPrivateKey = "test-key" cfg, err := b.Build() require.NoError(t, err) @@ -313,7 +315,8 @@ func TestBuilder_Build_SlackConnector(t *testing.T) { env["CONNECTOR_SLACK_REDIRECT_URI"] = "https://app.example.com/api/console/v1/connectors/complete" b := NewBuilder(mockEnv(env)) - b.SetSAMLCredentials("test-cert", "test-key") + b.samlCertificate = "test-cert" + b.samlPrivateKey = "test-key" cfg, err := b.Build() require.NoError(t, err) @@ -343,7 +346,8 @@ func TestBuilder_Build_SlackConnector_CustomURLs(t *testing.T) { env["CONNECTOR_SLACK_TOKEN_URL"] = "https://custom.slack.com/oauth/token" b := NewBuilder(mockEnv(env)) - b.SetSAMLCredentials("test-cert", "test-key") + b.samlCertificate = "test-cert" + b.samlPrivateKey = "test-key" cfg, err := b.Build() require.NoError(t, err) @@ -382,7 +386,8 @@ func TestBuilder_Build_SAMLFromEnv(t *testing.T) { func TestBuilder_Build_SAMLPreset(t *testing.T) { b := NewBuilder(mockEnv(requiredEnv())) - b.SetSAMLCredentials("preset-cert", "preset-key") + b.samlCertificate = "preset-cert" + b.samlPrivateKey = "preset-key" cfg, err := b.Build() require.NoError(t, err) @@ -396,7 +401,8 @@ func TestBuilder_Build_PgCABundleFromEnv(t *testing.T) { env["PG_CA_BUNDLE"] = "test-ca-bundle-content" b := NewBuilder(mockEnv(env)) - b.SetSAMLCredentials("test-cert", "test-key") + b.samlCertificate = "test-cert" + b.samlPrivateKey = "test-key" cfg, err := b.Build() require.NoError(t, err) @@ -414,7 +420,8 @@ func TestBuilder_Build_PgCABundleFromFile(t *testing.T) { env["PG_CA_BUNDLE_PATH"] = caFile b := NewBuilder(mockEnv(env)) - b.SetSAMLCredentials("test-cert", "test-key") + b.samlCertificate = "test-cert" + b.samlPrivateKey = "test-key" cfg, err := b.Build() require.NoError(t, err) diff --git a/pkg/certmanager/cache_store.go b/pkg/certmanager/cache_store.go index 575540150..e7e524408 100644 --- a/pkg/certmanager/cache_store.go +++ b/pkg/certmanager/cache_store.go @@ -141,32 +141,3 @@ func (w *CacheStore) warmDomain(ctx context.Context, conn pg.Conn, domain *cored return nil } -func (w *CacheStore) RefreshCache(ctx context.Context) error { - w.logger.InfoCtx(ctx, "refreshing certificate cache") - - return w.pg.WithConn( - ctx, - func(conn pg.Conn) error { - cachedCertificates := coredata.CachedCertificates{} - if err := cachedCertificates.CleanExpired(ctx, conn); err != nil { - w.logger.ErrorCtx(ctx, "cannot clean expired cache", log.Error(err)) - } - - return w.WarmCache(ctx) - }, - ) -} - -func (w *CacheStore) WarmSingleDomain(ctx context.Context, domainName string) error { - return w.pg.WithConn( - ctx, - func(conn pg.Conn) error { - var domain coredata.CustomDomain - if err := domain.LoadByDomain(ctx, conn, coredata.NewNoScope(), domainName); err != nil { - return fmt.Errorf("cannot load domain: %w", err) - } - - return w.warmDomain(ctx, conn, &domain) - }, - ) -} diff --git a/pkg/certmanager/selector.go b/pkg/certmanager/selector.go index b38064a68..febd7292f 100644 --- a/pkg/certmanager/selector.go +++ b/pkg/certmanager/selector.go @@ -160,11 +160,3 @@ func (s *Selector) rebuildCacheEntry(ctx context.Context, conn pg.Conn, domain s return nil } -func (s *Selector) ClearCache() { - s.cache.Range( - func(key, _ any) bool { - s.cache.Delete(key) - return true - }, - ) -} diff --git a/pkg/coredata/applicability_statement.go b/pkg/coredata/applicability_statement.go index b5fd36781..1a4ca3403 100644 --- a/pkg/coredata/applicability_statement.go +++ b/pkg/coredata/applicability_statement.go @@ -566,46 +566,3 @@ WHERE return nil } -func (sacs *ApplicabilityStatements) CountByControlID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - controlID gid.GID, -) (int, error) { - q := ` -WITH soac_ctrl AS ( - SELECT - soac.id, - soac.organization_id, - soac.tenant_id - FROM - applicability_statements soac - INNER JOIN - states_of_applicability soa ON soac.state_of_applicability_id = soa.id - WHERE - soac.%[1]s - AND soac.control_id = @control_id - AND soa.snapshot_id IS NULL -) -SELECT - COUNT(id) -FROM - soac_ctrl -WHERE - %[1]s; -` - - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.NamedArgs{"control_id": controlID} - maps.Copy(args, scope.SQLArguments()) - - row := conn.QueryRow(ctx, q, args) - - var count int - if err := row.Scan(&count); err != nil { - return 0, fmt.Errorf("cannot scan count: %w", err) - } - - return count, nil -} diff --git a/pkg/coredata/audit.go b/pkg/coredata/audit.go index a0d1fc8d0..a316eaa1b 100644 --- a/pkg/coredata/audit.go +++ b/pkg/coredata/audit.go @@ -385,44 +385,6 @@ WHERE return nil } -func (a *Audits) CountByControlID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - controlID gid.GID, -) (int, error) { - q := ` -WITH audits_by_control AS ( - SELECT - a.id, - a.tenant_id - FROM - audits a - INNER JOIN - controls_audits ca ON a.id = ca.audit_id - WHERE - ca.control_id = @control_id - ) - SELECT - COUNT(id) - FROM - audits_by_control - WHERE %s - ` - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"control_id": controlID} - maps.Copy(args, scope.SQLArguments()) - - row := conn.QueryRow(ctx, q, args) - - var count int - if err := row.Scan(&count); err != nil { - return 0, fmt.Errorf("cannot scan count: %w", err) - } - - return count, nil -} func (a *Audits) LoadByControlID( ctx context.Context, diff --git a/pkg/coredata/cached_certificate.go b/pkg/coredata/cached_certificate.go index 1aa5b9b2f..776cac841 100644 --- a/pkg/coredata/cached_certificate.go +++ b/pkg/coredata/cached_certificate.go @@ -39,15 +39,6 @@ type ( CachedCertificates []*CachedCertificate ) -func NewCachedCertificate(domain string, domainID gid.GID) *CachedCertificate { - now := time.Now() - return &CachedCertificate{ - Domain: domain, - CustomDomainID: domainID, - CachedAt: now, - } -} - func (cc *CachedCertificate) LoadByDomain(ctx context.Context, conn pg.Conn, domain string) error { q := ` SELECT diff --git a/pkg/coredata/compliance_framework.go b/pkg/coredata/compliance_framework.go index cc4e2e6ae..01066ed7b 100644 --- a/pkg/coredata/compliance_framework.go +++ b/pkg/coredata/compliance_framework.go @@ -397,32 +397,3 @@ WHERE %s return nil } -func (c *ComplianceFrameworks) CountByTrustCenterID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - trustCenterID gid.GID, -) (int, error) { - q := ` -SELECT - COUNT(*) -FROM - compliance_frameworks -WHERE - %s - AND trust_center_id = @trust_center_id -` - - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"trust_center_id": trustCenterID} - maps.Copy(args, scope.SQLArguments()) - - var count int - err := conn.QueryRow(ctx, q, args).Scan(&count) - if err != nil { - return 0, fmt.Errorf("cannot count compliance frameworks: %w", err) - } - - return count, nil -} diff --git a/pkg/coredata/connector.go b/pkg/coredata/connector.go index 3761d5fe7..1e0905c3d 100644 --- a/pkg/coredata/connector.go +++ b/pkg/coredata/connector.go @@ -72,44 +72,6 @@ func (c *Connector) AuthorizationAttributes(ctx context.Context, conn pg.Conn) ( return map[string]string{"organization_id": organizationID.String()}, nil } -func (c *Connectors) LoadByOrganizationID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - organizationID gid.GID, - cursor *page.Cursor[ConnectorOrderField], - encryptionKey cipher.EncryptionKey, - filter *ConnectorFilter, -) error { - if err := c.loadByOrganizationIDWithPagination(ctx, conn, scope, organizationID, cursor, filter); err != nil { - return fmt.Errorf("cannot load connectors by organization ID: %w", err) - } - - if err := c.decryptConnections(encryptionKey); err != nil { - return fmt.Errorf("cannot decrypt connections: %w", err) - } - - return nil -} - -func (c *Connectors) LoadAllByOrganizationID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - organizationID gid.GID, - encryptionKey cipher.EncryptionKey, -) error { - if err := c.loadAllByOrganizationID(ctx, conn, scope, organizationID); err != nil { - return fmt.Errorf("cannot load all connectors by organization ID: %w", err) - } - - if err := c.decryptConnections(encryptionKey); err != nil { - return fmt.Errorf("cannot decrypt connections: %w", err) - } - - return nil -} - func (c *Connectors) LoadAllByOrganizationIDProtocolAndProvider( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/control.go b/pkg/coredata/control.go index 5c250cde2..f130bd381 100644 --- a/pkg/coredata/control.go +++ b/pkg/coredata/control.go @@ -696,52 +696,6 @@ LIMIT 1; return nil } -func (c *Controls) LoadByIDs( - ctx context.Context, - conn pg.Conn, - scope Scoper, - controlIDs []gid.GID, -) error { - if len(controlIDs) == 0 { - *c = Controls{} - return nil - } - - q := ` -SELECT - id, - section_title, - framework_id, - organization_id, - name, - description, - best_practice, - created_at, - updated_at -FROM - controls -WHERE - %s - AND id = ANY(@control_ids) -` - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"control_ids": controlIDs} - maps.Copy(args, scope.SQLArguments()) - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query controls: %w", err) - } - - controls, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Control]) - if err != nil { - return fmt.Errorf("cannot collect controls: %w", err) - } - - *c = controls - return nil -} - func (c Control) Insert( ctx context.Context, conn pg.Conn, @@ -866,49 +820,6 @@ WHERE %s return nil } -func (c *Controls) CountByAuditID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - auditID gid.GID, - filter *ControlFilter, -) (int, error) { - q := ` -WITH ctrl AS ( - SELECT - c.id, - c.tenant_id, - c.search_vector - FROM - controls c - INNER JOIN - controls_audits ca ON c.id = ca.control_id - WHERE - ca.audit_id = @audit_id - ) - SELECT - COUNT(id) - FROM - ctrl - WHERE %s - AND %s - ` - q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) - - args := pgx.NamedArgs{"audit_id": auditID} - maps.Copy(args, scope.SQLArguments()) - maps.Copy(args, filter.SQLArguments()) - - row := conn.QueryRow(ctx, q, args) - - var count int - if err := row.Scan(&count); err != nil { - return 0, fmt.Errorf("cannot scan count: %w", err) - } - - return count, nil -} - func (c *Controls) LoadByAuditID( ctx context.Context, conn pg.Conn, @@ -976,49 +887,6 @@ WHERE %s return nil } -func (c *Controls) CountBySnapshotID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - snapshotID gid.GID, - filter *ControlFilter, -) (int, error) { - q := ` -WITH ctrl AS ( - SELECT - c.id, - c.tenant_id, - c.search_vector - FROM - controls c - INNER JOIN - controls_snapshots cs ON c.id = cs.control_id - WHERE - cs.snapshot_id = @snapshot_id -) -SELECT - COUNT(id) -FROM - ctrl -WHERE %s - AND %s -` - q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment()) - - args := pgx.NamedArgs{"snapshot_id": snapshotID} - maps.Copy(args, scope.SQLArguments()) - maps.Copy(args, filter.SQLArguments()) - - row := conn.QueryRow(ctx, q, args) - - var count int - if err := row.Scan(&count); err != nil { - return 0, fmt.Errorf("cannot scan count: %w", err) - } - - return count, nil -} - func (c *Controls) LoadBySnapshotID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/control_audit.go b/pkg/coredata/control_audit.go index 74d3dea18..d6a61b6fe 100644 --- a/pkg/coredata/control_audit.go +++ b/pkg/coredata/control_audit.go @@ -99,74 +99,3 @@ WHERE return err } -func (cas *ControlAudits) LoadByControlID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - controlID gid.GID, -) error { - q := ` -SELECT - control_id, - audit_id, - created_at -FROM - controls_audits -WHERE - %s - AND control_id = @control_id -` - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"control_id": controlID} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query control_audits: %w", err) - } - - controlAudits, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlAudit]) - if err != nil { - return fmt.Errorf("cannot collect control_audits: %w", err) - } - - *cas = controlAudits - return nil -} - -func (cas *ControlAudits) LoadByAuditID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - auditID gid.GID, -) error { - q := ` -SELECT - control_id, - audit_id, - created_at -FROM - controls_audits -WHERE - %s - AND audit_id = @audit_id -` - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"audit_id": auditID} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query control_audits: %w", err) - } - - controlAudits, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlAudit]) - if err != nil { - return fmt.Errorf("cannot collect control_audits: %w", err) - } - - *cas = controlAudits - return nil -} diff --git a/pkg/coredata/control_mesure.go b/pkg/coredata/control_mesure.go index c72d51f1d..a94e8271e 100644 --- a/pkg/coredata/control_mesure.go +++ b/pkg/coredata/control_mesure.go @@ -100,43 +100,6 @@ WHERE return err } -func (cms *ControlMeasures) LoadByMeasureID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - measureID gid.GID, -) error { - q := ` -SELECT - control_id, - measure_id, - tenant_id, - created_at -FROM - controls_measures -WHERE - %s - AND measure_id = @measure_id -` - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"measure_id": measureID} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query control_measures: %w", err) - } - - controlMeasures, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlMeasure]) - if err != nil { - return fmt.Errorf("cannot collect control_measures: %w", err) - } - - *cms = controlMeasures - return nil -} - type ControlWithRisk struct { ControlID gid.GID `db:"control_id"` } diff --git a/pkg/coredata/control_obligation.go b/pkg/coredata/control_obligation.go index 196f1a147..c54d8bd93 100644 --- a/pkg/coredata/control_obligation.go +++ b/pkg/coredata/control_obligation.go @@ -95,78 +95,6 @@ WHERE return err } -func (cos *ControlObligations) LoadByControlID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - controlID gid.GID, -) error { - q := ` -SELECT - control_id, - obligation_id, - created_at -FROM - controls_obligations -WHERE - %s - AND control_id = @control_id -` - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"control_id": controlID} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query control_obligations: %w", err) - } - - controlObligations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlObligation]) - if err != nil { - return fmt.Errorf("cannot collect control_obligations: %w", err) - } - - *cos = controlObligations - return nil -} - -func (cos *ControlObligations) LoadByObligationID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - obligationID gid.GID, -) error { - q := ` -SELECT - control_id, - obligation_id, - created_at -FROM - controls_obligations -WHERE - %s - AND obligation_id = @obligation_id -` - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"obligation_id": obligationID} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query control_obligations: %w", err) - } - - controlObligations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlObligation]) - if err != nil { - return fmt.Errorf("cannot collect control_obligations: %w", err) - } - - *cos = controlObligations - return nil -} - func (cos *ControlObligations) CountByControlID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/control_snapshot.go b/pkg/coredata/control_snapshot.go index bd6fe4582..0e68b9184 100644 --- a/pkg/coredata/control_snapshot.go +++ b/pkg/coredata/control_snapshot.go @@ -99,74 +99,3 @@ WHERE return err } -func (css *ControlSnapshots) LoadByControlID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - controlID gid.GID, -) error { - q := ` -SELECT - control_id, - snapshot_id, - created_at -FROM - controls_snapshots -WHERE - %s - AND control_id = @control_id -` - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"control_id": controlID} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query controls_snapshots: %w", err) - } - - controlSnapshots, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlSnapshot]) - if err != nil { - return fmt.Errorf("cannot collect controls_snapshots: %w", err) - } - - *css = controlSnapshots - return nil -} - -func (css *ControlSnapshots) LoadBySnapshotID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - snapshotID gid.GID, -) error { - q := ` -SELECT - control_id, - snapshot_id, - created_at -FROM - controls_snapshots -WHERE - %s - AND snapshot_id = @snapshot_id -` - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"snapshot_id": snapshotID} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query controls_snapshots: %w", err) - } - - controlSnapshots, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ControlSnapshot]) - if err != nil { - return fmt.Errorf("cannot collect controls_snapshots: %w", err) - } - - *css = controlSnapshots - return nil -} diff --git a/pkg/coredata/custom_domain_verification_status.go b/pkg/coredata/custom_domain_verification_status.go index 253a65d02..617e5c8d3 100644 --- a/pkg/coredata/custom_domain_verification_status.go +++ b/pkg/coredata/custom_domain_verification_status.go @@ -14,11 +14,6 @@ package coredata -import ( - "database/sql/driver" - "fmt" -) - type CustomDomainVerificationStatus string const ( @@ -26,41 +21,3 @@ const ( CustomDomainVerificationStatusVerified CustomDomainVerificationStatus = "VERIFIED" CustomDomainVerificationStatusFailed CustomDomainVerificationStatus = "FAILED" ) - -func (s CustomDomainVerificationStatus) MarshalText() ([]byte, error) { - return []byte(s.String()), nil -} - -func (s *CustomDomainVerificationStatus) UnmarshalText(data []byte) error { - val := string(data) - - switch val { - case CustomDomainVerificationStatusPending.String(): - *s = CustomDomainVerificationStatusPending - case CustomDomainVerificationStatusVerified.String(): - *s = CustomDomainVerificationStatusVerified - case CustomDomainVerificationStatusFailed.String(): - *s = CustomDomainVerificationStatusFailed - default: - return fmt.Errorf("invalid CustomDomainVerificationStatus value: %q", val) - } - - return nil -} - -func (s CustomDomainVerificationStatus) String() string { - return string(s) -} - -func (s *CustomDomainVerificationStatus) Scan(value any) error { - val, ok := value.(string) - if !ok { - return fmt.Errorf("invalid scan source for CustomDomainVerificationStatus, expected string got %T", value) - } - - return s.UnmarshalText([]byte(val)) -} - -func (s CustomDomainVerificationStatus) Value() (driver.Value, error) { - return s.String(), nil -} diff --git a/pkg/coredata/framework.go b/pkg/coredata/framework.go index 72cd7ce7a..558b72483 100644 --- a/pkg/coredata/framework.go +++ b/pkg/coredata/framework.go @@ -241,53 +241,6 @@ LIMIT 1; return nil } -func (f *Frameworks) LoadByIDs( - ctx context.Context, - conn pg.Conn, - scope Scoper, - frameworkIDs []gid.GID, -) error { - if len(frameworkIDs) == 0 { - *f = Frameworks{} - return nil - } - - q := ` -SELECT - id, - organization_id, - reference_id, - name, - description, - light_logo_file_id, - dark_logo_file_id, - created_at, - updated_at -FROM - frameworks -WHERE - %s - AND id = ANY(@framework_ids) -` - - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"framework_ids": frameworkIDs} - maps.Copy(args, scope.SQLArguments()) - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query frameworks: %w", err) - } - - frameworks, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Framework]) - if err != nil { - return fmt.Errorf("cannot collect frameworks: %w", err) - } - - *f = frameworks - return nil -} - func (f Framework) Insert( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/identity.go b/pkg/coredata/identity.go index 71bb89826..314145013 100644 --- a/pkg/coredata/identity.go +++ b/pkg/coredata/identity.go @@ -18,7 +18,6 @@ import ( "context" "errors" "fmt" - "maps" "strings" "time" @@ -54,84 +53,6 @@ func (i Identity) CursorKey(orderBy IdentityOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } -func (i *Identities) LoadByOrganizationID( - ctx context.Context, - conn pg.Conn, - organizationID gid.GID, - cursor *page.Cursor[IdentityOrderField], -) error { - q := ` -SELECT - id, - email_address, - full_name, - hashed_password, - email_address_verified, - saml_subject, - created_at, - updated_at -FROM - identities -WHERE - id IN ( - SELECT identity_id FROM iam_memberships WHERE organization_id = @organization_id - ) - AND %s -` - - q = fmt.Sprintf(q, cursor.SQLFragment()) - - args := pgx.StrictNamedArgs{"organization_id": organizationID} - maps.Copy(args, cursor.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query identities: %w", err) - } - - identities, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Identity]) - if err != nil { - return fmt.Errorf("cannot collect identities: %w", err) - } - - *i = identities - - return nil -} - -func (i *Identities) CountByOrganizationID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - organizationID gid.GID, -) (int, error) { - q := ` -SELECT - COUNT(*) -FROM - identities -WHERE - id IN ( - SELECT identity_id FROM iam_memberships WHERE organization_id = @organization_id AND %s - ) -` - - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"organization_id": organizationID} - maps.Copy(args, scope.SQLArguments()) - - row := conn.QueryRow(ctx, q, args) - - var count int - err := row.Scan(&count) - if err != nil { - return 0, fmt.Errorf("cannot count identities: %w", err) - } - - return count, nil -} - // Tenant id scope is not applied because we want to access identities across all tenants for authentication purposes. func (i *Identity) LoadByEmail( ctx context.Context, diff --git a/pkg/coredata/meeting_attendee.go b/pkg/coredata/meeting_attendee.go index f0d6693f0..32dac2796 100644 --- a/pkg/coredata/meeting_attendee.go +++ b/pkg/coredata/meeting_attendee.go @@ -17,7 +17,6 @@ package coredata import ( "context" "fmt" - "maps" "time" "github.com/jackc/pgx/v5" @@ -36,46 +35,6 @@ type ( MeetingAttendees []*MeetingAttendee ) -func (ma *MeetingAttendees) LoadByMeetingID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - meetingID gid.GID, -) error { - q := ` -SELECT - meeting_id, - attendee_profile_id, - organization_id, - created_at -FROM - meeting_attendees -WHERE - %s - AND meeting_id = @meeting_id -ORDER BY - created_at ASC -` - - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.NamedArgs{"meeting_id": meetingID} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query meeting attendees: %w", err) - } - - attendees, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[MeetingAttendee]) - if err != nil { - return fmt.Errorf("cannot collect meeting attendees: %w", err) - } - - *ma = attendees - return nil -} - func (ma *MeetingAttendees) Merge( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/organization.go b/pkg/coredata/organization.go index 707f44647..238e9b9f7 100644 --- a/pkg/coredata/organization.go +++ b/pkg/coredata/organization.go @@ -183,169 +183,6 @@ WHERE return nil } -func (o *Organizations) LoadAllByIdentityID( - ctx context.Context, - conn pg.Conn, - identityID gid.GID, -) error { - q := ` -WITH identity_org AS ( - SELECT - organization_id - FROM - iam_memberships - WHERE - identity_id = @identity_id -) -SELECT - tenant_id, - id, - name, - description, - website_url, - email, - headquarter_address, - custom_domain_id, - logo_file_id, - horizontal_logo_file_id, - created_at, - updated_at -FROM - organizations -INNER JOIN - identity_org ON organizations.id = identity_org.organization_id -ORDER BY - name ASC -` - - args := pgx.StrictNamedArgs{"identity_id": identityID} - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query organizations: %w", err) - } - - organizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Organization]) - if err != nil { - return fmt.Errorf("cannot collect organizations: %w", err) - } - - *o = organizations - - return nil -} - -func (o *Organizations) LoadAllByIdentityIDWithRole( - ctx context.Context, - conn pg.Conn, - identityID gid.GID, - role MembershipRole, -) error { - q := ` -WITH identity_org AS ( - SELECT - organization_id - FROM - iam_memberships - WHERE - identity_id = @identity_id - AND role = @role -) -SELECT - tenant_id, - id, - name, - description, - website_url, - email, - headquarter_address, - custom_domain_id, - logo_file_id, - horizontal_logo_file_id, - created_at, - updated_at -FROM - organizations -INNER JOIN - identity_org ON organizations.id = identity_org.organization_id -ORDER BY - name ASC -` - - args := pgx.StrictNamedArgs{ - "identity_id": identityID, - "role": role, - } - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query organizations: %w", err) - } - - organizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Organization]) - if err != nil { - return fmt.Errorf("cannot collect organizations: %w", err) - } - - *o = organizations - - return nil -} - -func (o *Organizations) LoadAllByPersonalAPIKeyID( - ctx context.Context, - conn pg.Conn, - personalAPIKeyID gid.GID, -) error { - q := ` -WITH personal_api_key_org AS ( - SELECT - am.organization_id - FROM - iam_personal_api_key_memberships akm - INNER JOIN - iam_memberships am ON akm.membership_id = am.id - WHERE - akm.personal_api_key_id = @personal_api_key_id -) -SELECT - tenant_id, - id, - name, - description, - website_url, - email, - headquarter_address, - custom_domain_id, - logo_file_id, - horizontal_logo_file_id, - created_at, - updated_at -FROM - organizations -INNER JOIN - personal_api_key_org ON organizations.id = personal_api_key_org.organization_id -ORDER BY - name ASC -` - - args := pgx.StrictNamedArgs{"personal_api_key_id": personalAPIKeyID} - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query organizations: %w", err) - } - - organizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Organization]) - if err != nil { - return fmt.Errorf("cannot collect organizations: %w", err) - } - - *o = organizations - - return nil -} - func (o *Organization) Insert( ctx context.Context, conn pg.Conn, @@ -509,49 +346,3 @@ LIMIT 1 return nil } -func (o *Organizations) BatchLoadByID( - ctx context.Context, - conn pg.Conn, - scope Scoper, - organizationIDs []gid.GID, -) error { - q := ` -SELECT - tenant_id, - id, - name, - logo_file_id, - horizontal_logo_file_id, - description, - website_url, - email, - headquarter_address, - custom_domain_id, - created_at, - updated_at -FROM - organizations -WHERE - %s - AND id = ANY(@organization_ids) -` - - q = fmt.Sprintf(q, scope.SQLFragment()) - - args := pgx.StrictNamedArgs{"organization_ids": organizationIDs} - maps.Copy(args, scope.SQLArguments()) - - rows, err := conn.Query(ctx, q, args) - if err != nil { - return fmt.Errorf("cannot query organizations: %w", err) - } - - organizations, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Organization]) - if err != nil { - return fmt.Errorf("cannot collect organizations: %w", err) - } - - *o = organizations - - return nil -} diff --git a/pkg/coredata/saml_request.go b/pkg/coredata/saml_request.go index bccbc26a7..a30a1acbd 100644 --- a/pkg/coredata/saml_request.go +++ b/pkg/coredata/saml_request.go @@ -55,63 +55,6 @@ VALUES (@id, @organization_id, @created_at, @expires_at) return nil } -func (s *SAMLRequest) Load( - ctx context.Context, - conn pg.Conn, - requestID string, - organizationID gid.GID, -) error { - query := ` -SELECT id, organization_id, created_at, expires_at -FROM iam_saml_requests -WHERE id = @id AND organization_id = @organization_id -LIMIT 1 -` - - args := pgx.NamedArgs{ - "id": requestID, - "organization_id": organizationID, - } - - rows, err := conn.Query(ctx, query, args) - if err != nil { - return fmt.Errorf("cannot query saml_requests: %w", err) - } - - req, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[SAMLRequest]) - if err == pgx.ErrNoRows { - return ErrResourceNotFound - } - - if err != nil { - return fmt.Errorf("cannot collect saml_request: %w", err) - } - - *s = req - return nil -} - -func (s *SAMLRequest) IsExpired(now time.Time) bool { - return now.After(s.ExpiresAt) || now.Equal(s.ExpiresAt) -} - -func (s *SAMLRequest) Delete( - ctx context.Context, - conn pg.Conn, -) error { - query := ` -DELETE FROM iam_saml_requests -WHERE id = @id -` - - _, err := conn.Exec(ctx, query, pgx.NamedArgs{"id": s.ID}) - if err != nil { - return fmt.Errorf("cannot delete saml_request: %w", err) - } - - return nil -} - func LoadValidRequestIDsForOrganization( ctx context.Context, conn pg.Conn, diff --git a/pkg/crypto/cipher/cipher.go b/pkg/crypto/cipher/cipher.go index ac761f4d8..aaec58629 100644 --- a/pkg/crypto/cipher/cipher.go +++ b/pkg/crypto/cipher/cipher.go @@ -28,17 +28,6 @@ type ( EncryptionKey [32]byte ) -func NewEncryptionKey(key string) (EncryptionKey, error) { - if len(key) != 32 { - return EncryptionKey{}, fmt.Errorf("key must be 32 bytes for AES-256") - } - - var encryptionKey EncryptionKey - copy(encryptionKey[:], key) - - return encryptionKey, nil -} - func (k EncryptionKey) Bytes() []byte { return k[:] } diff --git a/pkg/gid/tenant_id.go b/pkg/gid/tenant_id.go index 80ccba0f0..7060d8e7f 100644 --- a/pkg/gid/tenant_id.go +++ b/pkg/gid/tenant_id.go @@ -34,22 +34,6 @@ func NewTenantID() TenantID { return defaultTenantGenerator.NewTenantID() } -// ParseTenantID parses a string representation into a TenantID -func ParseTenantID(s string) (TenantID, error) { - var id TenantID - decoded, err := base64.RawURLEncoding.DecodeString(s) - if err != nil { - return NilTenant, fmt.Errorf("invalid tenant ID encoding: %w", err) - } - - if len(decoded) != len(id) { - return NilTenant, fmt.Errorf("invalid tenant ID length: got %d, want %d", len(decoded), len(id)) - } - - copy(id[:], decoded) - return id, nil -} - // newTenantGenerator creates a new generator with machine-specific components func newTenantGenerator() *tenantGenerator { g := &tenantGenerator{ diff --git a/pkg/iam/auth_service.go b/pkg/iam/auth_service.go index 1f91c53a0..6bac9601b 100644 --- a/pkg/iam/auth_service.go +++ b/pkg/iam/auth_service.go @@ -49,11 +49,6 @@ type ( InvitationToken string } - LoadOrCreateIdentityRequest struct { - Email mail.Addr - FullName string - } - CreateIdentityWithPasswordRequest struct { Email mail.Addr Password string @@ -115,14 +110,6 @@ func (req ChangePasswordRequest) Validate() error { return v.Error() } -func (req LoadOrCreateIdentityRequest) Validate() error { - v := validator.New() - - v.Check(req.FullName, "fullName", validator.NotEmpty(), validator.MinLen(1), validator.MaxLen(255)) - - return v.Error() -} - func (req CreateIdentityWithPasswordRequest) Validate() error { v := validator.New() diff --git a/pkg/iam/errors.go b/pkg/iam/errors.go index 396c19460..053e84cc2 100644 --- a/pkg/iam/errors.go +++ b/pkg/iam/errors.go @@ -313,19 +313,6 @@ func (e ErrInvalidCredentials) Error() string { return e.message } -type ErrInvitationNotDeleted struct { - InvitationID gid.GID - Status string -} - -func NewInvitationNotDeletedError(invitationID gid.GID, status string) error { - return &ErrInvitationNotDeleted{InvitationID: invitationID, Status: status} -} - -func (e ErrInvitationNotDeleted) Error() string { - return fmt.Sprintf("cannot delete invitation %q in %q status", e.InvitationID, e.Status) -} - type ErrPasswordAuthenticationRequired struct { Reason string } diff --git a/pkg/iam/policy/action.go b/pkg/iam/policy/action.go deleted file mode 100644 index 3a78eafae..000000000 --- a/pkg/iam/policy/action.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package policy - -import ( - "fmt" - "strings" -) - -// Action represents a permission action in the format "service:resource:operation" -// Examples: "iam:identity:get", "documents:document:write", "risks:risk:delete" -type Action string - -// ActionDefinition provides metadata about an action for documentation and validation. -type ActionDefinition struct { - Action Action - Service string // e.g., "iam", "documents", "risks" - Resource string // e.g., "identity", "document", "risk" - Operation string // e.g., "get", "list", "create", "update", "delete" - Description string -} - -// ActionRegistry holds all registered actions and provides lookup/validation. -type ActionRegistry struct { - actions map[Action]ActionDefinition -} - -// NewActionRegistry creates a new empty action registry. -func NewActionRegistry() *ActionRegistry { - return &ActionRegistry{ - actions: make(map[Action]ActionDefinition), - } -} - -// Register adds an action definition to the registry. -// Returns an error if the action is already registered. -func (r *ActionRegistry) Register(def ActionDefinition) error { - if _, exists := r.actions[def.Action]; exists { - return fmt.Errorf("action %q already registered", def.Action) - } - - // Validate action format - if err := validateActionFormat(def.Action); err != nil { - return fmt.Errorf("invalid action format: %w", err) - } - - r.actions[def.Action] = def - return nil -} - -// MustRegister is like Register but panics on error. -// Useful for setting up registries in application startup. -func (r *ActionRegistry) MustRegister(def ActionDefinition) { - if err := r.Register(def); err != nil { - panic(err) - } -} - -// Get returns the definition for an action, or false if not found. -func (r *ActionRegistry) Get(action Action) (ActionDefinition, bool) { - def, ok := r.actions[action] - return def, ok -} - -// Exists checks if an action is registered. -func (r *ActionRegistry) Exists(action Action) bool { - _, ok := r.actions[action] - return ok -} - -// All returns all registered action definitions. -func (r *ActionRegistry) All() []ActionDefinition { - result := make([]ActionDefinition, 0, len(r.actions)) - for _, def := range r.actions { - result = append(result, def) - } - return result -} - -// ByService returns all actions for a given service. -func (r *ActionRegistry) ByService(service string) []ActionDefinition { - var result []ActionDefinition - for _, def := range r.actions { - if def.Service == service { - result = append(result, def) - } - } - return result -} - -// validateActionFormat ensures action follows "service:resource:operation" format. -func validateActionFormat(action Action) error { - parts := strings.Split(string(action), ":") - if len(parts) != 3 { - return fmt.Errorf("action must have format 'service:resource:operation', got %q", action) - } - - for i, part := range parts { - if part == "" { - return fmt.Errorf("action part %d is empty in %q", i, action) - } - } - - return nil -} - -// ParseAction extracts service, resource, and operation from an action string. -func ParseAction(action Action) (service, resource, operation string, err error) { - parts := strings.Split(string(action), ":") - if len(parts) != 3 { - return "", "", "", fmt.Errorf("invalid action format: %q", action) - } - return parts[0], parts[1], parts[2], nil -} diff --git a/pkg/iam/policy/action_test.go b/pkg/iam/policy/action_test.go deleted file mode 100644 index 5b09d2c7f..000000000 --- a/pkg/iam/policy/action_test.go +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package policy - -import ( - "testing" -) - -func TestActionRegistry_Register(t *testing.T) { - tests := []struct { - name string - def ActionDefinition - wantErr bool - }{ - { - name: "valid action", - def: ActionDefinition{ - Action: "iam:identity:get", - Service: "iam", - Resource: "identity", - Operation: "get", - Description: "Get identity", - }, - wantErr: false, - }, - { - name: "invalid format - missing parts", - def: ActionDefinition{ - Action: "iam:identity", - Service: "iam", - }, - wantErr: true, - }, - { - name: "invalid format - empty part", - def: ActionDefinition{ - Action: "iam::get", - Service: "iam", - }, - wantErr: true, - }, - { - name: "invalid format - too many parts", - def: ActionDefinition{ - Action: "iam:identity:get:extra", - Service: "iam", - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := NewActionRegistry() - err := r.Register(tt.def) - if (err != nil) != tt.wantErr { - t.Errorf("Register() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestActionRegistry_DuplicateRegistration(t *testing.T) { - r := NewActionRegistry() - - def := ActionDefinition{ - Action: "iam:identity:get", - Service: "iam", - Resource: "identity", - Operation: "get", - Description: "Get identity", - } - - // First registration should succeed - if err := r.Register(def); err != nil { - t.Fatalf("First registration failed: %v", err) - } - - // Second registration should fail - if err := r.Register(def); err == nil { - t.Error("Expected error for duplicate registration, got nil") - } -} - -func TestActionRegistry_Get(t *testing.T) { - r := NewActionRegistry() - - def := ActionDefinition{ - Action: "iam:identity:get", - Service: "iam", - Resource: "identity", - Operation: "get", - Description: "Get identity", - } - r.MustRegister(def) - - // Get existing action - got, ok := r.Get("iam:identity:get") - if !ok { - t.Error("Expected to find action") - } - if got.Action != def.Action { - t.Errorf("Got action %v, want %v", got.Action, def.Action) - } - - // Get non-existing action - _, ok = r.Get("iam:identity:delete") - if ok { - t.Error("Expected not to find action") - } -} - -func TestActionRegistry_Exists(t *testing.T) { - r := NewActionRegistry() - - r.MustRegister(ActionDefinition{ - Action: "iam:identity:get", - Service: "iam", - Resource: "identity", - Operation: "get", - }) - - if !r.Exists("iam:identity:get") { - t.Error("Expected action to exist") - } - - if r.Exists("iam:identity:delete") { - t.Error("Expected action not to exist") - } -} - -func TestActionRegistry_ByService(t *testing.T) { - r := NewActionRegistry() - - r.MustRegister(ActionDefinition{Action: "iam:identity:get", Service: "iam", Resource: "identity", Operation: "get"}) - r.MustRegister(ActionDefinition{Action: "iam:identity:update", Service: "iam", Resource: "identity", Operation: "update"}) - r.MustRegister(ActionDefinition{Action: "documents:document:read", Service: "documents", Resource: "document", Operation: "read"}) - - iamActions := r.ByService("iam") - if len(iamActions) != 2 { - t.Errorf("Expected 2 IAM actions, got %d", len(iamActions)) - } - - docActions := r.ByService("documents") - if len(docActions) != 1 { - t.Errorf("Expected 1 documents action, got %d", len(docActions)) - } - - unknownActions := r.ByService("unknown") - if len(unknownActions) != 0 { - t.Errorf("Expected 0 unknown actions, got %d", len(unknownActions)) - } -} - -func TestParseAction(t *testing.T) { - tests := []struct { - action Action - wantSvc string - wantRes string - wantOp string - wantErr bool - }{ - { - action: "iam:identity:get", - wantSvc: "iam", - wantRes: "identity", - wantOp: "get", - wantErr: false, - }, - { - action: "documents:document:read", - wantSvc: "documents", - wantRes: "document", - wantOp: "read", - wantErr: false, - }, - { - action: "invalid", - wantErr: true, - }, - { - action: "invalid:action", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(string(tt.action), func(t *testing.T) { - svc, res, op, err := ParseAction(tt.action) - if (err != nil) != tt.wantErr { - t.Errorf("ParseAction() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !tt.wantErr { - if svc != tt.wantSvc { - t.Errorf("service = %v, want %v", svc, tt.wantSvc) - } - if res != tt.wantRes { - t.Errorf("resource = %v, want %v", res, tt.wantRes) - } - if op != tt.wantOp { - t.Errorf("operation = %v, want %v", op, tt.wantOp) - } - } - }) - } -} diff --git a/pkg/iam/policy/authorizer.go b/pkg/iam/policy/authorizer.go deleted file mode 100644 index 755e1b177..000000000 --- a/pkg/iam/policy/authorizer.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package policy - -import ( - "errors" - "fmt" - "maps" - - "go.probo.inc/probo/pkg/gid" -) - -var ( - // ErrAccessDenied is returned when access is explicitly denied. - ErrAccessDenied = errors.New("access denied") - - // ErrNoMatchingPolicy is returned when no policy grants access (implicit deny). - ErrNoMatchingPolicy = errors.New("no matching policy") -) - -// AccessDeniedError provides detailed information about why access was denied. -type AccessDeniedError struct { - Principal gid.GID - Resource gid.GID - Action string - Reason string - Statement *Statement // The statement that denied access (if explicit deny) -} - -func (e *AccessDeniedError) Error() string { - if e.Statement != nil && e.Statement.SID != "" { - return fmt.Sprintf("access denied: principal %s cannot perform %s on %s (denied by %s)", - e.Principal, e.Action, e.Resource, e.Statement.SID) - } - return fmt.Sprintf("access denied: principal %s cannot perform %s on %s: %s", - e.Principal, e.Action, e.Resource, e.Reason) -} - -func (e *AccessDeniedError) Unwrap() error { - return ErrAccessDenied -} - -// Authorizer evaluates policies to authorize actions. -type Authorizer struct { - evaluator *Evaluator - registry *ActionRegistry -} - -// NewAuthorizer creates a new authorizer with the given action registry. -func NewAuthorizer(registry *ActionRegistry) *Authorizer { - return &Authorizer{ - evaluator: NewEvaluator(), - registry: registry, - } -} - -// AuthorizeParams contains all parameters for an authorization check. -type AuthorizeParams struct { - // Principal is the actor requesting access. - Principal gid.GID - - // Resource is the target resource. - Resource gid.GID - - // Action is the operation being performed. - Action string - - // Policies are the policies to evaluate (typically role-based + self-manage). - Policies []*Policy - - // ResourceAttributes provides additional attributes about the resource - // for condition evaluation (e.g., owner_id, tenant_id). - ResourceAttributes map[string]string -} - -// Authorize checks if the action is allowed based on the provided policies. -// Returns nil if allowed, or an error describing why access was denied. -func (a *Authorizer) Authorize(params AuthorizeParams) error { - // Validate action exists in registry (optional - can be disabled for flexibility) - if a.registry != nil && !a.registry.Exists(Action(params.Action)) { - return &AccessDeniedError{ - Principal: params.Principal, - Resource: params.Resource, - Action: params.Action, - Reason: "unknown action", - } - } - - // Build condition context - conditionCtx := ConditionContext{ - Principal: map[string]string{ - "id": params.Principal.String(), - }, - Resource: map[string]string{ - "id": params.Resource.String(), - }, - } - - // Add resource attributes to context - maps.Copy(conditionCtx.Resource, params.ResourceAttributes) - - // Build authorization request - req := AuthorizationRequest{ - Principal: params.Principal, - Resource: params.Resource, - Action: params.Action, - ConditionContext: conditionCtx, - } - - // Evaluate policies - result := a.evaluator.Evaluate(req, params.Policies) - - switch result.Decision { - case DecisionAllow: - return nil - - case DecisionDeny: - return &AccessDeniedError{ - Principal: params.Principal, - Resource: params.Resource, - Action: params.Action, - Reason: "explicitly denied", - Statement: result.MatchedStatement, - } - - case DecisionNoMatch: - return &AccessDeniedError{ - Principal: params.Principal, - Resource: params.Resource, - Action: params.Action, - Reason: "no policy allows this action", - } - - default: - return &AccessDeniedError{ - Principal: params.Principal, - Resource: params.Resource, - Action: params.Action, - Reason: "unexpected evaluation result", - } - } -} - -// IsAllowed is a convenience method that returns true if access is allowed. -func (a *Authorizer) IsAllowed(params AuthorizeParams) bool { - return a.Authorize(params) == nil -} diff --git a/pkg/iam/policy/authorizer_test.go b/pkg/iam/policy/authorizer_test.go deleted file mode 100644 index 2481650a8..000000000 --- a/pkg/iam/policy/authorizer_test.go +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package policy - -import ( - "errors" - "testing" -) - -func TestAuthorizer_Authorize(t *testing.T) { - // Create a simple registry for testing - registry := NewActionRegistry() - registry.MustRegister(ActionDefinition{ - Action: "test:resource:get", - Service: "test", - Resource: "resource", - Operation: "get", - Description: "Get resource", - }) - registry.MustRegister(ActionDefinition{ - Action: "test:resource:update", - Service: "test", - Resource: "resource", - Operation: "update", - Description: "Update resource", - }) - registry.MustRegister(ActionDefinition{ - Action: "test:resource:delete", - Service: "test", - Resource: "resource", - Operation: "delete", - Description: "Delete resource", - }) - registry.MustRegister(ActionDefinition{ - Action: "test:other:get", - Service: "test", - Resource: "other", - Operation: "get", - Description: "Get other", - }) - - authorizer := NewAuthorizer(registry) - - policies := []*Policy{ - NewPolicy("test", "Test", - Allow("test:resource:get", "test:resource:update"), - Deny("test:resource:delete"), - ), - } - - tests := []struct { - name string - action string - wantErr bool - errType error - }{ - { - name: "allowed action", - action: "test:resource:get", - wantErr: false, - }, - { - name: "denied action", - action: "test:resource:delete", - wantErr: true, - errType: ErrAccessDenied, - }, - { - name: "no matching policy", - action: "test:other:get", - wantErr: true, - errType: ErrAccessDenied, - }, - { - name: "unknown action", - action: "unknown:action:here", - wantErr: true, - errType: ErrAccessDenied, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := authorizer.Authorize(AuthorizeParams{ - Action: tt.action, - Policies: policies, - ResourceAttributes: map[string]string{ - "id": "res_123", - }, - }) - - if (err != nil) != tt.wantErr { - t.Errorf("Authorize() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if tt.wantErr && tt.errType != nil { - if !errors.Is(err, tt.errType) { - t.Errorf("Authorize() error type = %T, want %T", err, tt.errType) - } - } - }) - } -} - -func TestAuthorizer_Authorize_WithConditions(t *testing.T) { - authorizer := NewAuthorizer(nil) - - // Self-manage policy for testing - selfManagePolicy := NewPolicy("self-manage", "Self Manage", - Allow("test:identity:get", "test:identity:update"). - When(Equals("principal.id", "resource.id")), - ) - - tests := []struct { - name string - action string - resourceAttributes map[string]string - wantErr bool - }{ - { - name: "condition not satisfied - GID won't match string", - action: "test:identity:get", - resourceAttributes: map[string]string{ - "id": "user_123", - }, - wantErr: true, // Will fail because principal GID won't match string - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := authorizer.Authorize(AuthorizeParams{ - Action: tt.action, - Policies: []*Policy{selfManagePolicy}, - ResourceAttributes: tt.resourceAttributes, - }) - - if (err != nil) != tt.wantErr { - t.Errorf("Authorize() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestAuthorizer_Authorize_WithoutRegistry(t *testing.T) { - // Authorizer without registry should not validate actions - authorizer := NewAuthorizer(nil) - - policies := []*Policy{ - NewPolicy("test", "Test", Allow("custom:action:here")), - } - - err := authorizer.Authorize(AuthorizeParams{ - Action: "custom:action:here", - Policies: policies, - }) - - if err != nil { - t.Errorf("Expected no error for custom action without registry, got %v", err) - } -} - -func TestAuthorizer_IsAllowed(t *testing.T) { - authorizer := NewAuthorizer(nil) - - allowPolicy := NewPolicy("test", "Test", Allow("test:resource:get")) - denyPolicy := NewPolicy("test", "Test", Deny("test:resource:delete")) - - tests := []struct { - name string - action string - policies []*Policy - want bool - }{ - { - name: "allowed", - action: "test:resource:get", - policies: []*Policy{allowPolicy}, - want: true, - }, - { - name: "denied", - action: "test:resource:delete", - policies: []*Policy{denyPolicy}, - want: false, - }, - { - name: "no match", - action: "test:resource:update", - policies: []*Policy{allowPolicy}, - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := authorizer.IsAllowed(AuthorizeParams{ - Action: tt.action, - Policies: tt.policies, - }) - - if got != tt.want { - t.Errorf("IsAllowed() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestAccessDeniedError(t *testing.T) { - t.Run("error message without statement", func(t *testing.T) { - err := &AccessDeniedError{ - Action: "test:resource:delete", - Reason: "no policy allows this action", - } - - msg := err.Error() - if msg == "" { - t.Error("Expected non-empty error message") - } - }) - - t.Run("error message with statement SID", func(t *testing.T) { - err := &AccessDeniedError{ - Action: "test:resource:delete", - Reason: "explicitly denied", - Statement: &Statement{ - SID: "deny-delete", - }, - } - - msg := err.Error() - if msg == "" { - t.Error("Expected non-empty error message") - } - // Should contain the SID - if !contains(msg, "deny-delete") { - t.Errorf("Expected error message to contain SID, got %q", msg) - } - }) - - t.Run("unwrap returns ErrAccessDenied", func(t *testing.T) { - err := &AccessDeniedError{ - Action: "test:resource:delete", - Reason: "no policy", - } - - if !errors.Is(err, ErrAccessDenied) { - t.Error("Expected error to unwrap to ErrAccessDenied") - } - }) -} - -func contains(s, substr string) bool { - return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr, 0)) -} - -func containsAt(s, substr string, start int) bool { - for i := start; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false -} diff --git a/pkg/iam/policy/example_test.go b/pkg/iam/policy/example_test.go index b2d3cda81..931b15e8a 100644 --- a/pkg/iam/policy/example_test.go +++ b/pkg/iam/policy/example_test.go @@ -20,60 +20,6 @@ import ( "go.probo.inc/probo/pkg/iam/policy" ) -func Example_definingActions() { - // Create an action registry - registry := policy.NewActionRegistry() - - // Register actions for the IAM service - registry.MustRegister(policy.ActionDefinition{ - Action: "iam:identity:get", - Service: "iam", - Resource: "identity", - Operation: "get", - Description: "Get identity details", - }) - - registry.MustRegister(policy.ActionDefinition{ - Action: "iam:identity:update", - Service: "iam", - Resource: "identity", - Operation: "update", - Description: "Update identity", - }) - - // Register actions for the documents service - registry.MustRegister(policy.ActionDefinition{ - Action: "documents:document:read", - Service: "documents", - Resource: "document", - Operation: "read", - Description: "Read a document", - }) - - registry.MustRegister(policy.ActionDefinition{ - Action: "documents:document:write", - Service: "documents", - Resource: "document", - Operation: "write", - Description: "Create or update a document", - }) - - registry.MustRegister(policy.ActionDefinition{ - Action: "documents:document:delete", - Service: "documents", - Resource: "document", - Operation: "delete", - Description: "Delete a document", - }) - - // List all actions for a service - docActions := registry.ByService("documents") - fmt.Printf("Documents service has %d actions\n", len(docActions)) - - // Output: - // Documents service has 3 actions -} - func Example_definingPolicies() { // Define a viewer policy - can read everything viewerPolicy := policy.NewPolicy("viewer", "Viewer Policy", diff --git a/pkg/iam/policy/policy.go b/pkg/iam/policy/policy.go index ecbd82794..da44592fe 100644 --- a/pkg/iam/policy/policy.go +++ b/pkg/iam/policy/policy.go @@ -107,27 +107,3 @@ func NotEquals(key string, values ...string) Condition { } } -// In creates an In condition. -func In(key string, values ...string) Condition { - return Condition{ - Operator: ConditionIn, - Key: key, - Values: values, - } -} - -// NotIn creates a NotIn condition. -func NotIn(key string, values ...string) Condition { - return Condition{ - Operator: ConditionNotIn, - Key: key, - Values: values, - } -} - -// ForEntityType creates a resource pattern for a specific entity type. -func ForEntityType(entityType uint16) ResourcePattern { - return ResourcePattern{ - EntityType: &entityType, - } -} diff --git a/pkg/iam/policy/statement_test.go b/pkg/iam/policy/statement_test.go index a0f3fac7d..2600bd06e 100644 --- a/pkg/iam/policy/statement_test.go +++ b/pkg/iam/policy/statement_test.go @@ -299,15 +299,12 @@ func TestConditionHelpers(t *testing.T) { } }) - t.Run("In helper", func(t *testing.T) { - c := In("principal.role", "admin", "owner") - if c.Operator != ConditionIn { - t.Errorf("Expected ConditionIn, got %v", c.Operator) + t.Run("NotIn condition", func(t *testing.T) { + c := Condition{ + Operator: ConditionNotIn, + Key: "principal.role", + Values: []string{"guest"}, } - }) - - t.Run("NotIn helper", func(t *testing.T) { - c := NotIn("principal.role", "guest") if c.Operator != ConditionNotIn { t.Errorf("Expected ConditionNotIn, got %v", c.Operator) } diff --git a/pkg/iam/scim/bridge/bridge.go b/pkg/iam/scim/bridge/bridge.go index 5bc5de253..b29db6fb1 100644 --- a/pkg/iam/scim/bridge/bridge.go +++ b/pkg/iam/scim/bridge/bridge.go @@ -31,25 +31,11 @@ type ( provider provider.Provider scimClient *scimclient.Client excludedUserNames []string - forceUpdate bool - dryRun bool } Option func(*Bridge) ) -func WithDryRun(dryRun bool) Option { - return func(s *Bridge) { - s.dryRun = dryRun - } -} - -func WithForceUpdate(forceUpdate bool) Option { - return func(s *Bridge) { - s.forceUpdate = forceUpdate - } -} - func WithExcludedUserNames(excludedUserNames []string) Option { return func(s *Bridge) { s.excludedUserNames = excludedUserNames @@ -96,16 +82,13 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate existingSCIM, exists := scimUsersByEmail[email] if !exists { - if !s.dryRun { - if err := s.scimClient.CreateUser(ctx, &pu); err != nil { - errs = append(errs, fmt.Errorf("cannot create user %q: %w", pu.UserName, err)) - continue - } + if err := s.scimClient.CreateUser(ctx, &pu); err != nil { + errs = append(errs, fmt.Errorf("cannot create user %q: %w", pu.UserName, err)) + continue } created++ } else { - needsUpdate := s.forceUpdate || - existingSCIM.Active != pu.Active || + needsUpdate := existingSCIM.Active != pu.Active || existingSCIM.DisplayName != pu.DisplayName || existingSCIM.Title != pu.Title || existingSCIM.GivenName != pu.GivenName || @@ -120,11 +103,9 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate existingSCIM.PreferredLanguage != pu.PreferredLanguage if needsUpdate { - if !s.dryRun { - if err := s.scimClient.UpdateUser(ctx, existingSCIM.ID, &pu); err != nil { - errs = append(errs, fmt.Errorf("cannot update user %q: %w", pu.UserName, err)) - continue - } + if err := s.scimClient.UpdateUser(ctx, existingSCIM.ID, &pu); err != nil { + errs = append(errs, fmt.Errorf("cannot update user %q: %w", pu.UserName, err)) + continue } updated++ } else { @@ -139,11 +120,9 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate } if s.isExcluded(email) { - if !s.dryRun { - if err := s.scimClient.DeleteUser(ctx, scimUser.ID); err != nil { - errs = append(errs, fmt.Errorf("cannot delete user %q: %w", email, err)) - continue - } + if err := s.scimClient.DeleteUser(ctx, scimUser.ID); err != nil { + errs = append(errs, fmt.Errorf("cannot delete user %q: %w", email, err)) + continue } deleted++ continue @@ -153,11 +132,9 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate continue } - if !s.dryRun { - if err := s.scimClient.DeactivateUser(ctx, scimUser.ID); err != nil { - errs = append(errs, fmt.Errorf("cannot deactivate user %q: %w", email, err)) - continue - } + if err := s.scimClient.DeactivateUser(ctx, scimUser.ID); err != nil { + errs = append(errs, fmt.Errorf("cannot deactivate user %q: %w", email, err)) + continue } deactivated++ } diff --git a/pkg/iam/session_service.go b/pkg/iam/session_service.go index fb143cf59..0d0a5c24d 100644 --- a/pkg/iam/session_service.go +++ b/pkg/iam/session_service.go @@ -23,7 +23,6 @@ import ( "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/validator" ) type ( @@ -36,18 +35,6 @@ func NewSessionService(svc *Service) *SessionService { return &SessionService{Service: svc} } -type ( - RevokeAllSessionsRequest struct { - CurrentSessionID gid.GID - } -) - -func (req RevokeAllSessionsRequest) Validate() error { - v := validator.New() - v.Check(req.CurrentSessionID, "current_session_id", validator.GID(coredata.SessionEntityType)) - return v.Error() -} - func (s SessionService) GetSession(ctx context.Context, sessionID gid.GID) (*coredata.Session, error) { var ( session = &coredata.Session{} diff --git a/pkg/probo/custom_domain_service.go b/pkg/probo/custom_domain_service.go index 9bf0b2f6b..6e0e0da60 100644 --- a/pkg/probo/custom_domain_service.go +++ b/pkg/probo/custom_domain_service.go @@ -50,20 +50,6 @@ func (ccdr *CreateCustomDomainRequest) Validate() error { return v.Error() } -func NewCustomDomainService( - svc *TenantService, - acmeService *certmanager.ACMEService, - encryptionKey cipher.EncryptionKey, - logger *log.Logger, -) *CustomDomainService { - return &CustomDomainService{ - svc: svc, - acmeService: acmeService, - encryptionKey: encryptionKey, - logger: logger.Named("custom_domain"), - } -} - func (s *CustomDomainService) CreateCustomDomain( ctx context.Context, req CreateCustomDomainRequest, diff --git a/pkg/probo/organization_service.go b/pkg/probo/organization_service.go index 56463dd78..1acc176a7 100644 --- a/pkg/probo/organization_service.go +++ b/pkg/probo/organization_service.go @@ -36,10 +36,6 @@ type ( fileValidator *filevalidation.FileValidator } - CreateOrganizationRequest struct { - Name string - } - UpdateOrganizationRequest struct { ID gid.GID Name *string @@ -57,14 +53,6 @@ type ( } ) -func (cor *CreateOrganizationRequest) Validate() error { - v := validator.New() - - v.Check(cor.Name, "name", validator.Required(), validator.SafeTextNoNewLine(TitleMaxLength)) - - return v.Error() -} - func (uor *UpdateOrganizationRequest) Validate() error { v := validator.New() diff --git a/pkg/probo/snapshot_service.go b/pkg/probo/snapshot_service.go index 0742f572f..1c396f23c 100644 --- a/pkg/probo/snapshot_service.go +++ b/pkg/probo/snapshot_service.go @@ -30,21 +30,12 @@ type SnapshotService struct { svc *TenantService } -type ( - CreateSnapshotRequest struct { - OrganizationID gid.GID - Name string - Description *string - Type coredata.SnapshotsType - } - - UpdateSnapshotRequest struct { - ID gid.GID - Name *string - Description **string - Type *coredata.SnapshotsType - } -) +type CreateSnapshotRequest struct { + OrganizationID gid.GID + Name string + Description *string + Type coredata.SnapshotsType +} func (csr *CreateSnapshotRequest) Validate() error { v := validator.New() @@ -57,17 +48,6 @@ func (csr *CreateSnapshotRequest) Validate() error { return v.Error() } -func (usr *UpdateSnapshotRequest) Validate() error { - v := validator.New() - - v.Check(usr.ID, "id", validator.Required(), validator.GID(coredata.SnapshotEntityType)) - v.Check(usr.Name, "name", validator.SafeTextNoNewLine(TitleMaxLength)) - v.Check(usr.Description, "description", validator.SafeText(ContentMaxLength)) - v.Check(usr.Type, "type", validator.OneOfSlice(coredata.SnapshotsTypes())) - - return v.Error() -} - func (s *SnapshotService) Get( ctx context.Context, snapshotID gid.GID, diff --git a/pkg/probo/trust_center_access_service.go b/pkg/probo/trust_center_access_service.go index 4ec564b77..f779985b2 100644 --- a/pkg/probo/trust_center_access_service.go +++ b/pkg/probo/trust_center_access_service.go @@ -58,14 +58,6 @@ type ( } ) -func (ctcar *CreateTrustCenterAccessRequest) Validate() error { - v := validator.New() - - v.Check(ctcar.TrustCenterID, "trust_center_id", validator.Required(), validator.GID(coredata.TrustCenterEntityType)) - - return v.Error() -} - func (utcar *UpdateTrustCenterAccessRequest) Validate() error { v := validator.New() diff --git a/pkg/saferedirect/saferedirect.go b/pkg/saferedirect/saferedirect.go index bc146a155..ab537f4a1 100644 --- a/pkg/saferedirect/saferedirect.go +++ b/pkg/saferedirect/saferedirect.go @@ -70,7 +70,3 @@ func (sr *SafeRedirect) Redirect(w http.ResponseWriter, r *http.Request, redirec http.Redirect(w, r, safeURL, statusCode) } -func (sr *SafeRedirect) RedirectFromQuery(w http.ResponseWriter, r *http.Request, paramName, fallbackURL string, statusCode int) { - redirectURL := r.URL.Query().Get(paramName) - sr.Redirect(w, r, redirectURL, fallbackURL, statusCode) -} diff --git a/pkg/saferedirect/saferedirect_test.go b/pkg/saferedirect/saferedirect_test.go index e596b7b82..d6797ea0a 100644 --- a/pkg/saferedirect/saferedirect_test.go +++ b/pkg/saferedirect/saferedirect_test.go @@ -255,96 +255,3 @@ func TestSafeRedirect_Redirect(t *testing.T) { } } -func TestSafeRedirect_RedirectFromQuery(t *testing.T) { - tests := []struct { - name string - allowedHost string - queryParam string - queryValue string - fallbackURL string - expectedStatus int - expectedURL string - }{ - { - name: "safe continue param", - allowedHost: "example.com", - queryParam: "continue", - queryValue: "/dashboard", - fallbackURL: "/home", - expectedStatus: http.StatusFound, - expectedURL: "/dashboard", - }, - { - name: "unsafe continue param", - allowedHost: "example.com", - queryParam: "continue", - queryValue: "https://evil.com/phishing", - fallbackURL: "/home", - expectedStatus: http.StatusFound, - expectedURL: "/home", - }, - { - name: "missing continue param", - allowedHost: "example.com", - queryParam: "continue", - queryValue: "", - fallbackURL: "/home", - expectedStatus: http.StatusFound, - expectedURL: "/home", - }, - { - name: "different query param name", - allowedHost: "example.com", - queryParam: "next", - queryValue: "/profile", - fallbackURL: "/home", - expectedStatus: http.StatusFound, - expectedURL: "/profile", - }, - { - name: "double slash attack in query", - allowedHost: "example.com", - queryParam: "continue", - queryValue: "//evil.com/phishing", - fallbackURL: "/home", - expectedStatus: http.StatusFound, - expectedURL: "/home", - }, - { - name: "slash-backslash attack in query", - allowedHost: "example.com", - queryParam: "continue", - queryValue: "/\\evil.com/phishing", - fallbackURL: "/home", - expectedStatus: http.StatusFound, - expectedURL: "/home", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sr := saferedirect.SafeRedirect{ - AllowedHost: tt.allowedHost, - } - - w := httptest.NewRecorder() - - url := "http://test.com" - if tt.queryValue != "" { - url = "http://test.com?" + tt.queryParam + "=" + tt.queryValue - } - r := httptest.NewRequest("GET", url, nil) - - sr.RedirectFromQuery(w, r, tt.queryParam, tt.fallbackURL, tt.expectedStatus) - - if w.Code != tt.expectedStatus { - t.Errorf("RedirectFromQuery() status = %v, want %v", w.Code, tt.expectedStatus) - } - - location := w.Header().Get("Location") - if location != tt.expectedURL { - t.Errorf("RedirectFromQuery() location = %v, want %v", location, tt.expectedURL) - } - }) - } -} diff --git a/pkg/securecookie/securecookie.go b/pkg/securecookie/securecookie.go index 0845ee422..85d5c6945 100644 --- a/pkg/securecookie/securecookie.go +++ b/pkg/securecookie/securecookie.go @@ -58,24 +58,6 @@ type Config struct { SameSite http.SameSite } -// DefaultConfig returns a default secure cookie configuration -func DefaultConfig(name, secret string, secure bool) Config { - sameSite := http.SameSiteNoneMode // None mode required for SAML (cross-site POST from IdP) - if !secure { - sameSite = http.SameSiteLaxMode - } - - return Config{ - Name: name, - Secret: secret, - Path: "/", - MaxAge: 86400 * 30, // 30 days - Secure: secure, - HTTPOnly: true, - SameSite: sameSite, - } -} - // Set creates and sets a secure cookie with the given value func Set(w http.ResponseWriter, config Config, value string) error { signedValue, err := Sign(value, config.Secret) diff --git a/pkg/server/api/compliancepage/context.go b/pkg/server/api/compliancepage/context.go index 1dc971239..6a0c3021c 100644 --- a/pkg/server/api/compliancepage/context.go +++ b/pkg/server/api/compliancepage/context.go @@ -24,7 +24,6 @@ type ctxKey struct{ name string } var ( compliancePageKey = &ctxKey{name: "compliance_page"} - complianceMembershipKey = &ctxKey{name: "compliance_membership"} compliancePageBaseURLKey = &ctxKey{name: "compliance_page_base_url"} ) @@ -33,11 +32,6 @@ func CompliancePageFromContext(ctx context.Context) *coredata.TrustCenter { return page } -func ComplianceMembershipFromContext(ctx context.Context) *coredata.TrustCenterAccess { - membership, _ := ctx.Value(complianceMembershipKey).(*coredata.TrustCenterAccess) - return membership -} - func CompliancePageBaseURLFromContext(ctx context.Context) *string { page, _ := ctx.Value(compliancePageBaseURLKey).(*string) return page diff --git a/pkg/server/api/console/v1/types/meeting.go b/pkg/server/api/console/v1/types/meeting.go index 73481f3ca..9699e32e8 100644 --- a/pkg/server/api/console/v1/types/meeting.go +++ b/pkg/server/api/console/v1/types/meeting.go @@ -53,16 +53,6 @@ func NewMeetingConnection( } } -func NewMeetingEdges(meetings []*coredata.Meeting, orderBy coredata.MeetingOrderField) []*MeetingEdge { - edges := make([]*MeetingEdge, len(meetings)) - - for i := range edges { - edges[i] = NewMeetingEdge(meetings[i], orderBy) - } - - return edges -} - func NewMeetingEdge(meeting *coredata.Meeting, orderBy coredata.MeetingOrderField) *MeetingEdge { return &MeetingEdge{ Cursor: meeting.CursorKey(orderBy), diff --git a/pkg/server/api/console/v1/types/trust_center_document_access.go b/pkg/server/api/console/v1/types/trust_center_document_access.go index 10effbe5b..6ad385aa0 100644 --- a/pkg/server/api/console/v1/types/trust_center_document_access.go +++ b/pkg/server/api/console/v1/types/trust_center_document_access.go @@ -107,16 +107,6 @@ func NewTrustCenterDocumentAccessConnection( } } -func NewTrustCenterDocumentAccessEdges(accesses []*coredata.TrustCenterDocumentAccess, orderBy coredata.TrustCenterDocumentAccessOrderField) []*TrustCenterDocumentAccessEdge { - edges := make([]*TrustCenterDocumentAccessEdge, len(accesses)) - - for i := range edges { - edges[i] = NewTrustCenterDocumentAccessEdge(accesses[i], orderBy) - } - - return edges -} - func NewTrustCenterDocumentAccessEdge(access *coredata.TrustCenterDocumentAccess, orderBy coredata.TrustCenterDocumentAccessOrderField) *TrustCenterDocumentAccessEdge { return &TrustCenterDocumentAccessEdge{ Cursor: access.CursorKey(orderBy), diff --git a/pkg/server/api/trust/v1/resolver.go b/pkg/server/api/trust/v1/resolver.go index 5ce793909..93bd93bd7 100644 --- a/pkg/server/api/trust/v1/resolver.go +++ b/pkg/server/api/trust/v1/resolver.go @@ -56,24 +56,6 @@ type ( } ) -type ctxKey struct{ name string } - -var ( - trustCenterIDKey = &ctxKey{name: "trust_center_id"} -) - -func TrustCenterIDFromContext(ctx context.Context) gid.GID { - if trustCenterID, ok := ctx.Value(trustCenterIDKey).(gid.GID); ok { - return trustCenterID - } - - return gid.Nil -} - -func ContextWithTrustCenterID(ctx context.Context, trustCenterID gid.GID) context.Context { - return context.WithValue(ctx, trustCenterIDKey, trustCenterID) -} - func NewMux( logger *log.Logger, iamSvc *iam.Service, diff --git a/pkg/server/gqlutils/errors.go b/pkg/server/gqlutils/errors.go index 8c53044c6..f4b74ee5a 100644 --- a/pkg/server/gqlutils/errors.go +++ b/pkg/server/gqlutils/errors.go @@ -62,10 +62,6 @@ func AssumptionRequired(ctx context.Context, err error) *gqlerror.Error { } } -func AssumptionRequiredf(ctx context.Context, format string, a ...any) *gqlerror.Error { - return AssumptionRequired(ctx, fmt.Errorf(format, a...)) -} - func FullNameRequired(ctx context.Context, err error) *gqlerror.Error { return &gqlerror.Error{ Message: err.Error(), @@ -104,10 +100,6 @@ func AccountAlreadyActivated(ctx context.Context, err error) *gqlerror.Error { } } -func AccountAlreadyActivatedf(ctx context.Context, format string, a ...any) *gqlerror.Error { - return AccountAlreadyActivated(ctx, fmt.Errorf(format, a...)) -} - func Forbidden(ctx context.Context, err error) *gqlerror.Error { return &gqlerror.Error{ Message: err.Error(), diff --git a/pkg/server/gqlutils/types/bigint/bigint.go b/pkg/server/gqlutils/types/bigint/bigint.go index a99674483..8a73d78f9 100644 --- a/pkg/server/gqlutils/types/bigint/bigint.go +++ b/pkg/server/gqlutils/types/bigint/bigint.go @@ -58,3 +58,4 @@ func UnmarshalBigIntScalar(v any) (int64, error) { return 0, fmt.Errorf("cannot unmarshal %T into BigInt", v) } } + diff --git a/pkg/server/statichandler/statichandler.go b/pkg/server/statichandler/statichandler.go index 5c4526be5..b7e1bbd13 100644 --- a/pkg/server/statichandler/statichandler.go +++ b/pkg/server/statichandler/statichandler.go @@ -32,13 +32,6 @@ type GzipOptions struct { FileTypes []string } -func DefaultGzipOptions() GzipOptions { - return GzipOptions{ - EnableFileTypeCheck: true, - FileTypes: []string{".js", ".css", ".html"}, - } -} - type Server struct { spaFS http.FileSystem etags map[string]string diff --git a/pkg/validator/checkeach_slice_test.go b/pkg/validator/checkeach_slice_test.go index 8f0588a5c..aff128b09 100644 --- a/pkg/validator/checkeach_slice_test.go +++ b/pkg/validator/checkeach_slice_test.go @@ -32,7 +32,7 @@ func TestCheckEach_EmptyTypedSlice(t *testing.T) { t.Error("callback should not be called for empty slice") }) - if v.HasErrors() { + if v.Error() != nil { t.Errorf("unexpected error for empty slice: %v", v.Error()) } } @@ -63,7 +63,7 @@ func TestCheckEach_NonEmptyTypedSlice(t *testing.T) { t.Errorf("expected callback to be called 2 times, got %d", callCount) } - if v.HasErrors() { + if v.Error() != nil { t.Errorf("unexpected error: %v", v.Error()) } } @@ -79,7 +79,7 @@ func TestCheckEach_NilTypedSlice(t *testing.T) { t.Error("callback should not be called for nil slice") }) - if v.HasErrors() { + if v.Error() != nil { t.Errorf("unexpected error for nil slice: %v", v.Error()) } } @@ -108,7 +108,7 @@ func TestCheckEach_PointerToNonEmptySlice(t *testing.T) { t.Errorf("expected callback to be called 3 times, got %d", callCount) } - if v.HasErrors() { + if v.Error() != nil { t.Errorf("unexpected error for pointer to slice: %v", v.Error()) } } @@ -124,7 +124,7 @@ func TestCheckEach_PointerToEmptySlice(t *testing.T) { t.Error("callback should not be called for empty slice") }) - if v.HasErrors() { + if v.Error() != nil { t.Errorf("unexpected error for pointer to empty slice: %v", v.Error()) } } @@ -139,7 +139,7 @@ func TestCheckEach_NilPointerToSlice(t *testing.T) { t.Error("callback should not be called for nil pointer to slice") }) - if v.HasErrors() { + if v.Error() != nil { t.Errorf("unexpected error for nil pointer to slice: %v", v.Error()) } } @@ -169,7 +169,7 @@ func TestCheckEach_DoublePointerToSlice(t *testing.T) { t.Errorf("expected callback to be called 2 times, got %d", callCount) } - if v.HasErrors() { + if v.Error() != nil { t.Errorf("unexpected error for double pointer to slice: %v", v.Error()) } } @@ -184,11 +184,11 @@ func TestCheckEach_NonSliceValue(t *testing.T) { t.Error("callback should not be called for non-slice value") }) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected error for non-slice value") } - errors := v.Errors() + errors := v.Error().(ValidationErrors) if len(errors) != 1 { t.Errorf("expected 1 error, got %d", len(errors)) } diff --git a/pkg/validator/double_pointer_test.go b/pkg/validator/double_pointer_test.go index 27d94a1da..76414aa88 100644 --- a/pkg/validator/double_pointer_test.go +++ b/pkg/validator/double_pointer_test.go @@ -29,7 +29,7 @@ func TestDoublePointerValidation(t *testing.T) { v.Check(doublePtr, "name", validator.Required(), validator.NotEmpty(), validator.MaxLen(1000)) - if v.HasErrors() { + if v.Error() != nil { t.Errorf("expected no errors, got: %v", v.Error()) } }) @@ -42,7 +42,7 @@ func TestDoublePointerValidation(t *testing.T) { v.Check(doublePtr, "name", validator.Required(), validator.NotEmpty()) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected errors for empty string") } }) @@ -55,7 +55,7 @@ func TestDoublePointerValidation(t *testing.T) { v.Check(doublePtr, "name", validator.Required(), validator.MaxLen(10)) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected errors for string exceeding max length") } }) @@ -66,7 +66,7 @@ func TestDoublePointerValidation(t *testing.T) { v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000)) - if v.HasErrors() { + if v.Error() != nil { t.Errorf("expected no errors for nil optional field, got: %v", v.Error()) } }) @@ -78,7 +78,7 @@ func TestDoublePointerValidation(t *testing.T) { v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000)) - if v.HasErrors() { + if v.Error() != nil { t.Errorf("expected no errors for nil optional field, got: %v", v.Error()) } }) @@ -91,7 +91,7 @@ func TestDoublePointerValidation(t *testing.T) { v.Check(doublePtr, "name", validator.NotEmpty(), validator.MaxLen(1000)) - if v.HasErrors() { + if v.Error() != nil { t.Errorf("expected no errors, got: %v", v.Error()) } }) diff --git a/pkg/validator/oneof_custom_type_test.go b/pkg/validator/oneof_custom_type_test.go index 0f220272b..c4bffd19e 100644 --- a/pkg/validator/oneof_custom_type_test.go +++ b/pkg/validator/oneof_custom_type_test.go @@ -69,11 +69,11 @@ func TestOneOf_CustomStringType(t *testing.T) { v.Check(tt.value, "asset_type", OneOfSlice(tt.allowed)) if tt.expectError { - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected error but got none") } } else { - if v.HasErrors() { + if v.Error() != nil { t.Errorf("unexpected error: %v", v.Error()) } } diff --git a/pkg/validator/optional_pointer_test.go b/pkg/validator/optional_pointer_test.go index 15ed9ce4f..52f53da77 100644 --- a/pkg/validator/optional_pointer_test.go +++ b/pkg/validator/optional_pointer_test.go @@ -56,11 +56,11 @@ func TestOptional_WithGIDPointer(t *testing.T) { v.Check(tt.value, "owner_id", GID(100)) if tt.expectError { - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected error but got none") } } else { - if v.HasErrors() { + if v.Error() != nil { t.Errorf("unexpected error: %v", v.Error()) } } @@ -100,14 +100,14 @@ func TestOptional_WithCustomTypePointer(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := New() - v.Check(tt.value, "asset_type", OneOf("VALID", "ANOTHER")) + v.Check(tt.value, "asset_type", OneOfSlice([]string{"VALID", "ANOTHER"})) if tt.expectError { - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected error but got none") } } else { - if v.HasErrors() { + if v.Error() != nil { t.Errorf("unexpected error: %v", v.Error()) } } diff --git a/pkg/validator/validation.go b/pkg/validator/validation.go index 368cd0274..c59bd8961 100644 --- a/pkg/validator/validation.go +++ b/pkg/validator/validation.go @@ -15,7 +15,6 @@ package validator import ( - "fmt" "reflect" ) @@ -97,29 +96,6 @@ func (v *Validator) CheckEach(items any, field string, fn func(index int, item a } } -func (v *Validator) CheckNested(field string, fn func(v *Validator)) { - nestedValidator := New() - fn(nestedValidator) - - for _, err := range nestedValidator.errors { - prefixedErr := &ValidationError{ - Field: fmt.Sprintf("%s.%s", field, err.Field), - Code: err.Code, - Message: err.Message, - Value: err.Value, - } - v.errors = append(v.errors, prefixedErr) - } -} - -func (v *Validator) HasErrors() bool { - return len(v.errors) > 0 -} - -func (v *Validator) Errors() ValidationErrors { - return v.errors -} - func (v *Validator) Error() error { if len(v.errors) == 0 { return nil diff --git a/pkg/validator/validation_bench_test.go b/pkg/validator/validation_bench_test.go index 19dd14e09..06fbf6afa 100644 --- a/pkg/validator/validation_bench_test.go +++ b/pkg/validator/validation_bench_test.go @@ -53,61 +53,6 @@ func BenchmarkValidate_OptionalField(b *testing.B) { } } -func BenchmarkValidate_NestedStruct(b *testing.B) { - type Address struct { - City string - ZipCode string - } - - type User struct { - Name string - Address Address - } - - user := User{ - Name: "John Doe", - Address: Address{ - City: "New York", - ZipCode: "10001", - }, - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - v := New() - v.Check(&user.Name, "name", Required()) - v.CheckNested("address", func(av *Validator) { - av.Check(&user.Address.City, "city", Required()) - av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, "")) - }) - } -} - -func BenchmarkValidate_ArrayValidation(b *testing.B) { - type Item struct { - Name string - Price int - } - - items := []Item{ - {Name: "Item 1", Price: 100}, - {Name: "Item 2", Price: 200}, - {Name: "Item 3", Price: 300}, - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - v := New() - for j, item := range items { - v.CheckNested("items[0]", func(iv *Validator) { - iv.Check(&item.Name, "name", Required()) - iv.Check(&item.Price, "price", Min(0)) - _ = j - }) - } - } -} - func BenchmarkURL(b *testing.B) { urlStr := "https://example.com" validator := URL() @@ -118,16 +63,6 @@ func BenchmarkURL(b *testing.B) { } } -func BenchmarkUUID(b *testing.B) { - uuid := "550e8400-e29b-41d4-a716-446655440000" - validator := UUID() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&uuid) - } -} - func BenchmarkMinLen(b *testing.B) { str := "hello world" validator := MinLen(5) @@ -148,36 +83,6 @@ func BenchmarkMin(b *testing.B) { } } -func BenchmarkMinFloat(b *testing.B) { - num := 99.99 - validator := MinFloat(0.01) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&num) - } -} - -func BenchmarkMaxFloat(b *testing.B) { - num := 50.50 - validator := MaxFloat(99.99) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&num) - } -} - -func BenchmarkRangeFloat(b *testing.B) { - num := 50.50 - validator := RangeFloat(0.01, 99.99) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&num) - } -} - func BenchmarkNotEmpty(b *testing.B) { str := "hello world" validator := NotEmpty() @@ -188,16 +93,6 @@ func BenchmarkNotEmpty(b *testing.B) { } } -func BenchmarkPattern(b *testing.B) { - zipCode := "12345" - validator := Pattern(`^\d{5}$`, "") - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&zipCode) - } -} - func BenchmarkValidate_WithErrors(b *testing.B) { email := "" @@ -205,7 +100,7 @@ func BenchmarkValidate_WithErrors(b *testing.B) { for i := 0; i < b.N; i++ { v := New() v.Check(&email, "email", Required(), NotEmpty()) - if !v.HasErrors() { + if v.Error() == nil { b.Fatal("expected validation error") } } @@ -251,73 +146,6 @@ func BenchmarkValidate_ComplexForm(b *testing.B) { v.Check(&user.Age, "age", Min(18), Max(120)) v.Check(user.Website, "website", URL()) v.Check(user.PhoneNumber, "phoneNumber", MinLen(10)) - v.Check(&user.Price, "price", MinFloat(0.01)) - - v.CheckNested("address", func(av *Validator) { - av.Check(&user.Address.Street, "street", Required()) - av.Check(&user.Address.City, "city", Required()) - av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, "")) - }) - } -} - -func BenchmarkMinItems(b *testing.B) { - items := []string{"a", "b", "c"} - validator := MinItems(2) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&items) - } -} - -func BenchmarkMaxItems(b *testing.B) { - items := []string{"a", "b", "c"} - validator := MaxItems(5) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&items) - } -} - -func BenchmarkUniqueItems(b *testing.B) { - items := []string{"a", "b", "c"} - validator := UniqueItems() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&items) - } -} - -func BenchmarkAlphaNumeric(b *testing.B) { - str := "abc123DEF456" - validator := AlphaNumeric() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&str) - } -} - -func BenchmarkNoSpaces(b *testing.B) { - str := "hello-world-test" - validator := NoSpaces() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&str) - } -} - -func BenchmarkSlug(b *testing.B) { - str := "hello-world-123" - validator := Slug() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&str) } } @@ -343,47 +171,6 @@ func BenchmarkBefore(b *testing.B) { } } -func BenchmarkFutureDate(b *testing.B) { - future := time.Now().Add(24 * time.Hour) - validator := FutureDate() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&future) - } -} - -func BenchmarkPastDate(b *testing.B) { - past := time.Now().Add(-24 * time.Hour) - validator := PastDate() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&past) - } -} - -func BenchmarkEqualTo(b *testing.B) { - str1 := "password" - str2 := "password" - validator := EqualTo(&str2) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&str1) - } -} - -func BenchmarkNotEqualTo(b *testing.B) { - str1 := "password" - str2 := "different" - validator := NotEqualTo(&str2) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&str1) - } -} func BenchmarkDomain(b *testing.B) { str := "api.example.com" validator := Domain() @@ -394,16 +181,6 @@ func BenchmarkDomain(b *testing.B) { } } -func BenchmarkHTTPUrl(b *testing.B) { - str := "http://api.example.com/v1/users" - validator := HTTPUrl() - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = validator(&str) - } -} - func BenchmarkHTTPSUrl(b *testing.B) { str := "https://api.example.com/v1/users" validator := HTTPSUrl() diff --git a/pkg/validator/validation_test.go b/pkg/validator/validation_test.go index c60c3ff6a..cde1f4acd 100644 --- a/pkg/validator/validation_test.go +++ b/pkg/validator/validation_test.go @@ -15,7 +15,6 @@ package validator import ( - "fmt" "testing" "go.probo.inc/probo/pkg/mail" @@ -28,8 +27,8 @@ func TestValidator_Validate(t *testing.T) { v.Check(&email, "email", Required(), NotEmpty()) - if v.HasErrors() { - t.Errorf("expected no errors, got: %v", v.Errors()) + if v.Error() != nil { + t.Errorf("expected no errors, got: %v", v.Error().(ValidationErrors)) } }) @@ -41,11 +40,11 @@ func TestValidator_Validate(t *testing.T) { v.Check(email, "email", NotEmpty()) v.Check(&password, "password", Required(), MinLen(8)) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected validation errors") } - errors := v.Errors() + errors := v.Error().(ValidationErrors) // email: 1 error (Required), password: 1 error (MinLen - Required passes because it's not empty) if len(errors) != 2 { t.Errorf("expected 2 errors, got %d: %v", len(errors), errors) @@ -58,7 +57,7 @@ func TestValidator_Validate(t *testing.T) { v.Check(&value, "password", MinLen(8), MaxLen(5)) - errors := v.Errors() + errors := v.Error().(ValidationErrors) // Both MinLen and MaxLen will fail (too short and somehow conflicts, but logically MinLen will fail) if len(errors) < 1 { t.Errorf("expected at least 1 error, got %d", len(errors)) @@ -66,41 +65,6 @@ func TestValidator_Validate(t *testing.T) { }) } -func TestValidator_CheckNested(t *testing.T) { - v := New() - - v.CheckNested("user", func(nv *Validator) { - email := mail.Nil - nv.Check(&email, "email", NotEmpty()) - - nv.CheckNested("address", func(av *Validator) { - city := "" - av.Check(&city, "city", Required()) - }) - }) - - if !v.HasErrors() { - t.Error("expected validation errors") - } - - errors := v.Errors() - if len(errors) != 2 { - t.Errorf("expected 2 errors, got %d", len(errors)) - } - - // Check field paths - expectedFields := map[string]bool{ - "user.email": true, - "user.address.city": true, - } - - for _, err := range errors { - if !expectedFields[err.Field] { - t.Errorf("unexpected field path: %s", err.Field) - } - } -} - func TestValidator_Error(t *testing.T) { t.Run("no errors", func(t *testing.T) { v := New() @@ -192,11 +156,11 @@ func TestOptionalFieldExample(t *testing.T) { v.Check(req.PhoneNumber, "phoneNumber", MinLen(10)) v.Check(req.Age, "age", Min(18), Max(120)) - if !v.HasErrors() { + if v.Error() == nil { t.Fatal("expected validation errors") } - errors := v.Errors() + errors := v.Error().(ValidationErrors) websiteErr := errors.ByField("website") if len(websiteErr) != 1 { @@ -216,140 +180,17 @@ func TestOptionalFieldExample(t *testing.T) { t.Logf("Optional field validation errors: %s", errors.Error()) } -func TestRealWorldExample(t *testing.T) { - // Simulate a user registration form - type Address struct { - City string - ZipCode string - } - - type User struct { - Email string - Password string - Age int - Website *string - Address Address - } - - user := User{ - Email: "", - Password: "123", - Age: 15, - Website: new("not-a-url"), - Address: Address{ - City: "", - ZipCode: "12345", - }, - } - - v := New() - - // Validate user fields - v.Check(&user.Email, "email", Required(), NotEmpty()) - v.Check(&user.Password, "password", Required(), MinLen(8)) - v.Check(&user.Age, "age", Min(18), Max(120)) - v.Check(user.Website, "website", URL()) - - // Validate nested address - v.CheckNested("address", func(av *Validator) { - av.Check(&user.Address.City, "city", Required()) - av.Check(&user.Address.ZipCode, "zipCode", Pattern(`^\d{5}$`, "must be 5 digits")) - }) - - if !v.HasErrors() { - t.Fatal("expected validation errors") - } - - errors := v.Errors() - expectedErrors := map[string]ErrorCode{ - "email": ErrorCodeRequired, - "password": ErrorCodeTooShort, - "age": ErrorCodeOutOfRange, - "website": ErrorCodeInvalidURL, - "address.city": ErrorCodeRequired, - } - - // Check that we have the expected errors - for field, expectedCode := range expectedErrors { - found := false - for _, err := range errors { - if err.Field == field && err.Code == expectedCode { - found = true - break - } - } - if !found { - t.Errorf("expected error for field '%s' with code '%s'", field, expectedCode) - } - } - - // Print errors for debugging - t.Logf("Validation errors: %s", errors.Error()) -} - -func TestArrayValidation(t *testing.T) { - type Item struct { - Name string - Price int - } - - items := []Item{ - {Name: "", Price: -10}, - {Name: "Valid", Price: 100}, - {Name: "X", Price: 10}, - } - - v := New() - - // Validate each item - for i, item := range items { - field := fmt.Sprintf("items[%d]", i) - v.CheckNested(field, func(iv *Validator) { - iv.Check(&item.Name, "name", Required(), MinLen(2)) - iv.Check(&item.Price, "price", Min(0)) - }) - } - - if !v.HasErrors() { - t.Fatal("expected validation errors") - } - - errors := v.Errors() - - // Check for specific field paths - expectedFields := []string{ - "items[0].name", - "items[0].price", - "items[2].name", - } - - for _, expectedField := range expectedFields { - found := false - for _, err := range errors { - if err.Field == expectedField { - found = true - break - } - } - if !found { - t.Errorf("expected error for field '%s'", expectedField) - } - } - - t.Logf("Array validation errors: %s", errors.Error()) -} - func TestDuplicateValidators(t *testing.T) { t.Run("duplicate MinLen creates two errors", func(t *testing.T) { v := New() name := "abc" v.Check(&name, "name", MinLen(5), MinLen(5)) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected validation errors") } - errors := v.Errors() + errors := v.Error().(ValidationErrors) if len(errors) != 2 { t.Errorf("expected 2 errors (one per MinLen), got %d", len(errors)) } @@ -367,7 +208,7 @@ func TestDuplicateValidators(t *testing.T) { name := "" v.Check(&name, "name", Required(), Required()) - errors := v.Errors() + errors := v.Error().(ValidationErrors) if len(errors) != 2 { t.Errorf("expected 2 errors, got %d", len(errors)) } @@ -378,7 +219,7 @@ func TestDuplicateValidators(t *testing.T) { email := mail.Nil v.Check(&email, "email", NotEmpty(), NotEmpty()) - errors := v.Errors() + errors := v.Error().(ValidationErrors) if len(errors) != 2 { t.Errorf("expected 2 errors, got %d", len(errors)) } @@ -389,7 +230,7 @@ func TestDuplicateValidators(t *testing.T) { name := "test" v.Check(&name, "name", MinLen(5), MinLen(10)) - errors := v.Errors() + errors := v.Error().(ValidationErrors) if len(errors) != 2 { t.Errorf("expected 2 errors, got %d", len(errors)) } diff --git a/pkg/validator/validator_collection.go b/pkg/validator/validator_collection.go deleted file mode 100644 index aefc02856..000000000 --- a/pkg/validator/validator_collection.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package validator - -import ( - "fmt" - "reflect" -) - -// MinItems validates that a slice or array has at least the specified minimum number of items. -func MinItems(min int) ValidatorFunc { - return func(value any) *ValidationError { - v := reflect.ValueOf(value) - if v.Kind() == reflect.Pointer { - if v.IsNil() { - return nil - } - v = v.Elem() - } - - if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { - return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array") - } - - if v.Len() < min { - return newValidationError( - ErrorCodeOutOfRange, - fmt.Sprintf("must contain at least %d items", min), - ) - } - - return nil - } -} - -// MaxItems validates that a slice or array does not exceed the specified maximum number of items. -func MaxItems(max int) ValidatorFunc { - return func(value any) *ValidationError { - v := reflect.ValueOf(value) - if v.Kind() == reflect.Pointer { - if v.IsNil() { - return nil - } - v = v.Elem() - } - - if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { - return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array") - } - - if v.Len() > max { - return newValidationError( - ErrorCodeOutOfRange, - fmt.Sprintf("must contain at most %d items", max), - ) - } - - return nil - } -} - -// UniqueItems validates that all items in a slice or array are unique. -func UniqueItems() ValidatorFunc { - return func(value any) *ValidationError { - v := reflect.ValueOf(value) - if v.Kind() == reflect.Pointer { - if v.IsNil() { - return nil - } - v = v.Elem() - } - - if v.Kind() != reflect.Slice && v.Kind() != reflect.Array { - return newValidationError(ErrorCodeInvalidFormat, "value must be a slice or array") - } - - if v.Len() == 0 { - return nil - } - - elemType := v.Type().Elem() - if !elemType.Comparable() { - return newValidationError(ErrorCodeInvalidFormat, "cannot validate uniqueness for non-comparable types") - } - - seen := make(map[any]bool) - for i := 0; i < v.Len(); i++ { - item := v.Index(i).Interface() - if seen[item] { - return newValidationError(ErrorCodeInvalidFormat, "items must be unique") - } - seen[item] = true - } - - return nil - } -} diff --git a/pkg/validator/validator_collection_test.go b/pkg/validator/validator_collection_test.go deleted file mode 100644 index 0d045113a..000000000 --- a/pkg/validator/validator_collection_test.go +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package validator - -import ( - "testing" -) - -func TestMinItems(t *testing.T) { - t.Run("valid slice", func(t *testing.T) { - items := []string{"a", "b", "c"} - err := MinItems(2)(&items) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("exact minimum", func(t *testing.T) { - items := []int{1, 2} - err := MinItems(2)(&items) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("too few items", func(t *testing.T) { - items := []string{"a"} - err := MinItems(2)(&items) - if err == nil { - t.Fatal("expected validation error") - } else if err.Code != ErrorCodeOutOfRange { - t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code) - } - }) - - t.Run("nil slice", func(t *testing.T) { - var items *[]string - err := MinItems(2)(items) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) - - t.Run("non-slice value", func(t *testing.T) { - value := "not a slice" - err := MinItems(2)(&value) - if err == nil || err.Code != ErrorCodeInvalidFormat { - t.Error("expected invalid format error") - } - }) -} - -func TestMaxItems(t *testing.T) { - t.Run("valid slice", func(t *testing.T) { - items := []string{"a", "b"} - err := MaxItems(5)(&items) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("exact maximum", func(t *testing.T) { - items := []int{1, 2, 3} - err := MaxItems(3)(&items) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("too many items", func(t *testing.T) { - items := []string{"a", "b", "c", "d"} - err := MaxItems(2)(&items) - if err == nil { - t.Fatal("expected validation error") - } else if err.Code != ErrorCodeOutOfRange { - t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code) - } - }) - - t.Run("nil slice", func(t *testing.T) { - var items *[]string - err := MaxItems(2)(items) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) -} - -func TestUniqueItems(t *testing.T) { - t.Run("unique items", func(t *testing.T) { - items := []string{"a", "b", "c"} - err := UniqueItems()(&items) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("duplicate items", func(t *testing.T) { - items := []string{"a", "b", "a"} - err := UniqueItems()(&items) - if err == nil { - t.Fatal("expected validation error") - } else if err.Code != ErrorCodeInvalidFormat { - t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code) - } - }) - - t.Run("unique integers", func(t *testing.T) { - items := []int{1, 2, 3} - err := UniqueItems()(&items) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("duplicate integers", func(t *testing.T) { - items := []int{1, 2, 1} - err := UniqueItems()(&items) - if err == nil { - t.Error("expected validation error") - } - }) - - t.Run("nil slice", func(t *testing.T) { - var items *[]string - err := UniqueItems()(items) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) - - t.Run("empty slice", func(t *testing.T) { - items := []string{} - err := UniqueItems()(&items) - if err != nil { - t.Errorf("expected no error for empty slice, got: %v", err) - } - }) - - t.Run("non-comparable type - slice of slices", func(t *testing.T) { - items := [][]int{{1, 2}, {3, 4}} - err := UniqueItems()(&items) - if err == nil { - t.Fatal("expected validation error for non-comparable type") - } else { - if err.Code != ErrorCodeInvalidFormat { - t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code) - } - if err.Message != "cannot validate uniqueness for non-comparable types" { - t.Errorf("unexpected error message: %s", err.Message) - } - } - }) - - t.Run("non-comparable type - slice of maps", func(t *testing.T) { - items := []map[string]int{{"a": 1}, {"b": 2}} - err := UniqueItems()(&items) - if err == nil { - t.Fatal("expected validation error for non-comparable type") - } else if err.Code != ErrorCodeInvalidFormat { - t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code) - } - }) - - t.Run("non-comparable type - struct with slice field", func(t *testing.T) { - type NonComparable struct { - Items []int - } - items := []NonComparable{{Items: []int{1, 2}}, {Items: []int{3, 4}}} - err := UniqueItems()(&items) - if err == nil { - t.Fatal("expected validation error for non-comparable type") - } else if err.Code != ErrorCodeInvalidFormat { - t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code) - } - }) - - t.Run("comparable struct with unique values", func(t *testing.T) { - type ComparableStruct struct { - ID int - Name string - } - items := []ComparableStruct{{ID: 1, Name: "a"}, {ID: 2, Name: "b"}} - err := UniqueItems()(&items) - if err != nil { - t.Errorf("expected no error for comparable structs, got: %v", err) - } - }) - - t.Run("comparable struct with duplicate values", func(t *testing.T) { - type ComparableStruct struct { - ID int - Name string - } - items := []ComparableStruct{{ID: 1, Name: "a"}, {ID: 2, Name: "b"}, {ID: 1, Name: "a"}} - err := UniqueItems()(&items) - if err == nil { - t.Fatal("expected validation error for duplicate comparable structs") - } else if err.Code != ErrorCodeInvalidFormat { - t.Errorf("expected error code %s, got %s", ErrorCodeInvalidFormat, err.Code) - } - }) -} diff --git a/pkg/validator/validator_common_test.go b/pkg/validator/validator_common_test.go index 41b2999c3..ceb8ecb35 100644 --- a/pkg/validator/validator_common_test.go +++ b/pkg/validator/validator_common_test.go @@ -23,8 +23,8 @@ func TestOptionalByDefault(t *testing.T) { v := New() v.Check(nil, "field", MinLen(5)) - if v.HasErrors() { - t.Errorf("expected no errors for nil (optional by default), got: %v", v.Errors()) + if v.Error() != nil { + t.Errorf("expected no errors for nil (optional by default), got: %v", v.Error()) } }) @@ -33,8 +33,8 @@ func TestOptionalByDefault(t *testing.T) { var str *string v.Check(str, "field", MinLen(5)) - if v.HasErrors() { - t.Errorf("expected no errors for nil pointer (optional by default), got: %v", v.Errors()) + if v.Error() != nil { + t.Errorf("expected no errors for nil pointer (optional by default), got: %v", v.Error()) } }) @@ -43,8 +43,8 @@ func TestOptionalByDefault(t *testing.T) { str := "hello world" v.Check(&str, "field", MinLen(5)) - if v.HasErrors() { - t.Errorf("expected no errors, got: %v", v.Errors()) + if v.Error() != nil { + t.Errorf("expected no errors, got: %v", v.Error()) } }) @@ -53,7 +53,7 @@ func TestOptionalByDefault(t *testing.T) { str := "hi" v.Check(&str, "field", MinLen(5)) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected validation error") } }) @@ -63,8 +63,8 @@ func TestOptionalByDefault(t *testing.T) { str := "hello" v.Check(&str, "field", MinLen(3), MaxLen(10)) - if v.HasErrors() { - t.Errorf("expected no errors, got: %v", v.Errors()) + if v.Error() != nil { + t.Errorf("expected no errors, got: %v", v.Error()) } }) @@ -73,7 +73,7 @@ func TestOptionalByDefault(t *testing.T) { str := "" v.Check(&str, "field", MinLen(5)) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected validation error for empty string") } }) @@ -83,7 +83,7 @@ func TestOptionalByDefault(t *testing.T) { var str *string v.Check(str, "field", Required()) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected validation error for nil with Required()") } }) diff --git a/pkg/validator/validator_conditional.go b/pkg/validator/validator_conditional.go deleted file mode 100644 index 3d6d70efc..000000000 --- a/pkg/validator/validator_conditional.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package validator - -import ( - "reflect" - "time" -) - -// EqualTo validates that a value equals another value using deep equality. -// Special handling for time.Time to compare instants rather than internal structure. -func EqualTo(other any) ValidatorFunc { - return func(value any) *ValidationError { - if !areEqual(value, other) { - return newValidationError(ErrorCodeInvalidFormat, "values must match") - } - return nil - } -} - -// NotEqualTo validates that a value does not equal another value using deep equality. -// Special handling for time.Time to compare instants rather than internal structure. -func NotEqualTo(other any) ValidatorFunc { - return func(value any) *ValidationError { - if areEqual(value, other) { - return newValidationError(ErrorCodeInvalidFormat, "values must not match") - } - return nil - } -} - -// areEqual compares two values for equality with special handling for time.Time. -func areEqual(a, b any) bool { - // Dereference both values - aVal, aIsNil := dereferenceValue(a) - bVal, bIsNil := dereferenceValue(b) - - // If both are nil, they're equal - if aIsNil && bIsNil { - return true - } - - // If only one is nil, they're not equal - if aIsNil || bIsNil { - return false - } - - // Special handling for time.Time - aTime, aIsTime := aVal.(time.Time) - bTime, bIsTime := bVal.(time.Time) - - if aIsTime && bIsTime { - // Use time.Time.Equal() which compares the instant, ignoring location and monotonic clock - return aTime.Equal(bTime) - } - - // Fall back to reflect.DeepEqual for all other types - return reflect.DeepEqual(aVal, bVal) -} diff --git a/pkg/validator/validator_conditional_test.go b/pkg/validator/validator_conditional_test.go deleted file mode 100644 index fe4f274b7..000000000 --- a/pkg/validator/validator_conditional_test.go +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package validator - -import ( - "testing" - "time" -) - -func TestEqualTo(t *testing.T) { - t.Run("equal strings", func(t *testing.T) { - str1 := "password" - str2 := "password" - err := EqualTo(&str2)(&str1) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("different strings", func(t *testing.T) { - str1 := "password" - str2 := "different" - err := EqualTo(&str2)(&str1) - if err == nil { - t.Error("expected validation error") - } - }) - - t.Run("equal integers", func(t *testing.T) { - num1 := 42 - num2 := 42 - err := EqualTo(&num2)(&num1) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("different integers", func(t *testing.T) { - num1 := 42 - num2 := 43 - err := EqualTo(&num2)(&num1) - if err == nil { - t.Error("expected validation error") - } - }) -} - -func TestNotEqualTo(t *testing.T) { - t.Run("different strings", func(t *testing.T) { - str1 := "password" - str2 := "different" - err := NotEqualTo(&str2)(&str1) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("equal strings", func(t *testing.T) { - str1 := "password" - str2 := "password" - err := NotEqualTo(&str2)(&str1) - if err == nil { - t.Error("expected validation error") - } - }) -} - -func TestEqualTo_TimeComparison(t *testing.T) { - t.Run("same instant same location", func(t *testing.T) { - time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - err := EqualTo(time2)(time1) - if err != nil { - t.Errorf("expected no error for same instant, got: %v", err) - } - }) - - t.Run("same instant different location", func(t *testing.T) { - // Create the same instant in different time zones - utcTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - est, _ := time.LoadLocation("America/New_York") - estTime := time.Date(2025, 11, 5, 7, 0, 0, 0, est) // 7am EST = 12pm UTC - - err := EqualTo(utcTime)(estTime) - if err != nil { - t.Errorf("expected no error for same instant in different locations, got: %v", err) - } - }) - - t.Run("different instants same location", func(t *testing.T) { - time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - time2 := time.Date(2025, 11, 5, 13, 0, 0, 0, time.UTC) - err := EqualTo(time2)(time1) - if err == nil { - t.Error("expected validation error for different instants") - } - }) - - t.Run("pointer to time same instant", func(t *testing.T) { - time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - err := EqualTo(&time2)(&time1) - if err != nil { - t.Errorf("expected no error for pointer to same instant, got: %v", err) - } - }) - - t.Run("nil time pointers", func(t *testing.T) { - var time1 *time.Time - var time2 *time.Time - err := EqualTo(time2)(time1) - if err != nil { - t.Errorf("expected no error for nil time pointers, got: %v", err) - } - }) - - t.Run("one nil one non-nil time pointer", func(t *testing.T) { - var time1 *time.Time - time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - err := EqualTo(&time2)(time1) - if err == nil { - t.Error("expected validation error for nil vs non-nil time") - } - }) - - t.Run("same instant with monotonic clock difference", func(t *testing.T) { - // Simulate times with different monotonic clock data - baseTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - time1 := baseTime - time.Sleep(1 * time.Millisecond) // Advances monotonic clock - time2 := baseTime - - // Even though monotonic clocks differ, the instants are the same - err := EqualTo(time2)(time1) - if err != nil { - t.Errorf("expected no error despite monotonic clock difference, got: %v", err) - } - }) -} - -func TestNotEqualTo_TimeComparison(t *testing.T) { - t.Run("different instants", func(t *testing.T) { - time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - time2 := time.Date(2025, 11, 5, 13, 0, 0, 0, time.UTC) - err := NotEqualTo(time2)(time1) - if err != nil { - t.Errorf("expected no error for different instants, got: %v", err) - } - }) - - t.Run("same instant same location", func(t *testing.T) { - time1 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - time2 := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - err := NotEqualTo(time2)(time1) - if err == nil { - t.Error("expected validation error for same instant") - } - }) - - t.Run("same instant different location", func(t *testing.T) { - utcTime := time.Date(2025, 11, 5, 12, 0, 0, 0, time.UTC) - est, _ := time.LoadLocation("America/New_York") - estTime := time.Date(2025, 11, 5, 7, 0, 0, 0, est) - - err := NotEqualTo(utcTime)(estTime) - if err == nil { - t.Error("expected validation error for same instant in different locations") - } - }) -} diff --git a/pkg/validator/validator_custom.go b/pkg/validator/validator_custom.go deleted file mode 100644 index fe8451a0f..000000000 --- a/pkg/validator/validator_custom.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package validator - -// Custom creates a custom validator with a specified error code, message, and validation function. -// The validation function should return true if the value is valid, false otherwise. -func Custom(code ErrorCode, message string, fn func(value any) bool) ValidatorFunc { - return func(value any) *ValidationError { - if !fn(value) { - return newValidationError(code, message) - } - return nil - } -} diff --git a/pkg/validator/validator_custom_test.go b/pkg/validator/validator_custom_test.go deleted file mode 100644 index 5cca4e9db..000000000 --- a/pkg/validator/validator_custom_test.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2025 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -package validator - -import ( - "testing" -) - -func TestCustom(t *testing.T) { - validator := Custom(ErrorCodeCustom, "value must be positive", func(value any) bool { - if num, ok := value.(int); ok { - return num > 0 - } - return false - }) - - tests := []struct { - name string - value any - wantError bool - }{ - {"valid positive", 5, false}, - {"invalid zero", 0, true}, - {"invalid negative", -5, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := validator(tt.value) - if (err != nil) != tt.wantError { - t.Errorf("Custom() error = %v, wantError %v", err, tt.wantError) - } - }) - } -} diff --git a/pkg/validator/validator_email.go b/pkg/validator/validator_email.go deleted file mode 100644 index a33074a43..000000000 --- a/pkg/validator/validator_email.go +++ /dev/null @@ -1,46 +0,0 @@ -package validator - -import ( - _ "embed" - "strings" -) - -var ( - //go:embed data/disposable-email-domains/disposable_email_blocklist.conf - disposableEmailsRaw []byte - testEmails = []string{ - "acme.com", - "acme.net", - "acme.org", - "ethereal.email", - "example.com", - "example.net", - "example.org", - "mailhog.local", - "mailslurp.com", - "test.com", - "test.net", - "test.org", - "localhost.localdomain", - } - blacklistedEmails = append( - strings.Split(strings.TrimSpace(string(disposableEmailsRaw)), "\n"), - testEmails..., - ) - notOneOfBlacklisted = NotOneOfSlice(blacklistedEmails) -) - -func NotBlacklisted() ValidatorFunc { - return func(value any) *ValidationError { - err := notOneOfBlacklisted(value) - - if err != nil { - return newValidationError( - ErrorCodeInvalidEmail, - "must not be blacklisted", - ) - } - - return nil - } -} diff --git a/pkg/validator/validator_format.go b/pkg/validator/validator_format.go index c5eae4aa1..46f04990c 100644 --- a/pkg/validator/validator_format.go +++ b/pkg/validator/validator_format.go @@ -23,7 +23,6 @@ import ( ) var ( - uuidRegex = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) domainRegex = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`) ) @@ -61,40 +60,6 @@ func URL() ValidatorFunc { } } -// HTTPUrl validates that a string is a valid HTTP URL (not HTTPS). -func HTTPUrl() ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - str, ok := actualValue.(string) - if !ok { - return newValidationError(ErrorCodeInvalidURL, "value must be a string") - } - - if str == "" { - return nil - } - - parsedURL, err := url.Parse(str) - if err != nil { - return newValidationError(ErrorCodeInvalidURL, "invalid URL format") - } - - if parsedURL.Scheme != "http" { - return newValidationError(ErrorCodeInvalidURL, "URL must use http scheme") - } - - if parsedURL.Host == "" { - return newValidationError(ErrorCodeInvalidURL, "URL must have a host") - } - - return nil - } -} - // HTTPSUrl validates that a string is a valid HTTPS URL (not HTTP). func HTTPSUrl() ValidatorFunc { return func(value any) *ValidationError { @@ -129,31 +94,6 @@ func HTTPSUrl() ValidatorFunc { } } -// UUID validates that a string is a valid UUID. -func UUID() ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - str, ok := actualValue.(string) - if !ok { - return newValidationError(ErrorCodeInvalidFormat, "value must be a string") - } - - if str == "" { - return nil - } - - if !uuidRegex.MatchString(str) { - return newValidationError(ErrorCodeInvalidFormat, "invalid UUID format") - } - - return nil - } -} - // GID validates that a string is a valid GID using gid.ParseGID. // Optionally validates the entity type if provided. // diff --git a/pkg/validator/validator_format_test.go b/pkg/validator/validator_format_test.go index 5390f5a57..3f8da4439 100644 --- a/pkg/validator/validator_format_test.go +++ b/pkg/validator/validator_format_test.go @@ -51,75 +51,6 @@ func TestURL(t *testing.T) { } } -func TestHTTPUrl(t *testing.T) { - t.Run("valid http URL", func(t *testing.T) { - str := "http://example.com" - err := HTTPUrl()(&str) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("valid http URL with path", func(t *testing.T) { - str := "http://example.com/path/to/resource" - err := HTTPUrl()(&str) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("valid http URL with query", func(t *testing.T) { - str := "http://example.com?foo=bar" - err := HTTPUrl()(&str) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("invalid - https scheme", func(t *testing.T) { - str := "https://example.com" - err := HTTPUrl()(&str) - if err == nil { - t.Fatal("expected validation error for https") - } - if err.Message != "URL must use http scheme" { - t.Errorf("unexpected error message: %s", err.Message) - } - }) - - t.Run("invalid - no scheme", func(t *testing.T) { - str := "example.com" - err := HTTPUrl()(&str) - if err == nil { - t.Error("expected validation error for missing scheme") - } - }) - - t.Run("invalid - no host", func(t *testing.T) { - str := "http://" - err := HTTPUrl()(&str) - if err == nil { - t.Error("expected validation error for missing host") - } - }) - - t.Run("empty string", func(t *testing.T) { - str := "" - err := HTTPUrl()(&str) - if err != nil { - t.Errorf("expected no error for empty string, got: %v", err) - } - }) - - t.Run("nil pointer", func(t *testing.T) { - var str *string - err := HTTPUrl()(str) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) -} - func TestHTTPSUrl(t *testing.T) { t.Run("valid https URL", func(t *testing.T) { str := "https://example.com" diff --git a/pkg/validator/validator_numeric.go b/pkg/validator/validator_numeric.go index ed90dbd8e..83d4bcab1 100644 --- a/pkg/validator/validator_numeric.go +++ b/pkg/validator/validator_numeric.go @@ -78,126 +78,3 @@ func Max(max int) ValidatorFunc { } } -// Range validates that a number is within the specified range (inclusive). -func Range(min, max int) ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - var num int - switch v := actualValue.(type) { - case int: - num = v - case int32: - num = int(v) - case int64: - num = int(v) - default: - return newValidationError(ErrorCodeInvalidFormat, "value must be a number") - } - - if num < min || num > max { - return newValidationError( - ErrorCodeOutOfRange, - fmt.Sprintf("must be between %d and %d", min, max), - ) - } - - return nil - } -} - -// MinFloat validates that a floating-point number is at least the specified minimum value. -func MinFloat(min float64) ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - var num float64 - switch v := actualValue.(type) { - case float32: - num = float64(v) - case float64: - num = v - case int: - num = float64(v) - default: - return newValidationError(ErrorCodeInvalidFormat, "value must be a number") - } - - if num < min { - return newValidationError( - ErrorCodeOutOfRange, - fmt.Sprintf("must be at least %g", min), - ) - } - - return nil - } -} - -// MaxFloat validates that a floating-point number does not exceed the specified maximum value. -func MaxFloat(max float64) ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - var num float64 - switch v := actualValue.(type) { - case float32: - num = float64(v) - case float64: - num = v - case int: - num = float64(v) - default: - return newValidationError(ErrorCodeInvalidFormat, "value must be a number") - } - - if num > max { - return newValidationError( - ErrorCodeOutOfRange, - fmt.Sprintf("must be at most %g", max), - ) - } - - return nil - } -} - -// RangeFloat validates that a floating-point number is within the specified range (inclusive). -func RangeFloat(min, max float64) ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - var num float64 - switch v := actualValue.(type) { - case float32: - num = float64(v) - case float64: - num = v - case int: - num = float64(v) - default: - return newValidationError(ErrorCodeInvalidFormat, "value must be a number") - } - - if num < min || num > max { - return newValidationError( - ErrorCodeOutOfRange, - fmt.Sprintf("must be between %g and %g", min, max), - ) - } - - return nil - } -} diff --git a/pkg/validator/validator_numeric_test.go b/pkg/validator/validator_numeric_test.go index 438a842f1..8fe120939 100644 --- a/pkg/validator/validator_numeric_test.go +++ b/pkg/validator/validator_numeric_test.go @@ -66,28 +66,3 @@ func TestMax(t *testing.T) { }) } } - -func TestRange(t *testing.T) { - tests := []struct { - name string - value any - min int - max int - wantError bool - }{ - {"in range", 5, 1, 10, false}, - {"at min", 1, 1, 10, false}, - {"at max", 10, 1, 10, false}, - {"below range", 0, 1, 10, true}, - {"above range", 11, 1, 10, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := Range(tt.min, tt.max)(tt.value) - if (err != nil) != tt.wantError { - t.Errorf("Range() error = %v, wantError %v", err, tt.wantError) - } - }) - } -} diff --git a/pkg/validator/validator_security_test.go b/pkg/validator/validator_security_test.go index 471437027..92e8cc21b 100644 --- a/pkg/validator/validator_security_test.go +++ b/pkg/validator/validator_security_test.go @@ -298,8 +298,8 @@ func TestNoHTML(t *testing.T) { title := "Product Title 2024" v.Check(&title, "title", Required(), NoHTML(), MinLen(3), MaxLen(100)) - if v.HasErrors() { - t.Errorf("expected no errors, got: %v", v.Errors()) + if v.Error() != nil { + t.Errorf("expected no errors, got: %v", v.Error()) } }) @@ -308,8 +308,8 @@ func TestNoHTML(t *testing.T) { title := "José García-O'Brien" v.Check(&title, "title", Required(), NoHTML(), PrintableText(), MinLen(3), MaxLen(100)) - if v.HasErrors() { - t.Errorf("expected no errors, got: %v", v.Errors()) + if v.Error() != nil { + t.Errorf("expected no errors, got: %v", v.Error()) } }) @@ -318,12 +318,12 @@ func TestNoHTML(t *testing.T) { malicious := "" v.Check(&malicious, "content", Required(), NoHTML(), PrintableText()) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected validation errors") } // Should have error from NoHTML - errors := v.Errors() + errors := v.Error().(ValidationErrors) found := false for _, err := range errors { if strings.Contains(err.Message, "HTML tags") { @@ -341,12 +341,12 @@ func TestNoHTML(t *testing.T) { malicious := "test\x00text" v.Check(&malicious, "content", NoHTML(), PrintableText()) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected validation errors") } // Should have at least one error (NoHTML will catch it first) - if len(v.Errors()) < 1 { + if ve := v.Error(); ve == nil || len(ve.(ValidationErrors)) < 1 { t.Error("expected at least one validation error") } }) @@ -635,8 +635,8 @@ func TestPrintableText(t *testing.T) { title := "Product Title 2024" v.Check(&title, "title", Required(), PrintableText(), MinLen(3), MaxLen(100)) - if v.HasErrors() { - t.Errorf("expected no errors, got: %v", v.Errors()) + if v.Error() != nil { + t.Errorf("expected no errors, got: %v", v.Error()) } }) @@ -840,8 +840,8 @@ func TestSafeText(t *testing.T) { title := "Product Title 2024" v.Check(&title, "title", SafeText(100)) - if v.HasErrors() { - t.Errorf("expected no errors, got: %v", v.Errors()) + if v.Error() != nil { + t.Errorf("expected no errors, got: %v", v.Error()) } }) @@ -850,11 +850,11 @@ func TestSafeText(t *testing.T) { malicious := "" v.Check(&malicious, "content", SafeText(100)) - if !v.HasErrors() { + if v.Error() == nil { t.Error("expected validation errors") } - errors := v.Errors() + errors := v.Error().(ValidationErrors) found := false for _, err := range errors { if strings.Contains(err.Message, "HTML tags") { diff --git a/pkg/validator/validator_string.go b/pkg/validator/validator_string.go index 29b17820d..103491125 100644 --- a/pkg/validator/validator_string.go +++ b/pkg/validator/validator_string.go @@ -17,15 +17,9 @@ package validator import ( "fmt" "reflect" - "regexp" "strings" ) -var ( - alphaNumericRegex = regexp.MustCompile(`^[a-zA-Z0-9]+$`) - slugRegex = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`) -) - // MinLen validates that a string has at least the specified minimum length. func MinLen(minLength int) ValidatorFunc { return func(value any) *ValidationError { @@ -74,107 +68,6 @@ func MaxLen(maxLength int) ValidatorFunc { } } -// Pattern validates that a string matches the specified regular expression pattern. -func Pattern(pattern string, message string) ValidatorFunc { - regex := regexp.MustCompile(pattern) - - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - str, ok := actualValue.(string) - if !ok { - return newValidationError(ErrorCodeInvalidFormat, "value must be a string") - } - - if !regex.MatchString(str) { - if message == "" { - message = fmt.Sprintf("must match pattern: %s", pattern) - } - return newValidationError(ErrorCodeInvalidFormat, message) - } - - return nil - } -} - -// AlphaNumeric validates that a string contains only letters and numbers. -func AlphaNumeric() ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - str, ok := actualValue.(string) - if !ok { - return newValidationError(ErrorCodeInvalidFormat, "value must be a string") - } - - if str == "" { - return nil - } - - if !alphaNumericRegex.MatchString(str) { - return newValidationError(ErrorCodeInvalidFormat, "must contain only letters and numbers") - } - - return nil - } -} - -// NoSpaces validates that a string does not contain any spaces. -func NoSpaces() ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - str, ok := actualValue.(string) - if !ok { - return newValidationError(ErrorCodeInvalidFormat, "value must be a string") - } - - if str == "" { - return nil - } - - if strings.Contains(str, " ") { - return newValidationError(ErrorCodeInvalidFormat, "must not contain spaces") - } - - return nil - } -} - -// Slug validates that a string is a valid URL slug (lowercase letters, numbers, and hyphens). -func Slug() ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - str, ok := actualValue.(string) - if !ok { - return newValidationError(ErrorCodeInvalidFormat, "value must be a string") - } - - if str == "" { - return nil - } - - if !slugRegex.MatchString(str) { - return newValidationError(ErrorCodeInvalidFormat, "must be a valid slug (lowercase letters, numbers, and hyphens)") - } - - return nil - } -} - // OneOfSlice validates that a value is one of the allowed values in the slice. // Accepts a slice of any type. Compares by value first, then by string representation. func OneOfSlice[T any](allowed []T) ValidatorFunc { @@ -224,105 +117,3 @@ func OneOfSlice[T any](allowed []T) ValidatorFunc { ) } } - -// NotOneOfSlice validates that a value is not one of the values in the slice. -// Accepts a slice of any type. Compares by value first, then by string representation. -func NotOneOfSlice[T any](disallowed []T) ValidatorFunc { - // Build disallowed map with string keys for flexible comparison - disallowedMap := make(map[string]bool) - disallowedStrings := make([]string, 0, len(disallowed)) - - for _, v := range disallowed { - str := fmt.Sprint(v) - disallowedMap[str] = true - disallowedStrings = append(disallowedStrings, str) - } - - return func(value any) *ValidationError { - // Handle nil values first - if value == nil { - return nil - } - - // Dereference all pointer levels - actualValue := value - val := reflect.ValueOf(value) - for val.Kind() == reflect.Pointer { - if val.IsNil() { - return nil - } - val = val.Elem() - actualValue = val.Interface() - } - - // First try exact match with DeepEqual - for _, disallowedVal := range disallowed { - if reflect.DeepEqual(actualValue, disallowedVal) { - return newValidationError( - ErrorCodeInvalidEnum, - fmt.Sprintf("must not be one of: %s", strings.Join(disallowedStrings, ", ")), - ) - } - } - - // Then try string comparison (for custom string types) - valueStr := fmt.Sprint(actualValue) - if disallowedMap[valueStr] { - return newValidationError( - ErrorCodeInvalidEnum, - fmt.Sprintf("must not be one of: %s", strings.Join(disallowedStrings, ", ")), - ) - } - - return nil - } -} - -// OneOf validates that a value is one of the allowed values. -// Accepts strings or types that implement fmt.Stringer as variadic arguments. -func OneOf(allowed ...any) ValidatorFunc { - allowedMap := make(map[string]bool) - allowedStrings := make([]string, 0, len(allowed)) - - for _, v := range allowed { - var str string - switch val := v.(type) { - case string: - str = val - case fmt.Stringer: - str = val.String() - default: - str = fmt.Sprint(val) - } - allowedMap[str] = true - allowedStrings = append(allowedStrings, str) - } - - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - var str string - switch v := actualValue.(type) { - case string: - str = v - default: - if stringer, ok := actualValue.(fmt.Stringer); ok { - str = stringer.String() - } else { - return newValidationError(ErrorCodeInvalidEnum, "value must be a string or implement fmt.Stringer") - } - } - - if !allowedMap[str] { - return newValidationError( - ErrorCodeInvalidEnum, - fmt.Sprintf("must be one of: %s", strings.Join(allowedStrings, ", ")), - ) - } - - return nil - } -} diff --git a/pkg/validator/validator_string_test.go b/pkg/validator/validator_string_test.go index a7170a193..39ec43948 100644 --- a/pkg/validator/validator_string_test.go +++ b/pkg/validator/validator_string_test.go @@ -67,215 +67,6 @@ func TestMaxLen(t *testing.T) { } } -func TestPattern(t *testing.T) { - tests := []struct { - name string - value any - pattern string - message string - wantError bool - }{ - {"valid pattern", "abc123", `^[a-z0-9]+$`, "", false}, - {"invalid pattern", "ABC123", `^[a-z0-9]+$`, "", true}, - {"custom message", "invalid", `^valid$`, "must be 'valid'", true}, - {"nil pointer", (*string)(nil), `^test$`, "", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := Pattern(tt.pattern, tt.message)(tt.value) - if (err != nil) != tt.wantError { - t.Errorf("Pattern() error = %v, wantError %v", err, tt.wantError) - } - if err != nil && tt.message != "" && err.Message != tt.message { - t.Errorf("Expected message '%s', got '%s'", tt.message, err.Message) - } - }) - } -} - -func TestAlphaNumeric(t *testing.T) { - t.Run("valid alphanumeric", func(t *testing.T) { - str := "abc123" - err := AlphaNumeric()(&str) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("only letters", func(t *testing.T) { - str := "abcDEF" - err := AlphaNumeric()(&str) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("only numbers", func(t *testing.T) { - str := "123456" - err := AlphaNumeric()(&str) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("contains spaces", func(t *testing.T) { - str := "abc 123" - err := AlphaNumeric()(&str) - if err == nil { - t.Error("expected validation error") - } - }) - - t.Run("contains special characters", func(t *testing.T) { - str := "abc-123" - err := AlphaNumeric()(&str) - if err == nil { - t.Error("expected validation error") - } - }) - - t.Run("empty string", func(t *testing.T) { - str := "" - err := AlphaNumeric()(&str) - if err != nil { - t.Errorf("expected no error for empty string, got: %v", err) - } - }) - - t.Run("nil pointer", func(t *testing.T) { - var str *string - err := AlphaNumeric()(str) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) -} - -func TestNoSpaces(t *testing.T) { - t.Run("no spaces", func(t *testing.T) { - str := "hello-world" - err := NoSpaces()(&str) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("contains spaces", func(t *testing.T) { - str := "hello world" - err := NoSpaces()(&str) - if err == nil { - t.Error("expected validation error") - } - }) - - t.Run("multiple spaces", func(t *testing.T) { - str := "hello world test" - err := NoSpaces()(&str) - if err == nil { - t.Error("expected validation error") - } - }) - - t.Run("empty string", func(t *testing.T) { - str := "" - err := NoSpaces()(&str) - if err != nil { - t.Errorf("expected no error for empty string, got: %v", err) - } - }) - - t.Run("nil pointer", func(t *testing.T) { - var str *string - err := NoSpaces()(str) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) -} - -func TestSlug(t *testing.T) { - t.Run("valid slug", func(t *testing.T) { - str := "hello-world" - err := Slug()(&str) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("valid slug with numbers", func(t *testing.T) { - str := "hello-world-123" - err := Slug()(&str) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("single word", func(t *testing.T) { - str := "hello" - err := Slug()(&str) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("contains uppercase", func(t *testing.T) { - str := "Hello-World" - err := Slug()(&str) - if err == nil { - t.Error("expected validation error for uppercase") - } - }) - - t.Run("contains spaces", func(t *testing.T) { - str := "hello world" - err := Slug()(&str) - if err == nil { - t.Error("expected validation error for spaces") - } - }) - - t.Run("contains underscores", func(t *testing.T) { - str := "hello_world" - err := Slug()(&str) - if err == nil { - t.Error("expected validation error for underscores") - } - }) - - t.Run("starts with hyphen", func(t *testing.T) { - str := "-hello" - err := Slug()(&str) - if err == nil { - t.Error("expected validation error for leading hyphen") - } - }) - - t.Run("ends with hyphen", func(t *testing.T) { - str := "hello-" - err := Slug()(&str) - if err == nil { - t.Error("expected validation error for trailing hyphen") - } - }) - - t.Run("empty string", func(t *testing.T) { - str := "" - err := Slug()(&str) - if err != nil { - t.Errorf("expected no error for empty string, got: %v", err) - } - }) - - t.Run("nil pointer", func(t *testing.T) { - var str *string - err := Slug()(str) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) -} - func TestOneOf(t *testing.T) { tests := []struct { name string diff --git a/pkg/validator/validator_time.go b/pkg/validator/validator_time.go index 55f45d461..250f7c917 100644 --- a/pkg/validator/validator_time.go +++ b/pkg/validator/validator_time.go @@ -93,96 +93,6 @@ func Before(t any) ValidatorFunc { } } -// FutureDate validates that a time is in the future. -func FutureDate() ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - timeVal, ok := actualValue.(time.Time) - if !ok { - return newValidationError(ErrorCodeInvalidFormat, "value must be a time.Time") - } - - if !timeVal.After(time.Now()) { - return newValidationError(ErrorCodeOutOfRange, "must be a future date") - } - - return nil - } -} - -// PastDate validates that a time is in the past. -func PastDate() ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - timeVal, ok := actualValue.(time.Time) - if !ok { - return newValidationError(ErrorCodeInvalidFormat, "value must be a time.Time") - } - - if !timeVal.Before(time.Now()) { - return newValidationError(ErrorCodeOutOfRange, "must be a past date") - } - - return nil - } -} - -// MinDuration validates that a duration is at least the specified minimum value. -func MinDuration(min time.Duration) ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - duration, ok := actualValue.(time.Duration) - if !ok { - return newValidationError(ErrorCodeInvalidFormat, "value must be a time.Duration") - } - - if duration < min { - return newValidationError( - ErrorCodeOutOfRange, - fmt.Sprintf("must be at least %s", min), - ) - } - - return nil - } -} - -// MaxDuration validates that a duration does not exceed the specified maximum value. -func MaxDuration(max time.Duration) ValidatorFunc { - return func(value any) *ValidationError { - actualValue, isNil := dereferenceValue(value) - if isNil { - return nil - } - - duration, ok := actualValue.(time.Duration) - if !ok { - return newValidationError(ErrorCodeInvalidFormat, "value must be a time.Duration") - } - - if duration > max { - return newValidationError( - ErrorCodeOutOfRange, - fmt.Sprintf("must be at most %s", max), - ) - } - - return nil - } -} - // RangeDuration validates that a duration is within the specified range (inclusive). func RangeDuration(min, max time.Duration) ValidatorFunc { return func(value any) *ValidationError { diff --git a/pkg/validator/validator_time_test.go b/pkg/validator/validator_time_test.go index fc27f3a02..980d51721 100644 --- a/pkg/validator/validator_time_test.go +++ b/pkg/validator/validator_time_test.go @@ -95,144 +95,6 @@ func TestBefore(t *testing.T) { }) } -func TestFutureDate(t *testing.T) { - future := time.Now().Add(24 * time.Hour) - past := time.Now().Add(-24 * time.Hour) - - t.Run("future date", func(t *testing.T) { - err := FutureDate()(&future) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("past date", func(t *testing.T) { - err := FutureDate()(&past) - if err == nil { - t.Fatal("expected validation error") - } - if err.Code != ErrorCodeOutOfRange { - t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code) - } - }) - - t.Run("nil pointer", func(t *testing.T) { - var timeVal *time.Time - err := FutureDate()(timeVal) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) -} - -func TestPastDate(t *testing.T) { - future := time.Now().Add(24 * time.Hour) - past := time.Now().Add(-24 * time.Hour) - - t.Run("past date", func(t *testing.T) { - err := PastDate()(&past) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("future date", func(t *testing.T) { - err := PastDate()(&future) - if err == nil { - t.Fatal("expected validation error") - } - if err.Code != ErrorCodeOutOfRange { - t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code) - } - }) - - t.Run("nil pointer", func(t *testing.T) { - var timeVal *time.Time - err := PastDate()(timeVal) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) -} - -func TestMinDuration(t *testing.T) { - minDuration := 10 * time.Minute - - t.Run("duration above minimum", func(t *testing.T) { - duration := 20 * time.Minute - err := MinDuration(minDuration)(&duration) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("duration equal to minimum", func(t *testing.T) { - duration := 10 * time.Minute - err := MinDuration(minDuration)(&duration) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("duration below minimum", func(t *testing.T) { - duration := 5 * time.Minute - err := MinDuration(minDuration)(&duration) - if err == nil { - t.Fatal("expected validation error") - } - if err.Code != ErrorCodeOutOfRange { - t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code) - } - }) - - t.Run("nil pointer", func(t *testing.T) { - var duration *time.Duration - err := MinDuration(minDuration)(duration) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) -} - -func TestMaxDuration(t *testing.T) { - maxDuration := 1 * time.Hour - - t.Run("duration below maximum", func(t *testing.T) { - duration := 30 * time.Minute - err := MaxDuration(maxDuration)(&duration) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("duration equal to maximum", func(t *testing.T) { - duration := 1 * time.Hour - err := MaxDuration(maxDuration)(&duration) - if err != nil { - t.Errorf("expected no error, got: %v", err) - } - }) - - t.Run("duration above maximum", func(t *testing.T) { - duration := 2 * time.Hour - err := MaxDuration(maxDuration)(&duration) - if err == nil { - t.Fatal("expected validation error") - } - if err.Code != ErrorCodeOutOfRange { - t.Errorf("expected error code %s, got %s", ErrorCodeOutOfRange, err.Code) - } - }) - - t.Run("nil pointer", func(t *testing.T) { - var duration *time.Duration - err := MaxDuration(maxDuration)(duration) - if err != nil { - t.Errorf("expected no error for nil, got: %v", err) - } - }) -} - func TestRangeDuration(t *testing.T) { minDuration := 10 * time.Minute maxDuration := 1 * time.Hour diff --git a/pkg/version/version.go b/pkg/version/version.go index 2a0671aec..067410bb7 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -100,29 +100,3 @@ func UserAgent(component string) string { return strings.Join(parts, " ") } -// Version returns the version string -func Version() string { - info := GetBuildInfo() - return info.Version -} - -// FullVersion returns a detailed version string with all build information -func FullVersion() string { - info := GetBuildInfo() - - parts := []string{ - fmt.Sprintf("Version: %s", info.Version), - } - - if info.Commit != "unknown" { - parts = append(parts, fmt.Sprintf("Commit: %s", info.Commit)) - } - - if info.BuildDate != "unknown" { - parts = append(parts, fmt.Sprintf("Built: %s", info.BuildDate)) - } - - parts = append(parts, fmt.Sprintf("Go: %s", info.GoVersion)) - - return strings.Join(parts, "\n") -} diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go index 64a67baff..c3cf6d388 100644 --- a/pkg/version/version_test.go +++ b/pkg/version/version_test.go @@ -91,24 +91,6 @@ func TestUserAgent(t *testing.T) { } } -func TestVersion(t *testing.T) { - v := version.Version() - assert.NotEmpty(t, v, "Version should not be empty") -} - -func TestFullVersion(t *testing.T) { - fv := version.FullVersion() - require.NotEmpty(t, fv, "Full version should not be empty") - - // Check that it contains expected fields - assert.Contains(t, fv, "Version:", "Full version should contain Version field") - assert.Contains(t, fv, "Go:", "Full version should contain Go field") - - // Check it's multiline - lines := strings.Split(fv, "\n") - assert.GreaterOrEqual(t, len(lines), 2, "Full version should have at least 2 lines") -} - func TestUserAgentConsistency(t *testing.T) { // Test that multiple calls return consistent results ua1 := version.UserAgent("test")