-
Notifications
You must be signed in to change notification settings - Fork 1
feat: custom priority validator #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package bulwark | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "log/slog" | ||
|
|
||
| "github.com/deixis/faults" | ||
| ) | ||
|
|
||
| var ( | ||
| // AssertValidPriority panics when a priority is out of range. | ||
| // A priority is out of range when it is less than 0 or greater than or equal | ||
| // to priorities. | ||
| AssertValidPriority = func(p Priority, priorities int) (Priority, error) { | ||
| if p < 0 || int(p) >= priorities { | ||
| panic(fmt.Sprintf("bulwark: priority must be in the range [0, %d), but got %d", priorities, p)) | ||
| } | ||
|
|
||
| return p, nil | ||
| } | ||
|
|
||
| // ClampInvalidPriority clamps any out-of-range priority to the lowest valid | ||
| // priority (priorities-1). This applies to both negative values and values | ||
| // that exceed the configured number of priorities, preventing invalid or | ||
| // malicious input from being promoted to a higher-importance tier. | ||
| ClampInvalidPriority = func(p Priority, priorities int) (Priority, error) { | ||
| if p >= 0 && int(p) < priorities { | ||
| return p, nil | ||
| } | ||
| slog.Warn("bulwark: priority is out of range", "max", priorities-1, "priority", p) | ||
|
|
||
| return Priority(priorities - 1), nil | ||
| } | ||
|
|
||
| // RejectInvalidPriority returns an error when a priority is out of range. | ||
| // A priority is out of range when it is less than 0 or greater than or equal | ||
| // to priorities. | ||
| RejectInvalidPriority = func(p Priority, priorities int) (Priority, error) { | ||
| if p < 0 || int(p) >= priorities { | ||
| return p, faults.Bad(&faults.FieldViolation{ | ||
| Field: "priority", | ||
| Description: fmt.Sprintf("priority must be in the range [0, %d), but got %d", priorities, p), | ||
| }) | ||
| } | ||
|
|
||
| return p, nil | ||
| } | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| package bulwark | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
| ) | ||
|
|
||
| // TestAssertValidPriority verifies that AssertValidPriority passes valid | ||
| // priorities through unchanged and panics for any priority outside [0, priorities). | ||
| // This validator is intended for development environments where misuse of the | ||
| // library should be caught immediately rather than silently corrected. | ||
| func TestAssertValidPriority(t *testing.T) { | ||
| t.Run("valid priority passes through", func(t *testing.T) { | ||
| for _, p := range []Priority{High, Important, Medium, Low} { | ||
| got, err := AssertValidPriority(p, StandardPriorities) | ||
| if err != nil { | ||
| t.Errorf("priority %d: unexpected error: %v", p, err) | ||
| } | ||
| if got != p { | ||
| t.Errorf("priority %d: expected %d, got %d", p, p, got) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| t.Run("negative priority panics", func(t *testing.T) { | ||
| defer func() { | ||
| if r := recover(); r == nil { | ||
| t.Error("expected panic, got none") | ||
| } | ||
| }() | ||
| AssertValidPriority(-1, StandardPriorities) | ||
| }) | ||
|
|
||
| t.Run("out of range priority panics", func(t *testing.T) { | ||
| defer func() { | ||
| if r := recover(); r == nil { | ||
| t.Error("expected panic, got none") | ||
| } | ||
| }() | ||
| AssertValidPriority(Priority(StandardPriorities), StandardPriorities) | ||
| }) | ||
| } | ||
|
|
||
| // TestClampInvalidPriority verifies that ClampInvalidPriority passes valid | ||
| // priorities through unchanged and clamps any out-of-range priority — including | ||
| // negative values — to the lowest valid priority (priorities-1). Clamping to | ||
| // the lowest rather than the nearest boundary ensures that invalid or malicious | ||
| // input is never silently promoted to a higher-importance tier. | ||
| func TestClampInvalidPriority(t *testing.T) { | ||
| t.Run("valid priority passes through", func(t *testing.T) { | ||
| for _, p := range []Priority{High, Important, Medium, Low} { | ||
| got, err := ClampInvalidPriority(p, StandardPriorities) | ||
| if err != nil { | ||
| t.Errorf("priority %d: unexpected error: %v", p, err) | ||
| } | ||
| if got != p { | ||
| t.Errorf("priority %d: expected %d, got %d", p, p, got) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| t.Run("negative priority clamped to lowest", func(t *testing.T) { | ||
| got, err := ClampInvalidPriority(-1, StandardPriorities) | ||
| if err != nil { | ||
| t.Errorf("unexpected error: %v", err) | ||
| } | ||
| if got != Priority(StandardPriorities-1) { | ||
| t.Errorf("expected %d, got %d", StandardPriorities-1, got) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("out of range priority adjusted to lowest", func(t *testing.T) { | ||
| got, err := ClampInvalidPriority(Priority(StandardPriorities), StandardPriorities) | ||
| if err != nil { | ||
| t.Errorf("unexpected error: %v", err) | ||
| } | ||
| if got != Priority(StandardPriorities-1) { | ||
| t.Errorf("expected %d, got %d", StandardPriorities-1, got) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| // TestRejectInvalidPriority verifies that RejectInvalidPriority passes valid | ||
| // priorities through unchanged and returns an error for any priority outside | ||
| // [0, priorities). This validator is suited for APIs where the caller is | ||
| // responsible for providing a valid priority and must handle the error. | ||
| func TestRejectInvalidPriority(t *testing.T) { | ||
| t.Run("valid priority passes through", func(t *testing.T) { | ||
| for _, p := range []Priority{High, Important, Medium, Low} { | ||
| got, err := RejectInvalidPriority(p, StandardPriorities) | ||
| if err != nil { | ||
| t.Errorf("priority %d: unexpected error: %v", p, err) | ||
| } | ||
| if got != p { | ||
| t.Errorf("priority %d: expected %d, got %d", p, p, got) | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| t.Run("negative priority returns error", func(t *testing.T) { | ||
| _, err := RejectInvalidPriority(-1, StandardPriorities) | ||
| if err == nil { | ||
| t.Error("expected error, got nil") | ||
| } | ||
| }) | ||
|
|
||
| t.Run("out of range priority returns error", func(t *testing.T) { | ||
| _, err := RejectInvalidPriority(Priority(StandardPriorities), StandardPriorities) | ||
| if err == nil { | ||
| t.Error("expected error, got nil") | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| // TestWithPriorityValidator verifies that WithPriorityValidator wires a custom | ||
| // validator into the throttle so that it is called on every request. It also | ||
| // confirms that the default behaviour (no option provided) uses | ||
| // ClampInvalidPriority: out-of-range priorities are clamped rather than | ||
| // rejected, so the throttled function is still invoked. | ||
| func TestWithPriorityValidator(t *testing.T) { | ||
| t.Run("custom validator is applied", func(t *testing.T) { | ||
| called := false | ||
| throttle := NewAdaptiveThrottle(StandardPriorities, | ||
| WithPriorityValidator(func(p Priority, priorities int) (Priority, error) { | ||
| called = true | ||
| return p, nil | ||
| }), | ||
| ) | ||
| throttle.Throttle(context.Background(), High, func(_ context.Context) error { | ||
| return nil | ||
| }) | ||
| if !called { | ||
| t.Error("expected custom validator to be called") | ||
| } | ||
| }) | ||
|
|
||
| t.Run("RejectInvalidPriority used as validator rejects invalid priority", func(t *testing.T) { | ||
| throttle := NewAdaptiveThrottle(StandardPriorities, | ||
| WithPriorityValidator(RejectInvalidPriority), | ||
| ) | ||
| err := throttle.Throttle(context.Background(), Priority(StandardPriorities), func(_ context.Context) error { | ||
| return nil | ||
| }) | ||
| if err == nil { | ||
| t.Error("expected error for out-of-range priority, got nil") | ||
| } | ||
| }) | ||
|
|
||
| t.Run("default validator adjusts invalid priority", func(t *testing.T) { | ||
| throttle := NewAdaptiveThrottle(StandardPriorities) | ||
| called := false | ||
| err := throttle.Throttle(context.Background(), Priority(StandardPriorities), func(_ context.Context) error { | ||
| called = true | ||
| return nil | ||
| }) | ||
| if err != nil { | ||
| t.Errorf("unexpected error: %v", err) | ||
| } | ||
| if !called { | ||
| t.Error("expected throttled function to be called after priority adjustment") | ||
| } | ||
| }) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.