diff --git a/options.go b/options.go index 9a6423b..edcc4bb 100644 --- a/options.go +++ b/options.go @@ -4,6 +4,7 @@ package webhook import ( + "fmt" "time" "github.com/xmidt-org/urlegit" @@ -18,7 +19,7 @@ type errorOption struct { err error } -func (e errorOption) Validate(Validator) error { +func (e errorOption) Validate(any) error { return error(e.err) } @@ -35,7 +36,7 @@ func AlwaysValid() Option { type AlwaysValidOption struct{} -func (a AlwaysValidOption) Validate(val Validator) error { +func (a AlwaysValidOption) Validate(any) error { return nil } @@ -51,12 +52,15 @@ func AtLeastOneEvent() Option { type atLeastOneEventOption struct{} -func (atLeastOneEventOption) Validate(val Validator) error { - if err := val.ValidateOneEvent(); err != nil { - return err +func (atLeastOneEventOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + return r.ValidateOneEvent() + case *RegistrationV2: + return fmt.Errorf("%w: RegistrationV2 does not have an events field to validate", ErrInvalidType) + default: + return ErrUknownType } - - return nil } func (atLeastOneEventOption) String() string { @@ -70,11 +74,15 @@ func EventRegexMustCompile() Option { type eventRegexMustCompileOption struct{} -func (eventRegexMustCompileOption) Validate(val Validator) error { - if err := val.ValidateEventRegex(); err != nil { - return err +func (eventRegexMustCompileOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + return r.ValidateEventRegex() + case *RegistrationV2: + return r.ValidateEventRegex() + default: + return ErrUknownType } - return nil } func (eventRegexMustCompileOption) String() string { @@ -89,11 +97,15 @@ func DeviceIDRegexMustCompile() Option { type deviceIDRegexMustCompileOption struct{} -func (deviceIDRegexMustCompileOption) Validate(val Validator) error { - if err := val.ValidateDeviceId(); err != nil { - return err +func (deviceIDRegexMustCompileOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + return r.ValidateDeviceId() + case *RegistrationV2: + return fmt.Errorf("%w: RegistrationV2 does not use DeviceID directly, use `FieldRegex` instead", ErrInvalidType) + default: + return ErrUknownType } - return nil } func (deviceIDRegexMustCompileOption) String() string { @@ -113,11 +125,15 @@ type validateRegistrationDurationOption struct { ttl time.Duration } -func (v validateRegistrationDurationOption) Validate(val Validator) error { - if err := val.ValidateDuration(v.ttl); err != nil { - return err +func (v validateRegistrationDurationOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + return r.ValidateDuration(v.ttl) + case *RegistrationV2: + return r.ValidateDuration() + default: + return ErrUknownType } - return nil } func (v validateRegistrationDurationOption) String() string { @@ -134,8 +150,12 @@ type provideTimeNowFuncOption struct { nowFunc func() time.Time } -func (p provideTimeNowFuncOption) Validate(val Validator) error { - val.SetNowFunc(p.nowFunc) +func (p provideTimeNowFuncOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + r.SetNowFunc(p.nowFunc) + } + return nil } @@ -156,13 +176,26 @@ type provideFailureURLValidatorOption struct { checker *urlegit.Checker } -func (p provideFailureURLValidatorOption) Validate(v Validator) error { +func (p provideFailureURLValidatorOption) Validate(i any) error { + var failureURL string + //TODO: do we want to move this check to be inside each case statement? if p.checker == nil { return nil } - if err := v.ValidateFailureURL(p.checker); err != nil { - return err + switch r := i.(type) { + case *RegistrationV1: + failureURL = r.FailureURL + case *RegistrationV2: + failureURL = r.FailureURL + default: + return ErrUknownType + } + + if failureURL != "" { + if err := p.checker.Text(failureURL); err != nil { + return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) + } } return nil } @@ -184,15 +217,19 @@ type provideReceiverURLValidatorOption struct { checker *urlegit.Checker } -func (p provideReceiverURLValidatorOption) Validate(val Validator) error { +func (p provideReceiverURLValidatorOption) Validate(i any) error { if p.checker == nil { return nil } - if err := val.ValidateReceiverURL(p.checker); err != nil { - return err - } - return nil + switch r := i.(type) { + case *RegistrationV1: + return r.ValidateReceiverURL(p.checker) + case *RegistrationV2: + return r.ValidateReceiverURL(p.checker) + default: + return ErrUknownType + } } func (p provideReceiverURLValidatorOption) String() string { @@ -212,15 +249,19 @@ type provideAlternativeURLValidatorOption struct { checker *urlegit.Checker } -func (p provideAlternativeURLValidatorOption) Validate(val Validator) error { +func (p provideAlternativeURLValidatorOption) Validate(i any) error { if p.checker == nil { return nil } - if err := val.ValidateAltURL(p.checker); err != nil { - return err + switch r := i.(type) { + case *RegistrationV1: + return r.ValidateAltURL(p.checker) + case *RegistrationV2: + return fmt.Errorf("%w: RegistrationV2 does not have an alternative urls field. Use ProvideReceiverURLValidator() to validate all non-failure urls", ErrInvalidType) + default: + return ErrUknownType } - return nil } func (p provideAlternativeURLValidatorOption) String() string { @@ -237,37 +278,17 @@ func NoUntil() Option { type noUntilOption struct{} -func (noUntilOption) Validate(val Validator) error { - if err := val.ValidateNoUntil(); err != nil { - return err +func (noUntilOption) Validate(i any) error { + switch r := i.(type) { + case *RegistrationV1: + return r.ValidateNoUntil() + case *RegistrationV2: + return fmt.Errorf("%w: RegistrationV2 does not use an Until field", ErrInvalidType) + default: + return ErrUknownType } - return nil } func (noUntilOption) String() string { return "NoUntil()" } - -func Until(j time.Duration, m time.Duration, now func() time.Time) Option { - return untilOption{ - jitter: j, - max: m, - now: now, - } -} - -type untilOption struct { - jitter time.Duration - max time.Duration - now func() time.Time -} - -func (u untilOption) Validate(val Validator) error { - if err := val.ValidateUntil(u.jitter, u.max, u.now); err != nil { - return err - } - return nil -} -func (untilOption) String() string { - return "Until()" -} diff --git a/options_test.go b/options_test.go index 90160ad..5fd182f 100644 --- a/options_test.go +++ b/options_test.go @@ -3,383 +3,436 @@ package webhook -// import ( -// "testing" -// "time" +import ( + "testing" + "time" -// "github.com/stretchr/testify/assert" -// "github.com/stretchr/testify/require" -// "github.com/xmidt-org/urlegit" -// ) + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xmidt-org/urlegit" +) -// type optionTest struct { -// description string -// in Registration -// opt Option -// opts []Option -// str string -// expectedErr error -// } +type optionTest struct { + description string + in any + opt Option + opts Validators + str string + expectedErr error +} -// func TestErrorOption(t *testing.T) { -// run_tests(t, []optionTest{ -// { -// description: "success", -// str: "foo", -// }, { -// description: "simple error", -// opt: Error(ErrInvalidInput), -// str: "Error('invalid input')", -// expectedErr: ErrInvalidInput, -// }, { -// description: "simple nil error", -// opt: Error(nil), -// str: "Error(nil)", -// }, -// }) -// } +func TestErrorOption(t *testing.T) { + run_tests(t, []optionTest{ + { + description: "success", + in: &RegistrationV1{}, + str: "foo", + }, + { + description: "simple error - RegistrationV1", + opt: Error(ErrInvalidInput), + str: "Error('invalid input')", + expectedErr: ErrInvalidInput, + in: &RegistrationV1{}, + }, + { + description: "simple error - RegistrationV2", + opt: Error(ErrInvalidInput), + str: "Error('invalid input')", + expectedErr: ErrInvalidInput, + in: &RegistrationV2{}, + }, + { + description: "simple nil error", + opt: Error(nil), + str: "Error(nil)", + }, + }) +} -// func TestAtLeastOneEventOption(t *testing.T) { -// run_tests(t, []optionTest{ -// { -// description: "there is an event", -// opt: AtLeastOneEvent(), -// in: Registration{Events: []string{"foo"}}, -// str: "AtLeastOneEvent()", -// }, { -// description: "multiple events", -// opt: AtLeastOneEvent(), -// in: Registration{Events: []string{"foo", "bar"}}, -// str: "AtLeastOneEvent()", -// }, { -// description: "there are no events", -// opt: AtLeastOneEvent(), -// expectedErr: ErrInvalidInput, -// }, -// }) -// } +func TestAlwaysOption(t *testing.T) { + run_tests(t, []optionTest{ + { + description: "success", + opt: AlwaysValid(), + in: &RegistrationV1{}, + str: "alwaysValidOption", + }, + }) +} +func TestAtLeastOneEventOption(t *testing.T) { + run_tests(t, []optionTest{ + { + description: "there is an event - V1", + opt: AtLeastOneEvent(), + in: &RegistrationV1{Events: []string{"foo"}}, + str: "AtLeastOneEvent()", + }, { + description: "multiple events - V1", + opt: AtLeastOneEvent(), + in: &RegistrationV1{Events: []string{"foo", "bar"}}, + str: "AtLeastOneEvent()", + }, { + description: "there are no events - V1", + opt: AtLeastOneEvent(), + in: &RegistrationV1{}, + expectedErr: ErrInvalidInput, + }, + { + description: "invalid type - RegistrationV2", + opt: AtLeastOneEvent(), + in: &RegistrationV2{}, + expectedErr: ErrInvalidType, + }, + { + description: "default case - unknown", + opt: AtLeastOneEvent(), + expectedErr: ErrUknownType, + }, + }) +} -// func TestEventRegexMustCompile(t *testing.T) { -// run_tests(t, []optionTest{ -// { -// description: "the regex compiles", -// opt: EventRegexMustCompile(), -// in: Registration{Events: []string{"event.*"}}, -// str: "EventRegexMustCompile()", -// }, { -// description: "multiple events", -// opt: EventRegexMustCompile(), -// in: Registration{Events: []string{"magic-thing", "event.*"}}, -// str: "EventRegexMustCompile()", -// }, { -// description: "failure", -// opt: EventRegexMustCompile(), -// in: Registration{Events: []string{"("}}, -// expectedErr: ErrInvalidInput, -// }, -// }) -// } +func TestEventRegexMustCompile(t *testing.T) { + run_tests(t, []optionTest{ + { + description: "the regex compiles - V1", + opt: EventRegexMustCompile(), + in: &RegistrationV1{Events: []string{"event.*"}}, + str: "EventRegexMustCompile()", + }, { + description: "multiple events", + opt: EventRegexMustCompile(), + in: &RegistrationV1{Events: []string{"magic-thing", "event.*"}}, + str: "EventRegexMustCompile()", + }, { + description: "failure - V1", + opt: EventRegexMustCompile(), + in: &RegistrationV1{Events: []string{"("}}, + expectedErr: ErrInvalidInput, + }, + { + description: "the regex compiles - V2", + opt: EventRegexMustCompile(), + in: &RegistrationV2{Matcher: []FieldRegex{{Field: "canonical_name", Regex: "webpa"}}}, + str: "EventRegexMustCompile()", + }, + { + description: "multiple matchers - V2", + opt: EventRegexMustCompile(), + in: &RegistrationV2{Matcher: []FieldRegex{{Field: "canonical_name", Regex: "webpa"}, {Field: "address", Regex: "www.example.com"}}}, + str: "EventRegexMustCompile()", + }, + { + description: "failure - V2", + opt: EventRegexMustCompile(), + in: &RegistrationV2{Matcher: []FieldRegex{{Regex: "("}}}, + expectedErr: ErrInvalidInput, + }, + { + description: "default case - unknown", + opt: EventRegexMustCompile(), + expectedErr: ErrUknownType, + }, + }) +} -// func TestDeviceIDRegexMustCompile(t *testing.T) { -// run_tests(t, []optionTest{ -// { -// description: "the regex compiles", -// opt: DeviceIDRegexMustCompile(), -// in: Registration{ -// Matcher: MetadataMatcherConfig{ -// DeviceID: []string{"device.*"}, -// }, -// }, -// str: "DeviceIDRegexMustCompile()", -// }, { -// description: "multiple device ids", -// opt: DeviceIDRegexMustCompile(), -// in: Registration{ -// Matcher: MetadataMatcherConfig{ -// DeviceID: []string{"device.*", "magic-thing"}, -// }, -// }, -// str: "DeviceIDRegexMustCompile()", -// }, { -// description: "failure", -// opt: DeviceIDRegexMustCompile(), -// in: Registration{ -// Matcher: MetadataMatcherConfig{ -// DeviceID: []string{"("}, -// }, -// }, -// expectedErr: ErrInvalidInput, -// }, -// }) -// } +func TestDeviceIDRegexMustCompile(t *testing.T) { + run_tests(t, []optionTest{ + { + description: "the regex compiles - v1", + opt: DeviceIDRegexMustCompile(), + in: &RegistrationV1{Matcher: MetadataMatcherConfig{DeviceID: []string{"device.*"}}}, + str: "DeviceIDRegexMustCompile()", + }, { + description: "multiple device ids - v1", + opt: DeviceIDRegexMustCompile(), + in: &RegistrationV1{Matcher: MetadataMatcherConfig{DeviceID: []string{"device.*", "magic-thing"}}}, + str: "DeviceIDRegexMustCompile()", + }, { + description: "failure - v1", + opt: DeviceIDRegexMustCompile(), + in: &RegistrationV1{Matcher: MetadataMatcherConfig{DeviceID: []string{"("}}}, + expectedErr: ErrInvalidInput, + }, + { + description: "invalid type - v2", + opt: DeviceIDRegexMustCompile(), + in: &RegistrationV2{}, + expectedErr: ErrInvalidType, + }, + { + description: "default case - unknown", + opt: DeviceIDRegexMustCompile(), + expectedErr: ErrUknownType, + }, + }) +} -// func TestValidateRegistrationDuration(t *testing.T) { -// now := func() time.Time { -// return time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) -// } -// run_tests(t, []optionTest{ -// { -// description: "success with time in bounds", -// opt: ValidateRegistrationDuration(5 * time.Minute), -// in: Registration{ -// Duration: CustomDuration(4 * time.Minute), -// }, -// str: "ValidateRegistrationDuration(5m0s)", -// }, { -// description: "success with time in bounds, exactly", -// opt: ValidateRegistrationDuration(5 * time.Minute), -// in: Registration{ -// Duration: CustomDuration(5 * time.Minute), -// }, -// }, { -// description: "failure with time out of bounds", -// opt: ValidateRegistrationDuration(5 * time.Minute), -// in: Registration{ -// Duration: CustomDuration(6 * time.Minute), -// }, -// expectedErr: ErrInvalidInput, -// }, { -// description: "success with max ttl ignored", -// opt: ValidateRegistrationDuration(-5 * time.Minute), -// in: Registration{ -// Duration: CustomDuration(1 * time.Minute), -// }, -// }, { -// description: "success with max ttl ignored, 0 duration", -// opt: ValidateRegistrationDuration(0), -// in: Registration{ -// Duration: CustomDuration(1 * time.Minute), -// }, -// }, { -// description: "success with until in bounds", -// opts: []Option{ -// ProvideTimeNowFunc(now), -// ValidateRegistrationDuration(5 * time.Minute), -// }, -// in: Registration{ -// Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), -// }, -// }, { -// description: "failure due to until being before now", -// opts: []Option{ -// ValidateRegistrationDuration(5 * time.Minute), -// ProvideTimeNowFunc(now), -// }, -// in: Registration{ -// Until: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), -// }, -// expectedErr: ErrInvalidInput, -// }, { -// description: "success with until exactly in bounds", -// opts: []Option{ -// ProvideTimeNowFunc(now), -// ValidateRegistrationDuration(5 * time.Minute), -// }, -// in: Registration{ -// Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), -// }, -// }, { -// description: "failure due to the options being out of order", -// opts: []Option{ -// ValidateRegistrationDuration(5 * time.Minute), -// ProvideTimeNowFunc(now), -// }, -// in: Registration{ -// Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC), -// }, -// expectedErr: ErrInvalidInput, -// }, { -// description: "failure with until out of bounds", -// opts: []Option{ -// ProvideTimeNowFunc(now), -// ValidateRegistrationDuration(5 * time.Minute), -// }, -// in: Registration{ -// Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), -// }, -// expectedErr: ErrInvalidInput, -// }, { -// description: "success with until just needing to be present", -// opts: []Option{ -// ProvideTimeNowFunc(now), -// ValidateRegistrationDuration(0), -// }, -// in: Registration{ -// Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC), -// }, -// }, { -// description: "failure, both expirations set", -// opt: ValidateRegistrationDuration(5 * time.Minute), -// in: Registration{ -// Duration: CustomDuration(1 * time.Minute), -// Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC), -// }, -// expectedErr: ErrInvalidInput, -// }, { -// description: "failure, no expiration set", -// opt: ValidateRegistrationDuration(5 * time.Minute), -// expectedErr: ErrInvalidInput, -// }, -// }) -// } +func TestValidateRegistrationDuration(t *testing.T) { + now := func() time.Time { + return time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) + } + run_tests(t, []optionTest{ + { + description: "success with time in bounds - V1", + opt: ValidateRegistrationDuration(5 * time.Minute), + in: &RegistrationV1{Duration: CustomDuration(4 * time.Minute)}, + str: "ValidateRegistrationDuration(5m0s)", + }, { + description: "success with time in bounds, exactly - V1", + opt: ValidateRegistrationDuration(5 * time.Minute), + in: &RegistrationV1{Duration: CustomDuration(5 * time.Minute)}, + }, { + description: "failure with time out of bounds - V1", + opt: ValidateRegistrationDuration(5 * time.Minute), + in: &RegistrationV1{Duration: CustomDuration(6 * time.Minute)}, + expectedErr: ErrInvalidInput, + }, { + description: "success with max ttl ignored - V1", + opt: ValidateRegistrationDuration(-5 * time.Minute), + in: &RegistrationV1{Duration: CustomDuration(1 * time.Minute)}, + }, { + description: "success with max ttl ignored, 0 duration - V1", + opt: ValidateRegistrationDuration(0), + in: &RegistrationV1{Duration: CustomDuration(1 * time.Minute)}, + }, { + description: "success with until in bounds - V1", + opts: []Option{ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute)}, + in: &RegistrationV1{Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC)}, + }, { + description: "failure due to until being before now - V1", + opts: []Option{ValidateRegistrationDuration(5 * time.Minute), ProvideTimeNowFunc(now)}, + in: &RegistrationV1{ + Until: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + }, + expectedErr: ErrInvalidInput, + }, { + description: "success with until exactly in bounds - V1", + opts: []Option{ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute)}, + in: &RegistrationV1{Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC)}, + }, { + description: "failure due to the options being out of order - V1", + opts: []Option{ValidateRegistrationDuration(5 * time.Minute), ProvideTimeNowFunc(now)}, + in: &RegistrationV1{Until: time.Date(2021, 1, 1, 0, 5, 0, 0, time.UTC)}, + expectedErr: ErrInvalidInput, + }, { + description: "failure with until out of bounds - V1", + opts: []Option{ProvideTimeNowFunc(now), ValidateRegistrationDuration(5 * time.Minute)}, + in: &RegistrationV1{Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC)}, + expectedErr: ErrInvalidInput, + }, { + description: "success with until just needing to be present - V1", + opts: []Option{ProvideTimeNowFunc(now), ValidateRegistrationDuration(0)}, + in: &RegistrationV1{Until: time.Date(2021, 1, 1, 0, 6, 0, 0, time.UTC)}, + }, { + description: "failure, both expirations set - V1", + opt: ValidateRegistrationDuration(5 * time.Minute), + in: &RegistrationV1{Duration: CustomDuration(1 * time.Minute), Until: time.Date(2021, 1, 1, 0, 4, 0, 0, time.UTC)}, + expectedErr: ErrInvalidInput, + }, { + description: "failure, no expiration set - V1", + in: &RegistrationV1{}, + opt: ValidateRegistrationDuration(5 * time.Minute), + expectedErr: ErrInvalidInput, + }, { + description: "failure, exipred - V2", + in: &RegistrationV2{Expires: now()}, + opt: ValidateRegistrationDuration(0), + expectedErr: ErrInvalidInput, + }, + { + description: "default case - unknown", + opt: ValidateRegistrationDuration(5 * time.Minute), + expectedErr: ErrUknownType, + }, + }) +} -// func TestProvideTimeNowFunc(t *testing.T) { -// now := func() time.Time { -// return time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) -// } +func TestProvideTimeNowFunc(t *testing.T) { + now := func() time.Time { + return time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) + } -// run_tests(t, []optionTest{ -// { -// description: "success", -// opt: ProvideTimeNowFunc(now), -// str: "ProvideTimeNowFunc(func)", -// }, { -// description: "success as nil", -// opt: ProvideTimeNowFunc(nil), -// str: "ProvideTimeNowFunc(nil)", -// }, -// }) -// } + run_tests(t, []optionTest{ + { + description: "success", + opt: ProvideTimeNowFunc(now), + str: "ProvideTimeNowFunc(func)", + }, { + description: "success as nil", + opt: ProvideTimeNowFunc(nil), + str: "ProvideTimeNowFunc(nil)", + }, + }) +} -// func TestProvideFailureURLValidator(t *testing.T) { -// checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) -// require.NoError(t, err) -// require.NotNil(t, checker) +func TestProvideFailureURLValidator(t *testing.T) { + checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) + require.NoError(t, err) + require.NotNil(t, checker) -// run_tests(t, []optionTest{ -// { -// description: "success, no checker", -// opt: ProvideFailureURLValidator(nil), -// str: "ProvideFailureURLValidator(nil)", -// }, { -// description: "success, with checker", -// opt: ProvideFailureURLValidator(checker), -// in: Registration{ -// FailureURL: "https://example.com", -// }, -// str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", -// }, { -// description: "failure, with checker", -// opt: ProvideFailureURLValidator(checker), -// in: Registration{ -// FailureURL: "http://example.com", -// }, -// expectedErr: ErrInvalidInput, -// }, -// }) -// } + run_tests(t, []optionTest{ + { + description: "success, no checker", + opt: ProvideFailureURLValidator(nil), + str: "ProvideFailureURLValidator(nil)", + }, { + description: "success, with checker - V1", + opt: ProvideFailureURLValidator(checker), + in: &RegistrationV1{FailureURL: "https://example.com"}, + str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + }, { + description: "failure, with checker - V1", + opt: ProvideFailureURLValidator(checker), + in: &RegistrationV1{FailureURL: "http://example.com"}, + expectedErr: ErrInvalidInput, + }, { + description: "success, with checker - V2", + opt: ProvideFailureURLValidator(checker), + in: &RegistrationV2{FailureURL: "https://example.com"}, + str: "ProvideFailureURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + }, { + description: "failure, with checker - V2", + opt: ProvideFailureURLValidator(checker), + in: &RegistrationV2{FailureURL: "http://example.com"}, + expectedErr: ErrInvalidInput, + }, { + description: "default case - unknown", + opt: ProvideFailureURLValidator(checker), + expectedErr: ErrUknownType, + }, + }) +} -// func TestProvideReceiverURLValidator(t *testing.T) { -// checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) -// require.NoError(t, err) -// require.NotNil(t, checker) +func TestProvideReceiverURLValidator(t *testing.T) { + checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) + require.NoError(t, err) + require.NotNil(t, checker) -// run_tests(t, []optionTest{ -// { -// description: "success, no checker", -// opt: ProvideReceiverURLValidator(nil), -// str: "ProvideReceiverURLValidator(nil)", -// }, { -// description: "success, with checker", -// opt: ProvideReceiverURLValidator(checker), -// in: Registration{ -// Config: DeliveryConfig{ -// ReceiverURL: "https://example.com", -// }, -// }, -// str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", -// }, { -// description: "failure, with checker", -// opt: ProvideReceiverURLValidator(checker), -// in: Registration{ -// Config: DeliveryConfig{ -// ReceiverURL: "http://example.com", -// }, -// }, -// expectedErr: ErrInvalidInput, -// }, -// }) -// } + run_tests(t, []optionTest{ + { + description: "success, no checker", + opt: ProvideReceiverURLValidator(nil), + str: "ProvideReceiverURLValidator(nil)", + }, { + description: "success, with checker - V1", + opt: ProvideReceiverURLValidator(checker), + in: &RegistrationV1{Config: DeliveryConfig{ReceiverURL: "https://example.com"}}, + str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + }, { + description: "failure, with checker - V1", + opt: ProvideReceiverURLValidator(checker), + in: &RegistrationV1{Config: DeliveryConfig{ReceiverURL: "http://example.com"}}, + expectedErr: ErrInvalidInput, + }, { + description: "success, with checker - V2", + opt: ProvideReceiverURLValidator(checker), + in: &RegistrationV2{Webhooks: []Webhook{{ReceiverURLs: []string{"https://example.com", "https://example2.com"}}}}, + str: "ProvideReceiverURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + }, { + description: "failure, with checker - V2", + opt: ProvideReceiverURLValidator(checker), + in: &RegistrationV2{Webhooks: []Webhook{{ReceiverURLs: []string{"https://example.com", "http://example2.com"}}}}, + expectedErr: ErrInvalidInput, + }, { + description: "default case - unknown", + opt: ProvideReceiverURLValidator(checker), + expectedErr: ErrUknownType, + }, + }) +} -// func TestProvideAlternativeURLValidator(t *testing.T) { -// checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) -// require.NoError(t, err) -// require.NotNil(t, checker) +func TestProvideAlternativeURLValidator(t *testing.T) { + checker, err := urlegit.New(urlegit.OnlyAllowSchemes("https")) + require.NoError(t, err) + require.NotNil(t, checker) -// run_tests(t, []optionTest{ -// { -// description: "success, no checker", -// opt: ProvideAlternativeURLValidator(nil), -// str: "ProvideAlternativeURLValidator(nil)", -// }, { -// description: "success, with checker", -// opt: ProvideAlternativeURLValidator(checker), -// in: Registration{ -// Config: DeliveryConfig{ -// AlternativeURLs: []string{"https://example.com"}, -// }, -// }, -// str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", -// }, { -// description: "success, with checker and multiple urls", -// opt: ProvideAlternativeURLValidator(checker), -// in: Registration{ -// Config: DeliveryConfig{ -// AlternativeURLs: []string{"https://example.com", "https://example.org"}, -// }, -// }, -// str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", -// }, { -// description: "failure, with checker", -// opt: ProvideAlternativeURLValidator(checker), -// in: Registration{ -// Config: DeliveryConfig{ -// AlternativeURLs: []string{"http://example.com"}, -// }, -// }, -// expectedErr: ErrInvalidInput, -// }, { -// description: "failure, with checker with multiple urls", -// opt: ProvideAlternativeURLValidator(checker), -// in: Registration{ -// Config: DeliveryConfig{ -// AlternativeURLs: []string{"https://example.com", "http://example.com"}, -// }, -// }, -// expectedErr: ErrInvalidInput, -// }, -// }) -// } + run_tests(t, []optionTest{ + { + description: "success, no checker", + opt: ProvideAlternativeURLValidator(nil), + str: "ProvideAlternativeURLValidator(nil)", + }, { + description: "success, with checker", + opt: ProvideAlternativeURLValidator(checker), + in: &RegistrationV1{Config: DeliveryConfig{AlternativeURLs: []string{"https://example.com"}}}, + str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + }, { + description: "success, with checker and multiple urls", + opt: ProvideAlternativeURLValidator(checker), + in: &RegistrationV1{Config: DeliveryConfig{AlternativeURLs: []string{"https://example.com", "https://example.org"}}}, + str: "ProvideAlternativeURLValidator(urlegit.Checker{ OnlyAllowSchemes('https') })", + }, { + description: "failure, with checker", + opt: ProvideAlternativeURLValidator(checker), + in: &RegistrationV1{Config: DeliveryConfig{AlternativeURLs: []string{"http://example.com"}}}, + expectedErr: ErrInvalidInput, + }, { + description: "failure, with checker with multiple urls", + opt: ProvideAlternativeURLValidator(checker), + in: &RegistrationV1{Config: DeliveryConfig{AlternativeURLs: []string{"https://example.com", "http://example.com"}}}, + expectedErr: ErrInvalidInput, + }, { + description: "failure - RegistrationV2", + opt: ProvideAlternativeURLValidator(checker), + in: &RegistrationV2{}, + expectedErr: ErrInvalidType, + }, { + description: "default case - unknown", + opt: ProvideAlternativeURLValidator(checker), + expectedErr: ErrUknownType, + }, + }) +} -// func TestNoUntil(t *testing.T) { -// run_tests(t, []optionTest{ -// { -// description: "success, no until set", -// opt: NoUntil(), -// str: "NoUntil()", -// }, { -// description: "detect until set", -// opt: NoUntil(), -// in: Registration{ -// Until: time.Now(), -// }, -// expectedErr: ErrInvalidInput, -// }, -// }) -// } -// func run_tests(t *testing.T, tests []optionTest) { -// for _, tc := range tests { -// t.Run(tc.description, func(t *testing.T) { -// assert := assert.New(t) +func TestNoUntil(t *testing.T) { + run_tests(t, []optionTest{ + { + description: "success, no until set", + in: &RegistrationV1{}, + opt: NoUntil(), + str: "NoUntil()", + }, { + description: "detect until set", + opt: NoUntil(), + in: &RegistrationV1{ + Until: time.Now(), + }, + expectedErr: ErrInvalidInput, + }, + { + description: "failure - V2", + opt: NoUntil(), + in: &RegistrationV2{}, + expectedErr: ErrInvalidType, + }, + { + description: "default case - unknown", + opt: NoUntil(), + expectedErr: ErrUknownType, + }, + }) +} -// opts := append(tc.opts, tc.opt) -// err := tc.in.Validate(opts...) +func run_tests(t *testing.T, tests []optionTest) { + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + assert := assert.New(t) + var err error + opts := append(tc.opts, tc.opt) + switch r := tc.in.(type) { + case *RegistrationV1: + err = opts.Validate(r) + case *RegistrationV2: + err = opts.Validate(r) + default: + err = opts.Validate(nil) + } + assert.ErrorIs(err, tc.expectedErr) -// assert.ErrorIs(err, tc.expectedErr) - -// if tc.str != "" && tc.opt != nil { -// assert.Equal(tc.str, tc.opt.String()) -// } -// }) -// } -// } + if tc.str != "" && tc.opt != nil { + assert.Equal(tc.str, tc.opt.String()) + } + }) + } +} diff --git a/validation.go b/validation.go deleted file mode 100644 index 48db5fc..0000000 --- a/validation.go +++ /dev/null @@ -1,104 +0,0 @@ -package webhook - -import ( - "time" - - "github.com/xmidt-org/urlegit" -) - -type Validator interface { - ValidateOneEvent() error - ValidateEventRegex() error - ValidateDeviceId() error - ValidateUntil(time.Duration, time.Duration, func() time.Time) error - ValidateNoUntil() error - ValidateDuration(time.Duration) error - ValidateFailureURL(*urlegit.Checker) error - ValidateReceiverURL(*urlegit.Checker) error - ValidateAltURL(*urlegit.Checker) error - SetNowFunc(func() time.Time) -} - -type ValidatorConfig struct { - URL URLVConfig - TTL TTLVConfig -} - -type URLVConfig struct { - HTTPSOnly bool - AllowLoopback bool - AllowIP bool - AllowSpecialUseHosts bool - AllowSpecialUseIPs bool - InvalidHosts []string - InvalidSubnets []string -} - -type TTLVConfig struct { - Max time.Duration - Jitter time.Duration - Now func() time.Time -} - -var ( - SpecialUseIPs = []string{ - "0.0.0.0/8", //local ipv4 - "fe80::/10", //local ipv6 - "255.255.255.255/32", //broadcast to neighbors - "2001::/32", //ipv6 TEREDO prefix - "2001:5::/32", //EID space for lisp - "2002::/16", //ipv6 6to4 - "fc00::/7", //ipv6 unique local - "192.0.0.0/24", //ipv4 IANA - "2001:0000::/23", //ipv6 IANA - "224.0.0.1/32", //ipv4 multicast - } - // errFailedToBuildValidators = errors.New("failed to build validators") - // errFailedToBuildValidURLFuncs = errors.New("failed to build ValidURLFuncs") -) - -// BuildURLChecker translates the configuration into url Checker to be run on the webhook. -func buildURLChecker(config ValidatorConfig) (*urlegit.Checker, error) { - var o []urlegit.Option - if config.URL.HTTPSOnly { - o = append(o, urlegit.OnlyAllowSchemes("https")) - } - if !config.URL.AllowLoopback { - o = append(o, urlegit.ForbidLoopback()) - } - if !config.URL.AllowIP { - o = append(o, urlegit.ForbidAnyIPs()) - } - if !config.URL.AllowSpecialUseHosts { - o = append(o, urlegit.ForbidSpecialUseDomains()) - } - if !config.URL.AllowSpecialUseIPs { - o = append(o, urlegit.ForbidSubnets(SpecialUseIPs)) - } - checker, err := urlegit.New(o...) - if err != nil { - return nil, err - } - return checker, nil -} - -// BuildValidators translates the configuration into a list of validators to be run on the -// webhook. -func BuildValidators(config ValidatorConfig) ([]Option, error) { - var opts []Option - - checker, err := buildURLChecker(config) - if err != nil { - return nil, err - } - opts = append(opts, - AtLeastOneEvent(), - EventRegexMustCompile(), - DeviceIDRegexMustCompile(), - ValidateRegistrationDuration(config.TTL.Max), - ProvideReceiverURLValidator(checker), - ProvideFailureURLValidator(checker), - ProvideAlternativeURLValidator(checker), - ) - return opts, nil -} diff --git a/webhook.go b/webhook.go index b1f9c9c..c0a3a4a 100644 --- a/webhook.go +++ b/webhook.go @@ -14,13 +14,10 @@ import ( var ( ErrInvalidInput = fmt.Errorf("invalid input") + ErrInvalidType = fmt.Errorf("invalid type") + ErrUknownType = fmt.Errorf("unknown type") ) -type Register interface { - GetId() string - GetUntil() time.Time -} - // Deprecated: This substructure should only be used for backwards compatibility // matching. Use Webhook instead. // DeliveryConfig is a Webhook substructure with data related to event delivery. @@ -209,7 +206,7 @@ type RegistrationV2 struct { FailureURL string `json:"failure_url"` // Matcher is the list of regular expressions to match incoming events against to. - // Note. Any failures due to a bad regex feild or regex expression will result in a silent failure. + // Note. Any failures due to a bad regex field or regex expression will result in a silent failure. Matcher []FieldRegex `json:"matcher,omitempty"` // Expires describes the time this subscription expires. @@ -219,16 +216,17 @@ type RegistrationV2 struct { type Option interface { fmt.Stringer - Validate(Validator) error + Validate(any) error } +type Validators []Option -// Validate is a method on Registration that validates the registration +// Validate is a method that validates the registration // against a list of options. -func Validate(v Validator, opts []Option) error { +func (vs Validators) Validate(r any) error { var errs error - for _, opt := range opts { + for _, opt := range vs { if opt != nil { - if err := opt.Validate(v); err != nil { + if err := opt.Validate(r); err != nil { errs = errors.Join(errs, err) } } @@ -302,15 +300,6 @@ func (v1 *RegistrationV1) ValidateDuration(ttl time.Duration) error { return errs } -func (v1 *RegistrationV1) ValidateFailureURL(c *urlegit.Checker) error { - if v1.FailureURL != "" { - if err := c.Text(v1.FailureURL); err != nil { - return fmt.Errorf("%w: failure url is invalid", ErrInvalidInput) - } - } - return nil -} - func (v1 *RegistrationV1) ValidateReceiverURL(c *urlegit.Checker) error { if v1.Config.ReceiverURL != "" { if err := c.Text(v1.Config.ReceiverURL); err != nil { @@ -363,3 +352,36 @@ func (v1 *RegistrationV1) ValidateUntil(jitter time.Duration, maxTTL time.Durati func (v1 *RegistrationV1) SetNowFunc(now func() time.Time) { v1.nowFunc = now } + +func (v2 *RegistrationV2) ValidateEventRegex() error { + var errs error + for _, m := range v2.Matcher { + _, err := regexp.Compile(m.Regex) + if err != nil { + errs = errors.Join(fmt.Errorf("%w: %v", ErrInvalidInput, err)) + } + } + return errs +} + +func (v2 *RegistrationV2) ValidateDuration() error { + now := time.Now() + if now.After(v2.Expires) { + return fmt.Errorf("%w: the registration has already expired", ErrInvalidInput) + } + return nil +} + +func (v2 *RegistrationV2) ValidateReceiverURL(checker *urlegit.Checker) error { + var errs error + for _, w := range v2.Webhooks { + for _, url := range w.ReceiverURLs { + if url != "" { + if err := checker.Text(url); err != nil { + errs = errors.Join(errs, fmt.Errorf("%w: receiver url [%v] is invalid for webhook [%v]", ErrInvalidInput, url, w)) + } + } + } + } + return errs +}