├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── validate.yml ├── .gitignore ├── .golangci.yml ├── LICENSE.md ├── README.md ├── benchmark_test.go ├── doc.go ├── example_package_test.go ├── example_test.go ├── fault.go ├── fault_test.go ├── go.mod ├── go.sum ├── helpers_test.go ├── injector.go ├── injector_chain.go ├── injector_chain_test.go ├── injector_error.go ├── injector_error_test.go ├── injector_random.go ├── injector_random_test.go ├── injector_reject.go ├── injector_reject_test.go ├── injector_slow.go ├── injector_slow_test.go └── reporter.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @lingrino 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | open-pull-requests-limit: 100 5 | directory: / 6 | schedule: 7 | time: "08:00" 8 | timezone: "America/Los_Angeles" 9 | interval: weekly 10 | allow: 11 | - dependency-type: direct 12 | groups: 13 | all: 14 | patterns: 15 | - "*" 16 | reviewers: 17 | - lingrino 18 | 19 | - package-ecosystem: gomod 20 | open-pull-requests-limit: 100 21 | directory: / 22 | schedule: 23 | time: "08:00" 24 | timezone: "America/Los_Angeles" 25 | interval: weekly 26 | allow: 27 | - dependency-type: direct 28 | groups: 29 | all: 30 | patterns: 31 | - "*" 32 | reviewers: 33 | - lingrino 34 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs all of our lints, tests, and other requirements for merging code. 2 | name: Validate 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | validate: 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | go-version: [oldstable, stable] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Install Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: ${{ matrix.go-version }} 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Format 21 | run: test -z $(gofmt -l -w -s ./) 22 | - name: Lint 23 | uses: golangci/golangci-lint-action@v6.5.1 24 | - name: Test 25 | run: go test -v -race -cover -coverprofile=coverage.txt ./... | tee -a test-results.txt 26 | - name: Enforce 100% Test Coverage 27 | run: | 28 | if ! grep -q "coverage: 100.0% of statements" test-results.txt; then 29 | echo "::error::Test Coverage is not 100%" 30 | exit 1 31 | fi 32 | - name: Benchmark 33 | run: | 34 | for i in {1..5}; do 35 | go test -run=XXX -bench=. | tee -a bench.txt 36 | done 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories 15 | vendor/ 16 | 17 | # Benchmark artifacts 18 | bench.txt 19 | new.txt 20 | old.txt 21 | 22 | # Test Artifacts 23 | coverage.txt 24 | test-results.txt 25 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | 4 | linters: 5 | enable: 6 | - bodyclose 7 | - copyloopvar 8 | - dogsled 9 | - dupl 10 | - err113 11 | - errcheck 12 | - funlen 13 | - gochecknoglobals 14 | - gochecknoinits 15 | - gocognit 16 | - goconst 17 | - gocritic 18 | - gocyclo 19 | - godot 20 | - godox 21 | - gofmt 22 | - goimports 23 | - goprintffuncname 24 | - gosec 25 | - gosimple 26 | - govet 27 | - ineffassign 28 | - lll 29 | - misspell 30 | - mnd 31 | - nakedret 32 | - nestif 33 | - noctx 34 | - nolintlint 35 | - prealloc 36 | - rowserrcheck 37 | - staticcheck 38 | - stylecheck 39 | - typecheck 40 | - unconvert 41 | - unparam 42 | - unused 43 | - whitespace 44 | 45 | linters-settings: 46 | dupl: 47 | threshold: 100 48 | errcheck: 49 | check-blank: true 50 | gocognit: 51 | min-complexity: 10 52 | goconst: 53 | min-occurrences: 2 54 | gocyclo: 55 | min-complexity: 10 56 | nakedret: 57 | max-func-lines: 0 58 | 59 | issues: 60 | exclude-rules: 61 | - text: "G404:" # Ignore weak random number generator lint. We do not need strong randomness here. 62 | linters: 63 | - gosec 64 | - path: _test.go 65 | linters: 66 | - dupl # many functions in tests look like duplicates 67 | - funlen # test function can be very long due to test cases 68 | - goconst # test function can contain many constants 69 | - path: (example|benchmark)_.*test.go 70 | linters: 71 | - errcheck # not required to check errors in examples/benchmarks 72 | - ineffassign # not required to check errors in examples/benchmarks 73 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Sean Lingren 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fault 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/lingrino/go-fault)](https://pkg.go.dev/github.com/lingrino/go-fault) [![goreportcard](https://goreportcard.com/badge/github.com/lingrino/go-fault)](https://goreportcard.com/report/github.com/lingrino/go-fault) 4 | 5 | The fault package provides go http middleware that makes it easy to inject faults into your service. Use the fault package to reject incoming requests, respond with an HTTP error, inject latency into a percentage of your requests, or inject any of your own custom faults. 6 | 7 | ## Features 8 | 9 | The fault package works through [standard go http middleware](https://pkg.go.dev/net/http/?tab=doc#Handler). You first create an `Injector`, which is a middleware with the code to be run on injection. Then you wrap that `Injector` in a `Fault` which handles logic about when to run your `Injector`. 10 | 11 | There are currently three kinds of injectors: `SlowInjector`, `ErrorInjector`, and `RejectInjector`. Each of these injectors can be configured through a `Fault` to run on a small percent of your requests. You can also configure the `Fault` to blocklist/allowlist certain paths. 12 | 13 | See the usage section below for an example of how to get started and the [godoc](https://pkg.go.dev/github.com/lingrino/go-fault?tab=doc) for further documentation. 14 | 15 | ## Limitations 16 | 17 | This package is useful for safely testing failure scenarios in go services that can make use of `net/http` handlers/middleware. 18 | 19 | One common failure scenario that we cannot perfectly simulate is dropped requests. The `RejectInjector` will always return immediately to the user, but in many cases requests can be dropped without ever sending a response. The best way to simulate this scenario using the fault package is to chain a `SlowInjector` with a very long wait time in front of an eventual `RejectInjector`. 20 | 21 | ## Status 22 | 23 | This project is in a stable and supported state. There are no plans to introduce significant new features however we welcome and encourage any ideas and contributions from the community. Contributions should follow the guidelines in our [CONTRIBUTING.md](.github/CONTRIBUTING.md). 24 | 25 | ## Usage 26 | 27 | ```go 28 | // main.go 29 | package main 30 | 31 | import ( 32 | "net/http" 33 | "time" 34 | 35 | "github.com/lingrino/go-fault" 36 | ) 37 | 38 | var mainHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | http.Error(w, http.StatusText(http.StatusOK), http.StatusOK) 40 | }) 41 | 42 | func main() { 43 | slowInjector, _ := fault.NewSlowInjector(time.Second * 2) 44 | slowFault, _ := fault.NewFault(slowInjector, 45 | fault.WithEnabled(true), 46 | fault.WithParticipation(0.25), 47 | fault.WithPathBlocklist([]string{"/ping", "/health"}), 48 | ) 49 | 50 | // Add 2 seconds of latency to 25% of our requests 51 | handlerChain := slowFault.Handler(mainHandler) 52 | 53 | http.ListenAndServe("127.0.0.1:3000", handlerChain) 54 | } 55 | ``` 56 | 57 | ## Development 58 | 59 | This package uses standard go tooling for testing and development. The [go](https://golang.org/dl/) language is all you need to contribute. Tests use the popular [testify/assert](https://github.com/stretchr/testify/) which will be downloaded automatically the first time you run tests. GitHub Actions will also run a linter using [golangci-lint](https://github.com/golangci/golangci-lint) after you push. You can also download the linter and use `golangci-lint run` to run locally. 60 | 61 | ## Testing 62 | 63 | The fault package has extensive tests that are run in [GitHub Actions](https://github.com/lingrino/go-fault/actions?query=workflow%3AValidate) on every push. Code coverage is 100% and is published as an artifact on every Actions run. 64 | 65 | You can also run tests locally: 66 | 67 | ```shell 68 | $ go test -v -cover -race ./... 69 | [...] 70 | PASS 71 | coverage: 100.0% of statements 72 | ok github.com/lingrino/go-fault 0.575s 73 | ``` 74 | 75 | ## Benchmarks 76 | 77 | The fault package is safe to leave implemented even when you are not running a fault injection. While the fault is disabled there is negligible performance degradation compared to removing the package from the request path. While enabled there may be minor performance differences, but this will only be the case *while you are already injecting faults.* 78 | 79 | Benchmarks are provided to compare without faults, with faults disabled, and with faults enabled. Benchmarks are uploaded as artifacts in GitHub Actions and you can download them from any [Validate Workflow](https://github.com/lingrino/go-fault/actions?query=workflow%3AValidate). 80 | 81 | You can also run benchmarks locally (example output): 82 | 83 | ```shell 84 | $ go test -run=XXX -bench=. 85 | goos: darwin 86 | goarch: amd64 87 | pkg: github.com/lingrino/go-fault 88 | BenchmarkNoFault-8 684826 1734 ns/op 89 | BenchmarkFaultDisabled-8 675291 1771 ns/op 90 | BenchmarkFaultErrorZeroPercent-8 667903 1823 ns/op 91 | BenchmarkFaultError100Percent-8 663661 1833 ns/op 92 | PASS 93 | ok github.com/lingrino/go-fault 8.814s 94 | ``` 95 | 96 | ## Maintainers 97 | 98 | [@lingrino](https://github.com/lingrino) 99 | 100 | ### Contributors 101 | 102 | [@mrfaizal](https://github.com/mrfaizal) 103 | [@vroldanbet](https://github.com/vroldanbet) 104 | [@fatih](https://github.com/fatih) 105 | 106 | ## License 107 | 108 | This project is licensed under the [MIT License](LICENSE.md). 109 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package fault_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/lingrino/go-fault" 10 | ) 11 | 12 | // benchmarkRequest simulates a request with the provided Fault injected. 13 | func benchmarkRequest(b *testing.B, f *fault.Fault) *httptest.ResponseRecorder { 14 | b.Helper() 15 | 16 | // benchmarkHandler is the main handler that runs on our request. 17 | var benchmarkHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | http.Error(w, "OK", http.StatusOK) 19 | }) 20 | 21 | // If we instead use httptest.NewRequest here our benchmark times will approximately double. 22 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 23 | rr := httptest.NewRecorder() 24 | 25 | if f != nil { 26 | finalHandler := f.Handler(benchmarkHandler) 27 | finalHandler.ServeHTTP(rr, req) 28 | } else { 29 | benchmarkHandler.ServeHTTP(rr, req) 30 | } 31 | 32 | return rr 33 | } 34 | 35 | // runBenchmark benchmarks the provided Fault. 36 | func runBenchmark(b *testing.B, f *fault.Fault) { 37 | var rr *httptest.ResponseRecorder 38 | 39 | for n := 0; n < b.N; n++ { 40 | rr = benchmarkRequest(b, f) 41 | } 42 | 43 | _ = rr 44 | } 45 | 46 | // BenchmarkNoFault is our control using no Fault. 47 | func BenchmarkNoFault(b *testing.B) { 48 | runBenchmark(b, nil) 49 | } 50 | 51 | // BenchmarkFaultDisabled benchmarks a disabled Fault. 52 | func BenchmarkFaultDisabled(b *testing.B) { 53 | i, _ := fault.NewErrorInjector(http.StatusInternalServerError) 54 | f, _ := fault.NewFault(i, 55 | fault.WithEnabled(false), 56 | ) 57 | 58 | runBenchmark(b, f) 59 | } 60 | 61 | // BenchmarkFaultErrorZeroPercent benchmarks an enabled Fault with 0% participation. 62 | func BenchmarkFaultErrorZeroPercent(b *testing.B) { 63 | i, _ := fault.NewErrorInjector(http.StatusInternalServerError) 64 | f, _ := fault.NewFault(i, 65 | fault.WithEnabled(true), 66 | fault.WithParticipation(0.0), 67 | ) 68 | 69 | runBenchmark(b, f) 70 | } 71 | 72 | // BenchmarkFaultError100Percent benchmarks an enabled Fault with 100% participation. 73 | func BenchmarkFaultError100Percent(b *testing.B) { 74 | i, _ := fault.NewErrorInjector(http.StatusInternalServerError) 75 | f, _ := fault.NewFault(i, 76 | fault.WithEnabled(true), 77 | fault.WithParticipation(1.0), 78 | ) 79 | 80 | runBenchmark(b, f) 81 | } 82 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package fault provides standard http middleware for fault injection in go. 3 | 4 | # Basics 5 | 6 | Use the fault package to inject faults into the http request path of your service. Faults work by 7 | modifying and/or delaying your service's http responses. Place the Fault middleware high enough in 8 | the chain that it can act quickly, but after any other middlewares that should complete before fault 9 | injection (auth, redirects, etc...). 10 | 11 | The type and severity of injected faults is controlled by options passed to NewFault(Injector, 12 | Options). NewFault must be passed an Injector, which is an interface that holds the actual fault 13 | injection code in Injector.Handler. The Fault wraps Injector.Handler in another Fault.Handler that 14 | applies generic Fault logic (such as what % of requests to run the Injector on) to the Injector. 15 | 16 | Make sure you use the NewFault() and NewTypeInjector() constructors to create valid Faults and 17 | Injectors. 18 | 19 | # Injectors 20 | 21 | There are three main Injectors provided by the fault package: 22 | 23 | fault.RejectInjector 24 | fault.ErrorInjector 25 | fault.SlowInjector 26 | 27 | # RejectInjector 28 | 29 | Use fault.RejectInjector to immediately return an empty response. For example, a curl for a rejected 30 | response will produce: 31 | 32 | $ curl https://github.com 33 | curl: (52) Empty reply from server 34 | 35 | # ErrorInjector 36 | 37 | Use fault.ErrorInjector to immediately return a valid http status code of your choosing along with 38 | the standard HTTP response body for that code. For example, you can return a 200, 301, 418, 500, or 39 | any other valid status code to test how your clients respond to different statuses. Pass the 40 | WithStatusText() option to customize the response text. 41 | 42 | # SlowInjector 43 | 44 | Use fault.SlowInjector to wait a configured time.Duration before proceeding with the request. For 45 | example, you can use the SlowInjector to add a 10ms delay to your requests. 46 | 47 | # RandomInjector 48 | 49 | Use fault.RandomInjector to randomly choose one of the above faults to inject. Pass a list of 50 | Injector to fault.NewRandomInjector and when RandomInjector is evaluated it will randomly run one of 51 | the injectors that you passed. 52 | 53 | # Combining Faults 54 | 55 | It is easy to combine any of the Injectors into a chained action. There are two ways you might want 56 | to combine Injectors. 57 | 58 | First, you can create separate Faults for each Injector that are sequential but independent of each 59 | other. For example, you can chain Faults such that 1% of requests will return a 500 error and 60 | another 1% of requests will be rejected. 61 | 62 | Second, you might want to combine Faults such that 1% of requests will be slowed for 10ms and then 63 | rejected. You want these Faults to depend on each other. For this use the special ChainInjector, 64 | which consolidates any number of Injectors into a single Injector that runs each of the provided 65 | Injectors sequentially. When you add the ChainInjector to a Fault the entire chain will always 66 | execute together. 67 | 68 | # Allowing And Blocking Paths 69 | 70 | The NewFault() constructor has WithPathBlocklist() and WithPathAllowlist() options. Any path you 71 | include in the PathBlocklist will never have faults run against it. With PathAllowlist, if you 72 | provide a non-empty list then faults will not be run against any paths except those specified in 73 | PathAllowlist. The PathBlocklist take priority over the PathAllowlist, a path in both lists will 74 | never have a fault run against it. The paths that you include must match exactly the path in 75 | req.URL.Path, including leading and trailing slashes. 76 | 77 | Simmilarly, you may also use WithHeaderBlocklist() and WithHeaderAllowlist() to block or allow 78 | faults based on a map of header keys to values. These lists behave in the same way as the path 79 | allowlists and blocklists except that they operate on headers. Header equality is determined using 80 | http.Header.Get(key) which automatically canonicalizes your keys and does not support multi-value 81 | headers. Keep these limitations in mind when working with header allowlists and blocklists. 82 | 83 | Specifying very large lists of paths or headers may cause memory or performance issues. If you're 84 | running into these problems you should instead consider using your http router to enable the 85 | middleware on only a subset of your routes. 86 | 87 | # Custom Injectors 88 | 89 | The fault package provides an Injector interface and you can satisfy that interface to provide your 90 | own Injector. Use custom injectors to add additional logic to the package-provided injectors or to 91 | create your own completely new Injector that can still be managed by a Fault. 92 | 93 | # Reporter 94 | 95 | The package provides a Reporter interface that can be added to Faults and Injectors using the 96 | WithReporter option. A Reporter will receive events when the state of the Injector changes. For 97 | example, Reporter.Report(InjectorName, StateStarted) is run at the beginning of all Injectors. The 98 | Reporter is meant to be provided by the consumer of the package and integrate with services like 99 | stats and logging. The default Reporter throws away all events. 100 | 101 | # Random Seeds 102 | 103 | By default all randomness is seeded with defaultRandSeed(1), the same default as math/rand. This 104 | helps you reproduce any errors you see when running an Injector. If you prefer, you can also 105 | customize the seed passing WithRandSeed() to NewFault and NewRandomInjector. 106 | 107 | # Custom Injector Functions 108 | 109 | Some Injectors support customizing the functions they use to run their injections. You can take 110 | advantage of these options to add your own logic to an existing Injector instead of creating your 111 | own. For example, modify the SlowInjector function to slow in a rancom distribution instead of for a 112 | fixed duration. Be careful when you use these options that your return values fall within the same 113 | range of values expected by the default functions to avoid panics or other undesirable begavior. 114 | 115 | Customize the function a Fault uses to determine participation (default: rand.Float32) by passing 116 | WithRandFloat32Func() to NewFault(). 117 | 118 | Customize the function a RandomInjector uses to choose which injector to run (default: rand.Intn) by 119 | passing WithRandIntFunc() to NewRandomInjector(). 120 | 121 | Customize the function a SlowInjector uses to wait (default: time.Sleep) by passing WithSlowFunc() 122 | to NewSlowInjector(). 123 | 124 | # Configuration 125 | 126 | Configuration for the fault package is done through options passed to NewFault and NewInjector. Once 127 | a Fault is created its enabled state and participation percentage can be updated with SetEnabled() 128 | and SetParticipation(). There is no other way to manage configuration for the package. It is up to 129 | the user of the fault package to manage how the options are generated. Common options are feature 130 | flags, environment variables, or code changes in deploys. 131 | */ 132 | package fault 133 | -------------------------------------------------------------------------------- /example_package_test.go: -------------------------------------------------------------------------------- 1 | package fault_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "time" 9 | 10 | "github.com/lingrino/go-fault" 11 | ) 12 | 13 | // Example is a package-level documentation example. 14 | func Example() { 15 | // Wait one millisecond then continue 16 | si, _ := fault.NewSlowInjector(time.Millisecond) 17 | 18 | // Return a 500 19 | ei, _ := fault.NewErrorInjector(http.StatusInternalServerError) 20 | 21 | // Chain slow and error injectors together 22 | ci, _ := fault.NewChainInjector([]fault.Injector{si, ei}) 23 | 24 | // Run our fault injection 100% of the time 25 | f, _ := fault.NewFault(ci, 26 | fault.WithEnabled(true), 27 | fault.WithParticipation(1.0), 28 | fault.WithPathBlocklist([]string{"/ping", "/health"}), 29 | ) 30 | 31 | // mainHandler responds 200/OK 32 | var mainHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | http.Error(w, "OK", http.StatusOK) 34 | }) 35 | 36 | // Insert our middleware before the mainHandler 37 | handlerChain := f.Handler((mainHandler)) 38 | 39 | // Create dummy request and response records 40 | req, _ := http.NewRequestWithContext(context.Background(), "GET", "/", nil) 41 | rr := httptest.NewRecorder() 42 | 43 | // Run our request 44 | handlerChain.ServeHTTP(rr, req) 45 | 46 | // Verify the correct response 47 | fmt.Println(rr.Code) 48 | fmt.Println(rr.Body.String()) 49 | // Output: 500 50 | // Internal Server Error 51 | } 52 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package fault_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/lingrino/go-fault" 9 | ) 10 | 11 | // ExampleNewFault shows how to create a new Fault. 12 | func ExampleNewFault() { 13 | ei, err := fault.NewErrorInjector(http.StatusInternalServerError) 14 | fmt.Print(err) 15 | 16 | _, err = fault.NewFault(ei, 17 | fault.WithEnabled(true), 18 | fault.WithParticipation(0.25), 19 | ) 20 | 21 | fmt.Print(err) 22 | // Output: 23 | } 24 | 25 | // ExampleNewFault_blocklist shows how to create a new Fault with a path/header blocklist. 26 | func ExampleNewFault_blocklist() { 27 | ei, err := fault.NewErrorInjector(http.StatusInternalServerError) 28 | fmt.Print(err) 29 | 30 | _, err = fault.NewFault(ei, 31 | fault.WithEnabled(true), 32 | fault.WithParticipation(0.25), 33 | fault.WithPathBlocklist([]string{"/ping", "/health"}), 34 | fault.WithHeaderBlocklist(map[string]string{"block": "this header"}), 35 | ) 36 | 37 | fmt.Print(err) 38 | // Output: 39 | } 40 | 41 | // ExampleNewFault_allowlist shows how to create a new Fault with a path/header allowlist. 42 | func ExampleNewFault_allowlist() { 43 | ei, err := fault.NewErrorInjector(http.StatusInternalServerError) 44 | fmt.Print(err) 45 | 46 | _, err = fault.NewFault(ei, 47 | fault.WithEnabled(true), 48 | fault.WithParticipation(0.25), 49 | fault.WithPathAllowlist([]string{"/injecthere", "/andhere"}), 50 | fault.WithHeaderAllowlist(map[string]string{"allow": "this header"}), 51 | ) 52 | 53 | fmt.Print(err) 54 | // Output: 55 | } 56 | 57 | // ExampleNewChainInjector shows how to create a new ChainInjector. 58 | func ExampleNewChainInjector() { 59 | si, err := fault.NewSlowInjector(time.Minute) 60 | fmt.Print(err) 61 | ri, err := fault.NewRejectInjector() 62 | fmt.Print(err) 63 | 64 | _, err = fault.NewChainInjector([]fault.Injector{si, ri}) 65 | 66 | fmt.Print(err) 67 | // Output: 68 | } 69 | 70 | // ExampleNewChainInjector shows how to create a new RandomInjector. 71 | func ExampleNewRandomInjector() { 72 | si, err := fault.NewSlowInjector(time.Minute) 73 | fmt.Print(err) 74 | ri, err := fault.NewRejectInjector() 75 | fmt.Print(err) 76 | 77 | _, err = fault.NewRandomInjector([]fault.Injector{si, ri}) 78 | 79 | fmt.Print(err) 80 | // Output: 81 | } 82 | 83 | // ExampleNewRejectInjector shows how to create a new RejectInjector. 84 | func ExampleNewRejectInjector() { 85 | _, err := fault.NewRejectInjector() 86 | 87 | fmt.Print(err) 88 | // Output: 89 | } 90 | 91 | // ExampleNewErrorInjector shows how to create a new ErrorInjector. 92 | func ExampleNewErrorInjector() { 93 | _, err := fault.NewErrorInjector(http.StatusInternalServerError) 94 | 95 | fmt.Print(err) 96 | // Output: 97 | } 98 | 99 | // ExampleNewSlowInjector shows how to create a new SlowInjector. 100 | func ExampleNewSlowInjector() { 101 | _, err := fault.NewSlowInjector(time.Second * 10) 102 | 103 | fmt.Print(err) 104 | // Output: 105 | } 106 | -------------------------------------------------------------------------------- /fault.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "errors" 5 | "math/rand" 6 | "net/http" 7 | "sync" 8 | ) 9 | 10 | const ( 11 | // defaultRandSeed is used when a random seed is not set explicitly. 12 | defaultRandSeed = 1 13 | ) 14 | 15 | var ( 16 | // ErrNilInjector when a nil Injector is passed. 17 | ErrNilInjector = errors.New("injector cannot be nil") 18 | // ErrInvalidPercent when a percent is outside of [0.0,1.0). 19 | ErrInvalidPercent = errors.New("percent must be 0.0 <= percent <= 1.0") 20 | ) 21 | 22 | // Fault combines an Injector with options on when to use that Injector. 23 | type Fault struct { 24 | // enabled determines if the fault should evaluate. 25 | enabled bool 26 | 27 | // injector is the Injector that will be injected. 28 | injector Injector 29 | 30 | // participation is the percent of requests that run the injector. 0.0 <= p <= 1.0. 31 | participation float32 32 | 33 | // pathBlocklist is a map of paths that the Injector will never run against. 34 | pathBlocklist map[string]bool 35 | 36 | // pathAllowlist, if set, is a map of the only paths that the Injector will run against. 37 | pathAllowlist map[string]bool 38 | 39 | // headerBlocklist is a map of headers that the Injector will never run against. 40 | headerBlocklist map[string]string 41 | 42 | // headerAllowlist, if set, is a map of the only headers the Injector will run against. 43 | headerAllowlist map[string]string 44 | 45 | // randSeed is a number to seed rand with. 46 | randSeed int64 47 | 48 | // rand is our random number source. 49 | rand *rand.Rand 50 | 51 | // randF is a function that returns a float32 [0.0,1.0). 52 | randF func() float32 53 | 54 | // randMtx protects Fault.rand, which is not thread safe. 55 | randMtx sync.Mutex 56 | } 57 | 58 | // Option configures a Fault. 59 | type Option interface { 60 | applyFault(f *Fault) error 61 | } 62 | 63 | type enabledOption bool 64 | 65 | func (o enabledOption) applyFault(f *Fault) error { 66 | f.enabled = bool(o) 67 | return nil 68 | } 69 | 70 | // WithEnabled sets if the Fault should evaluate. 71 | func WithEnabled(e bool) Option { 72 | return enabledOption(e) 73 | } 74 | 75 | type participationOption float32 76 | 77 | func (o participationOption) applyFault(f *Fault) error { 78 | if o < 0.0 || o > 1.0 { 79 | return ErrInvalidPercent 80 | } 81 | f.participation = float32(o) 82 | return nil 83 | } 84 | 85 | // WithParticipation sets the percent of requests that run the Injector. 0.0 <= p <= 1.0. 86 | func WithParticipation(p float32) Option { 87 | return participationOption(p) 88 | } 89 | 90 | type pathBlocklistOption []string 91 | 92 | func (o pathBlocklistOption) applyFault(f *Fault) error { 93 | blocklist := make(map[string]bool, len(o)) 94 | for _, path := range o { 95 | blocklist[path] = true 96 | } 97 | f.pathBlocklist = blocklist 98 | return nil 99 | } 100 | 101 | // WithPathBlocklist is a list of paths that the Injector will not run against. 102 | func WithPathBlocklist(blocklist []string) Option { 103 | return pathBlocklistOption(blocklist) 104 | } 105 | 106 | type pathAllowlistOption []string 107 | 108 | func (o pathAllowlistOption) applyFault(f *Fault) error { 109 | allowlist := make(map[string]bool, len(o)) 110 | for _, path := range o { 111 | allowlist[path] = true 112 | } 113 | f.pathAllowlist = allowlist 114 | return nil 115 | } 116 | 117 | // WithPathAllowlist is, if set, a list of the only paths that the Injector will run against. 118 | func WithPathAllowlist(allowlist []string) Option { 119 | return pathAllowlistOption(allowlist) 120 | } 121 | 122 | type headerBlocklistOption map[string]string 123 | 124 | func (o headerBlocklistOption) applyFault(f *Fault) error { 125 | blocklist := make(map[string]string, len(o)) 126 | for key, val := range o { 127 | blocklist[key] = val 128 | } 129 | f.headerBlocklist = blocklist 130 | return nil 131 | } 132 | 133 | // WithHeaderBlocklist is a map of header keys to values that the Injector will not run against. 134 | func WithHeaderBlocklist(blocklist map[string]string) Option { 135 | return headerBlocklistOption(blocklist) 136 | } 137 | 138 | type headerAllowlistOption map[string]string 139 | 140 | func (o headerAllowlistOption) applyFault(f *Fault) error { 141 | allowlist := make(map[string]string, len(o)) 142 | for key, val := range o { 143 | allowlist[key] = val 144 | } 145 | f.headerAllowlist = allowlist 146 | return nil 147 | } 148 | 149 | // WithHeaderAllowlist is, if set, a map of header keys to values of the only headers that the 150 | // Injector will run against. 151 | func WithHeaderAllowlist(allowlist map[string]string) Option { 152 | return headerAllowlistOption(allowlist) 153 | } 154 | 155 | // RandSeedOption configures things that can set a random seed. 156 | type RandSeedOption interface { 157 | Option 158 | RandomInjectorOption 159 | } 160 | 161 | type randSeedOption int64 162 | 163 | func (o randSeedOption) applyFault(f *Fault) error { 164 | f.randSeed = int64(o) 165 | return nil 166 | } 167 | 168 | // WithRandSeed sets the rand.Rand seed for this struct. 169 | func WithRandSeed(s int64) RandSeedOption { 170 | return randSeedOption(s) 171 | } 172 | 173 | type randFloat32FuncOption func() float32 174 | 175 | func (o randFloat32FuncOption) applyFault(f *Fault) error { 176 | f.randF = o 177 | return nil 178 | } 179 | 180 | // WithRandFloat32Func sets the function that will be used to randomly get our float value. Default 181 | // rand.Float32. Always returns a float32 between [0.0,1.0) to avoid errors. 182 | func WithRandFloat32Func(f func() float32) Option { 183 | return randFloat32FuncOption(f) 184 | } 185 | 186 | // NewFault sets/validates the Injector and Options and returns a usable Fault. 187 | func NewFault(i Injector, opts ...Option) (*Fault, error) { 188 | if i == nil { 189 | return nil, ErrNilInjector 190 | } 191 | 192 | // set defaults 193 | f := &Fault{ 194 | injector: i, 195 | randSeed: defaultRandSeed, 196 | randF: nil, 197 | } 198 | 199 | // apply options 200 | for _, opt := range opts { 201 | err := opt.applyFault(f) 202 | if err != nil { 203 | return nil, err 204 | } 205 | } 206 | 207 | // set seeded rand source and function 208 | f.rand = rand.New(rand.NewSource(f.randSeed)) 209 | if f.randF == nil { 210 | f.randF = f.rand.Float32 211 | } 212 | 213 | return f, nil 214 | } 215 | 216 | // Handler determines if the Injector should execute and runs it if so. 217 | func (f *Fault) Handler(next http.Handler) http.Handler { 218 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 219 | // By default faults do not evaluate. Here we go through conditions where faults 220 | // will evaluate, if everything is configured correctly. 221 | var shouldEvaluate bool 222 | 223 | shouldEvaluate = f.enabled 224 | 225 | shouldEvaluate = shouldEvaluate && f.checkAllowBlockLists(shouldEvaluate, r) 226 | 227 | // false if not selected for participation 228 | shouldEvaluate = shouldEvaluate && f.participate() 229 | 230 | // run the injector or pass 231 | if shouldEvaluate { 232 | f.injector.Handler(next).ServeHTTP(w, r) 233 | } else { 234 | next.ServeHTTP(w, r) 235 | } 236 | }) 237 | } 238 | 239 | // SetEnabled updates the enabled state of the Fault. 240 | func (f *Fault) SetEnabled(o enabledOption) error { 241 | return o.applyFault(f) 242 | } 243 | 244 | // SetParticipation updates the participation percentage of the Fault. 245 | func (f *Fault) SetParticipation(o participationOption) error { 246 | return o.applyFault(f) 247 | } 248 | 249 | // checkAllowBlockLists checks the request against the provided allowlists and blocklists, returning 250 | // true if the request may proceed and false otherwise. 251 | func (f *Fault) checkAllowBlockLists(shouldEvaluate bool, r *http.Request) bool { 252 | // false if path is in pathBlocklist 253 | shouldEvaluate = shouldEvaluate && !f.pathBlocklist[r.URL.Path] 254 | 255 | // false if pathAllowlist exists and path is not in it 256 | if len(f.pathAllowlist) > 0 { 257 | shouldEvaluate = shouldEvaluate && f.pathAllowlist[r.URL.Path] 258 | } 259 | 260 | // false if any headers match headerBlocklist 261 | for key, val := range f.headerBlocklist { 262 | shouldEvaluate = shouldEvaluate && !(r.Header.Get(key) == val) 263 | } 264 | 265 | // false if headerAllowlist exists and headers are not in it 266 | if len(f.headerAllowlist) > 0 { 267 | for key, val := range f.headerAllowlist { 268 | shouldEvaluate = shouldEvaluate && (r.Header.Get(key) == val) 269 | } 270 | } 271 | 272 | return shouldEvaluate 273 | } 274 | 275 | // participate randomly decides (returns true) if the Injector should run based on f.participation. 276 | // Numbers outside of [0.0,1.0] will always return false. 277 | func (f *Fault) participate() bool { 278 | f.randMtx.Lock() 279 | rn := f.randF() 280 | f.randMtx.Unlock() 281 | 282 | if rn < f.participation && f.participation <= 1.0 { 283 | return true 284 | } 285 | 286 | return false 287 | } 288 | -------------------------------------------------------------------------------- /fault_test.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net/http" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // TestNewFault tests NewFault. 14 | func TestNewFault(t *testing.T) { 15 | t.Parallel() 16 | 17 | tests := []struct { 18 | name string 19 | giveInjector Injector 20 | giveOptions []Option 21 | wantFault *Fault 22 | wantErr error 23 | }{ 24 | { 25 | name: "all options", 26 | giveInjector: newTestInjectorNoop(), 27 | giveOptions: []Option{ 28 | WithEnabled(true), 29 | WithParticipation(1.0), 30 | WithPathBlocklist([]string{"/donotinject"}), 31 | WithPathAllowlist([]string{"/onlyinject"}), 32 | WithHeaderBlocklist(map[string]string{"block": "yes"}), 33 | WithHeaderAllowlist(map[string]string{"allow": "yes"}), 34 | WithRandSeed(100), 35 | WithRandFloat32Func(func() float32 { return 0.0 }), 36 | }, 37 | wantFault: &Fault{ 38 | enabled: true, 39 | injector: newTestInjectorNoop(), 40 | participation: 1.0, 41 | pathBlocklist: map[string]bool{ 42 | "/donotinject": true, 43 | }, 44 | pathAllowlist: map[string]bool{ 45 | "/onlyinject": true, 46 | }, 47 | headerBlocklist: map[string]string{ 48 | "block": "yes", 49 | }, 50 | headerAllowlist: map[string]string{ 51 | "allow": "yes", 52 | }, 53 | randSeed: 100, 54 | rand: rand.New(rand.NewSource(100)), 55 | randF: func() float32 { return 0.0 }, 56 | }, 57 | wantErr: nil, 58 | }, 59 | { 60 | name: "nil injector", 61 | giveInjector: nil, 62 | giveOptions: nil, 63 | wantFault: nil, 64 | wantErr: ErrNilInjector, 65 | }, 66 | { 67 | name: "invalid percent", 68 | giveInjector: newTestInjectorNoop(), 69 | giveOptions: []Option{ 70 | WithParticipation(100.0), 71 | }, 72 | wantFault: nil, 73 | wantErr: ErrInvalidPercent, 74 | }, 75 | { 76 | name: "option error", 77 | giveInjector: newTestInjectorNoop(), 78 | giveOptions: []Option{ 79 | withError(), 80 | }, 81 | wantFault: nil, 82 | wantErr: errErrorOption, 83 | }, 84 | { 85 | name: "empty options", 86 | giveInjector: newTestInjectorNoop(), 87 | giveOptions: []Option{}, 88 | wantFault: &Fault{ 89 | enabled: false, 90 | injector: newTestInjectorNoop(), 91 | participation: 0.0, 92 | pathBlocklist: nil, 93 | pathAllowlist: nil, 94 | randSeed: defaultRandSeed, 95 | rand: rand.New(rand.NewSource(defaultRandSeed)), 96 | randF: rand.New(rand.NewSource(defaultRandSeed)).Float32, 97 | }, 98 | wantErr: nil, 99 | }, 100 | } 101 | 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | t.Parallel() 105 | 106 | f, err := NewFault(tt.giveInjector, tt.giveOptions...) 107 | 108 | // Function equality cannot be determined so set to nil before comparing 109 | if tt.wantFault != nil { 110 | f.randF = nil 111 | tt.wantFault.randF = nil 112 | } 113 | 114 | assert.Equal(t, tt.wantErr, err) 115 | assert.Equal(t, tt.wantFault, f) 116 | }) 117 | } 118 | } 119 | 120 | // TestFaultHandler tests Fault.Handler. 121 | func TestFaultHandler(t *testing.T) { 122 | t.Parallel() 123 | 124 | tests := []struct { 125 | name string 126 | giveInjector Injector 127 | giveOptions []Option 128 | wantCode int 129 | wantBody string 130 | }{ 131 | { 132 | name: "not enabled", 133 | giveInjector: newTestInjectorNoop(), 134 | giveOptions: []Option{ 135 | WithEnabled(false), 136 | WithParticipation(1.0), 137 | }, 138 | wantCode: testHandlerCode, 139 | wantBody: testHandlerBody, 140 | }, 141 | { 142 | name: "zero percent", 143 | giveInjector: newTestInjectorNoop(), 144 | giveOptions: []Option{ 145 | WithEnabled(true), 146 | WithParticipation(0.0), 147 | }, 148 | wantCode: testHandlerCode, 149 | wantBody: testHandlerBody, 150 | }, 151 | { 152 | name: "100 percent 500s", 153 | giveInjector: newTestInjector500s(), 154 | giveOptions: []Option{ 155 | WithEnabled(true), 156 | WithParticipation(1.0), 157 | }, 158 | wantCode: http.StatusInternalServerError, 159 | wantBody: http.StatusText(http.StatusInternalServerError), 160 | }, 161 | { 162 | name: "0 percent 500s custom function", 163 | giveInjector: newTestInjector500s(), 164 | giveOptions: []Option{ 165 | WithEnabled(true), 166 | WithParticipation(1.0), 167 | WithRandFloat32Func(func() float32 { return 1.0 }), 168 | }, 169 | wantCode: testHandlerCode, 170 | wantBody: testHandlerBody, 171 | }, 172 | { 173 | name: "100 percent 500s with blocklist root", 174 | giveInjector: newTestInjector500s(), 175 | giveOptions: []Option{ 176 | WithEnabled(true), 177 | WithParticipation(1.0), 178 | WithPathBlocklist([]string{"/"}), 179 | }, 180 | wantCode: testHandlerCode, 181 | wantBody: testHandlerBody, 182 | }, 183 | { 184 | name: "100 percent 500s with allowlist root", 185 | giveInjector: newTestInjector500s(), 186 | giveOptions: []Option{ 187 | WithEnabled(true), 188 | WithParticipation(1.0), 189 | WithPathAllowlist([]string{"/"}), 190 | }, 191 | wantCode: http.StatusInternalServerError, 192 | wantBody: http.StatusText(http.StatusInternalServerError), 193 | }, 194 | { 195 | name: "100 percent 500s with allowlist other", 196 | giveInjector: newTestInjector500s(), 197 | giveOptions: []Option{ 198 | WithEnabled(true), 199 | WithParticipation(1.0), 200 | WithPathAllowlist([]string{"/onlyinject"}), 201 | }, 202 | wantCode: testHandlerCode, 203 | wantBody: testHandlerBody, 204 | }, 205 | { 206 | name: "100 percent 500s with allowlist and blocklist root", 207 | giveInjector: newTestInjector500s(), 208 | giveOptions: []Option{ 209 | WithEnabled(true), 210 | WithParticipation(1.0), 211 | WithPathBlocklist([]string{"/"}), 212 | WithPathAllowlist([]string{"/"}), 213 | }, 214 | wantCode: testHandlerCode, 215 | wantBody: testHandlerBody, 216 | }, 217 | { 218 | name: "100 percent 500s with header block", 219 | giveInjector: newTestInjector500s(), 220 | giveOptions: []Option{ 221 | WithEnabled(true), 222 | WithParticipation(1.0), 223 | WithHeaderBlocklist(map[string]string{testHeaderKey: testHeaderVal}), 224 | }, 225 | wantCode: testHandlerCode, 226 | wantBody: testHandlerBody, 227 | }, 228 | { 229 | name: "100 percent 500s with header allow", 230 | giveInjector: newTestInjector500s(), 231 | giveOptions: []Option{ 232 | WithEnabled(true), 233 | WithParticipation(1.0), 234 | WithHeaderAllowlist(map[string]string{testHeaderKey: testHeaderVal}), 235 | }, 236 | wantCode: http.StatusInternalServerError, 237 | wantBody: http.StatusText(http.StatusInternalServerError), 238 | }, 239 | { 240 | name: "100 percent 500s with header allow other", 241 | giveInjector: newTestInjector500s(), 242 | giveOptions: []Option{ 243 | WithEnabled(true), 244 | WithParticipation(1.0), 245 | WithHeaderAllowlist(map[string]string{"header": "not in test request"}), 246 | }, 247 | wantCode: testHandlerCode, 248 | wantBody: testHandlerBody, 249 | }, 250 | { 251 | name: "100 percent 500s with header allowlist and blocklist", 252 | giveInjector: newTestInjector500s(), 253 | giveOptions: []Option{ 254 | WithEnabled(true), 255 | WithParticipation(1.0), 256 | WithHeaderBlocklist(map[string]string{testHeaderKey: testHeaderVal}), 257 | WithHeaderAllowlist(map[string]string{testHeaderKey: testHeaderVal}), 258 | }, 259 | wantCode: testHandlerCode, 260 | wantBody: testHandlerBody, 261 | }, 262 | { 263 | name: "disabled with with path/header allowlists", 264 | giveInjector: newTestInjector500s(), 265 | giveOptions: []Option{ 266 | WithEnabled(false), 267 | WithParticipation(1.0), 268 | WithPathAllowlist([]string{"/"}), 269 | WithHeaderAllowlist(map[string]string{testHeaderKey: testHeaderVal}), 270 | }, 271 | wantCode: testHandlerCode, 272 | wantBody: testHandlerBody, 273 | }, 274 | { 275 | name: "100 percent inject nothing", 276 | giveInjector: newTestInjectorNoop(), 277 | giveOptions: []Option{ 278 | WithEnabled(true), 279 | WithParticipation(1.0), 280 | }, 281 | wantCode: testHandlerCode, 282 | wantBody: testHandlerBody, 283 | }, 284 | } 285 | 286 | for _, tt := range tests { 287 | t.Run(tt.name, func(t *testing.T) { 288 | t.Parallel() 289 | 290 | f, err := NewFault(tt.giveInjector, tt.giveOptions...) 291 | assert.NoError(t, err) 292 | 293 | rr := testRequest(t, f) 294 | 295 | assert.Equal(t, tt.wantCode, rr.Code) 296 | assert.Equal(t, tt.wantBody, strings.TrimSpace(rr.Body.String())) 297 | }) 298 | } 299 | } 300 | 301 | // TestFaultSetEnabled tests Fault.SetEnabled(). 302 | func TestFaultSetEnabled(t *testing.T) { 303 | t.Parallel() 304 | 305 | f, err := NewFault(newTestInjector500s(), 306 | WithEnabled(true), 307 | WithParticipation(1.0), 308 | ) 309 | assert.NoError(t, err) 310 | 311 | rr := testRequest(t, f) 312 | assert.Equal(t, http.StatusInternalServerError, rr.Code) 313 | assert.Equal(t, http.StatusText(http.StatusInternalServerError), strings.TrimSpace(rr.Body.String())) 314 | 315 | err = f.SetEnabled(false) 316 | assert.NoError(t, err) 317 | 318 | rr = testRequest(t, f) 319 | assert.Equal(t, testHandlerCode, rr.Code) 320 | assert.Equal(t, testHandlerBody, strings.TrimSpace(rr.Body.String())) 321 | } 322 | 323 | // TestFaultSetParticipation tests Fault.SetParticipation(). 324 | func TestFaultSetParticipation(t *testing.T) { 325 | t.Parallel() 326 | 327 | f, err := NewFault(newTestInjector500s(), 328 | WithEnabled(true), 329 | WithParticipation(1.0), 330 | ) 331 | assert.NoError(t, err) 332 | 333 | rr := testRequest(t, f) 334 | assert.Equal(t, http.StatusInternalServerError, rr.Code) 335 | assert.Equal(t, http.StatusText(http.StatusInternalServerError), strings.TrimSpace(rr.Body.String())) 336 | 337 | err = f.SetParticipation(0.0) 338 | assert.NoError(t, err) 339 | 340 | rr = testRequest(t, f) 341 | assert.Equal(t, testHandlerCode, rr.Code) 342 | assert.Equal(t, testHandlerBody, strings.TrimSpace(rr.Body.String())) 343 | } 344 | 345 | // TestFaultPercentDo tests the internal Fault.participate(). 346 | func TestFaultPercentDo(t *testing.T) { 347 | t.Parallel() 348 | 349 | tests := []struct { 350 | givePercent float32 351 | wantPercent float32 352 | wantRange float32 353 | }{ 354 | {}, 355 | {0.0, 0.0, 0.0}, 356 | {0.0001, 0.0001, 0.005}, 357 | {0.3298, 0.3298, 0.005}, 358 | {0.75, 0.75, 0.005}, 359 | {1.0, 1.0, 0.0}, 360 | } 361 | 362 | for _, tt := range tests { 363 | t.Run(fmt.Sprintf("%g", tt.givePercent), func(t *testing.T) { 364 | t.Parallel() 365 | 366 | f, err := NewFault(newTestInjectorNoop(), 367 | WithParticipation(tt.givePercent), 368 | ) 369 | assert.NoError(t, err) 370 | 371 | var trueC, totalC float32 372 | for totalC <= 100000 { 373 | result := f.participate() 374 | if result { 375 | trueC++ 376 | } 377 | totalC++ 378 | } 379 | 380 | minP := tt.wantPercent - tt.wantRange 381 | per := trueC / totalC 382 | maxP := tt.wantPercent + tt.wantRange 383 | 384 | assert.GreaterOrEqual(t, per, minP) 385 | assert.LessOrEqual(t, per, maxP) 386 | }) 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lingrino/go-fault 2 | 3 | go 1.23.4 4 | 5 | require github.com/stretchr/testify v1.10.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 6 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | const ( 12 | // testHandlerCode and testHandlerBody are the default status code and status text expected 13 | // from a handler that has not been changed by an Injector. Don't use http.StatusOK because 14 | // some http methods default to http.StatusOK and then there's no difference between our 15 | // test response and other standard responses. 16 | testHandlerCode = http.StatusAccepted 17 | testHandlerBody = "Accepted" 18 | testHeaderKey = "testing header key" 19 | testHeaderVal = "testing header val" 20 | ) 21 | 22 | // testRequest simulates a request to testHandler with a Fault injected. 23 | func testRequest(t *testing.T, f *Fault) *httptest.ResponseRecorder { 24 | t.Helper() 25 | 26 | var testHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 27 | http.Error(w, testHandlerBody, testHandlerCode) 28 | }) 29 | 30 | req := httptest.NewRequest("GET", "/", nil) 31 | req.Header.Add(testHeaderKey, testHeaderVal) 32 | 33 | rr := httptest.NewRecorder() 34 | 35 | if f != nil { 36 | finalHandler := f.Handler(testHandler) 37 | finalHandler.ServeHTTP(rr, req) 38 | } else { 39 | testHandler.ServeHTTP(rr, req) 40 | } 41 | 42 | return rr 43 | } 44 | 45 | // testRequestExpectPanic runs testRequest and catches/passes if panic(http.ErrAbortHandler). 46 | func testRequestExpectPanic(t *testing.T, f *Fault) *httptest.ResponseRecorder { 47 | t.Helper() 48 | 49 | defer func() { 50 | if r := recover(); r != nil { 51 | if !errors.Is(r.(error), http.ErrAbortHandler) { 52 | t.Fatal(r) 53 | } 54 | } 55 | }() 56 | 57 | rr := testRequest(t, f) 58 | 59 | return rr 60 | } 61 | 62 | // testInjectorNoop is an injector that does nothing. 63 | type testInjectorNoop struct{} 64 | 65 | // newTestInjectorNoop creates a new testInjectorNoop. 66 | func newTestInjectorNoop() *testInjectorNoop { 67 | return &testInjectorNoop{} 68 | } 69 | 70 | // Handler does nothing. 71 | func (i *testInjectorNoop) Handler(next http.Handler) http.Handler { return next } 72 | 73 | // testInjectorStop is an injector that stops a request. 74 | type testInjectorStop struct{} 75 | 76 | // newTestInjectorStop creates a new testInjectorStop. 77 | func newTestInjectorStop() *testInjectorStop { 78 | return &testInjectorStop{} 79 | } 80 | 81 | // Handler returns a Handler that stops the request. 82 | func (i *testInjectorStop) Handler(next http.Handler) http.Handler { 83 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 84 | } 85 | 86 | // testInjector500s is an injector that returns 500s. 87 | type testInjector500s struct{} 88 | 89 | // newTestInjector500 creates a new testInjector500s. 90 | func newTestInjector500s() *testInjector500s { 91 | return &testInjector500s{} 92 | } 93 | 94 | // Handler returns a 500. 95 | func (i *testInjector500s) Handler(next http.Handler) http.Handler { 96 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 97 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 98 | }) 99 | } 100 | 101 | // testInjectorOneOK is an injector that writes "one" and statusOK. 102 | type testInjectorOneOK struct{} 103 | 104 | // newTestInjectorOneOK creates a new testInjectorOneOK. 105 | func newTestInjectorOneOK() *testInjectorOneOK { 106 | return &testInjectorOneOK{} 107 | } 108 | 109 | // Handler writes statusOK and "one" and continues. 110 | func (i *testInjectorOneOK) Handler(next http.Handler) http.Handler { 111 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 112 | w.WriteHeader(http.StatusOK) 113 | fmt.Fprint(w, "one") 114 | next.ServeHTTP(w, r) 115 | }) 116 | } 117 | 118 | // testInjectorTwoTeapot is an injector that writes "two" and statusTeapot. 119 | type testInjectorTwoTeapot struct{} 120 | 121 | // newTestInjectorTwoTeapot creates a new testInjectorTwoTeapot. 122 | func newTestInjectorTwoTeapot() *testInjectorTwoTeapot { 123 | return &testInjectorTwoTeapot{} 124 | } 125 | 126 | // Handler writes StatusTeapot and "two" and continues. 127 | func (i *testInjectorTwoTeapot) Handler(next http.Handler) http.Handler { 128 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 129 | w.WriteHeader(http.StatusTeapot) 130 | fmt.Fprint(w, "two") 131 | next.ServeHTTP(w, r) 132 | }) 133 | } 134 | 135 | var ( 136 | errErrorOption = errors.New("intentional error for tests") 137 | ) 138 | 139 | // errorOption returns errErrorOption. 140 | type errorOption interface { 141 | Option 142 | ChainInjectorOption 143 | RandomInjectorOption 144 | RejectInjectorOption 145 | ErrorInjectorOption 146 | SlowInjectorOption 147 | } 148 | 149 | type errorOptionBool bool 150 | 151 | func (o errorOptionBool) applyFault(f *Fault) error { 152 | return errErrorOption 153 | } 154 | 155 | func (o errorOptionBool) applyChainInjector(f *ChainInjector) error { 156 | return errErrorOption 157 | } 158 | 159 | func (o errorOptionBool) applyRandomInjector(f *RandomInjector) error { 160 | return errErrorOption 161 | } 162 | 163 | func (o errorOptionBool) applyRejectInjector(f *RejectInjector) error { 164 | return errErrorOption 165 | } 166 | 167 | func (o errorOptionBool) applyErrorInjector(f *ErrorInjector) error { 168 | return errErrorOption 169 | } 170 | 171 | func (o errorOptionBool) applySlowInjector(f *SlowInjector) error { 172 | return errErrorOption 173 | } 174 | 175 | func withError() errorOption { 176 | return errorOptionBool(true) 177 | } 178 | 179 | // testReporter is a reporter that does nothing. 180 | type testReporter struct{} 181 | 182 | // NewTestReporter returns a new testReporter. 183 | func newTestReporter() *testReporter { 184 | return &testReporter{} 185 | } 186 | 187 | // Report does nothing. 188 | func (r *testReporter) Report(name string, state InjectorState) {} 189 | -------------------------------------------------------------------------------- /injector.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // InjectorState represents the states an injector can be in. 8 | type InjectorState int 9 | 10 | const ( 11 | // StateStarted when an Injector has started. 12 | StateStarted InjectorState = iota + 1 13 | // StateFinished when an Injector has finished. 14 | StateFinished 15 | // StateSkipped when an Injector is skipped. 16 | StateSkipped 17 | ) 18 | 19 | // Injector are added to Faults and run as middleware in a request. 20 | type Injector interface { 21 | Handler(next http.Handler) http.Handler 22 | } 23 | -------------------------------------------------------------------------------- /injector_chain.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import "net/http" 4 | 5 | // ChainInjector combines many Injectors into a single Injector that runs them in order. 6 | type ChainInjector struct { 7 | middlewares []func(next http.Handler) http.Handler 8 | } 9 | 10 | // ChainInjectorOption configures a ChainInjector. 11 | type ChainInjectorOption interface { 12 | applyChainInjector(i *ChainInjector) error 13 | } 14 | 15 | // NewChainInjector combines many Injectors into a single Injector that runs them in order. 16 | func NewChainInjector(is []Injector, opts ...ChainInjectorOption) (*ChainInjector, error) { 17 | // set defaults 18 | ci := &ChainInjector{} 19 | 20 | // apply options 21 | for _, opt := range opts { 22 | err := opt.applyChainInjector(ci) 23 | if err != nil { 24 | return nil, err 25 | } 26 | } 27 | 28 | // set middleware 29 | for _, i := range is { 30 | ci.middlewares = append(ci.middlewares, i.Handler) 31 | } 32 | 33 | return ci, nil 34 | } 35 | 36 | // Handler executes ChainInjector.middlewares in order and then returns. 37 | func (i *ChainInjector) Handler(next http.Handler) http.Handler { 38 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | // Loop in reverse to preserve handler order 40 | for idx := len(i.middlewares) - 1; idx >= 0; idx-- { 41 | next = i.middlewares[idx](next) 42 | } 43 | 44 | next.ServeHTTP(w, r) 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /injector_chain_test.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // TestNewChainInjector tests NewChainInjector. 12 | func TestNewChainInjector(t *testing.T) { 13 | t.Parallel() 14 | 15 | tests := []struct { 16 | name string 17 | giveInjector []Injector 18 | giveOptions []ChainInjectorOption 19 | wantErr error 20 | }{ 21 | { 22 | name: "nil", 23 | giveInjector: nil, 24 | giveOptions: []ChainInjectorOption{}, 25 | wantErr: nil, 26 | }, 27 | { 28 | name: "empty", 29 | giveInjector: []Injector{}, 30 | giveOptions: []ChainInjectorOption{}, 31 | wantErr: nil, 32 | }, 33 | { 34 | name: "one", 35 | giveInjector: []Injector{ 36 | newTestInjectorNoop(), 37 | }, 38 | giveOptions: []ChainInjectorOption{}, 39 | wantErr: nil, 40 | }, 41 | { 42 | name: "two", 43 | giveInjector: []Injector{ 44 | newTestInjectorNoop(), 45 | newTestInjector500s(), 46 | }, 47 | giveOptions: []ChainInjectorOption{}, 48 | wantErr: nil, 49 | }, 50 | { 51 | name: "option error", 52 | giveInjector: []Injector{ 53 | newTestInjectorNoop(), 54 | }, 55 | giveOptions: []ChainInjectorOption{ 56 | withError(), 57 | }, 58 | wantErr: errErrorOption, 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | t.Parallel() 65 | 66 | ci, err := NewChainInjector(tt.giveInjector, tt.giveOptions...) 67 | 68 | assert.Equal(t, tt.wantErr, err) 69 | 70 | if tt.wantErr == nil { 71 | assert.Equal(t, len(tt.giveInjector), len(ci.middlewares)) 72 | } else { 73 | assert.Nil(t, ci) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | // TestChainInjectorHandler tests ChainInjector.Handler. 80 | func TestChainInjectorHandler(t *testing.T) { 81 | t.Parallel() 82 | 83 | tests := []struct { 84 | name string 85 | giveInjector []Injector 86 | giveOptions []ChainInjectorOption 87 | wantCode int 88 | wantBody string 89 | }{ 90 | { 91 | name: "nil", 92 | giveInjector: nil, 93 | giveOptions: []ChainInjectorOption{}, 94 | wantCode: testHandlerCode, 95 | wantBody: testHandlerBody, 96 | }, 97 | { 98 | name: "empty", 99 | giveInjector: []Injector{}, 100 | giveOptions: []ChainInjectorOption{}, 101 | wantCode: testHandlerCode, 102 | wantBody: testHandlerBody, 103 | }, 104 | { 105 | name: "one", 106 | giveInjector: []Injector{ 107 | newTestInjectorOneOK(), 108 | }, 109 | giveOptions: []ChainInjectorOption{}, 110 | wantCode: http.StatusOK, 111 | wantBody: "one" + testHandlerBody, 112 | }, 113 | { 114 | name: "noop one", 115 | giveInjector: []Injector{ 116 | newTestInjectorNoop(), 117 | newTestInjectorOneOK(), 118 | }, 119 | giveOptions: []ChainInjectorOption{}, 120 | wantCode: http.StatusOK, 121 | wantBody: "one" + testHandlerBody, 122 | }, 123 | { 124 | name: "two error", 125 | giveInjector: []Injector{ 126 | newTestInjectorTwoTeapot(), 127 | newTestInjector500s(), 128 | }, 129 | giveOptions: []ChainInjectorOption{}, 130 | wantCode: http.StatusTeapot, 131 | wantBody: "two" + http.StatusText(http.StatusInternalServerError), 132 | }, 133 | { 134 | name: "one two", 135 | giveInjector: []Injector{ 136 | newTestInjectorOneOK(), 137 | newTestInjectorTwoTeapot(), 138 | }, 139 | giveOptions: []ChainInjectorOption{}, 140 | wantCode: http.StatusOK, 141 | wantBody: "one" + "two" + testHandlerBody, 142 | }, 143 | { 144 | name: "one stop two", 145 | giveInjector: []Injector{ 146 | newTestInjectorOneOK(), 147 | newTestInjectorStop(), 148 | newTestInjectorTwoTeapot(), 149 | }, 150 | giveOptions: []ChainInjectorOption{}, 151 | wantCode: http.StatusOK, 152 | wantBody: "one", 153 | }, 154 | } 155 | 156 | for _, tt := range tests { 157 | t.Run(tt.name, func(t *testing.T) { 158 | t.Parallel() 159 | 160 | ci, err := NewChainInjector(tt.giveInjector, tt.giveOptions...) 161 | assert.NoError(t, err) 162 | 163 | f, err := NewFault(ci, 164 | WithEnabled(true), 165 | WithParticipation(1.0), 166 | ) 167 | assert.NoError(t, err) 168 | 169 | rr := testRequest(t, f) 170 | 171 | assert.Equal(t, tt.wantCode, rr.Code) 172 | assert.Equal(t, tt.wantBody, strings.TrimSpace(rr.Body.String())) 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /injector_error.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "reflect" 7 | ) 8 | 9 | var ( 10 | // ErrInvalidHTTPCode when an invalid status code is provided. 11 | ErrInvalidHTTPCode = errors.New("not a valid http status code") 12 | ) 13 | 14 | // ErrorInjector responds with an http status code and message. 15 | type ErrorInjector struct { 16 | statusCode int 17 | statusText string 18 | reporter Reporter 19 | } 20 | 21 | // ErrorInjectorOption configures an ErrorInjector. 22 | type ErrorInjectorOption interface { 23 | applyErrorInjector(i *ErrorInjector) error 24 | } 25 | 26 | type statusTextOption string 27 | 28 | func (o statusTextOption) applyErrorInjector(i *ErrorInjector) error { 29 | i.statusText = string(o) 30 | return nil 31 | } 32 | 33 | // WithStatusText sets custom status text to write. 34 | func WithStatusText(t string) ErrorInjectorOption { 35 | return statusTextOption(t) 36 | } 37 | 38 | func (o reporterOption) applyErrorInjector(i *ErrorInjector) error { 39 | i.reporter = o.reporter 40 | return nil 41 | } 42 | 43 | // NewErrorInjector returns an ErrorInjector that reponds with a status code. 44 | func NewErrorInjector(code int, opts ...ErrorInjectorOption) (*ErrorInjector, error) { 45 | const placeholderStatusText = "go-fault: replace with default code text" 46 | 47 | // set defaults 48 | ei := &ErrorInjector{ 49 | statusCode: code, 50 | statusText: placeholderStatusText, 51 | reporter: NewNoopReporter(), 52 | } 53 | 54 | // apply options 55 | for _, opt := range opts { 56 | err := opt.applyErrorInjector(ei) 57 | if err != nil { 58 | return nil, err 59 | } 60 | } 61 | 62 | // check options 63 | if http.StatusText(ei.statusCode) == "" { 64 | return nil, ErrInvalidHTTPCode 65 | } 66 | if ei.statusText == placeholderStatusText { 67 | ei.statusText = http.StatusText(ei.statusCode) 68 | } 69 | 70 | return ei, nil 71 | } 72 | 73 | // Handler responds with the configured status code and text. 74 | func (i *ErrorInjector) Handler(next http.Handler) http.Handler { 75 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 76 | go i.reporter.Report(reflect.ValueOf(*i).Type().Name(), StateStarted) 77 | http.Error(w, i.statusText, i.statusCode) 78 | go i.reporter.Report(reflect.ValueOf(*i).Type().Name(), StateFinished) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /injector_error_test.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // TestNewErrorInjector tests NewErrorInjector. 12 | func TestNewErrorInjector(t *testing.T) { 13 | t.Parallel() 14 | 15 | tests := []struct { 16 | name string 17 | giveCode int 18 | giveOptions []ErrorInjectorOption 19 | want *ErrorInjector 20 | wantErr error 21 | }{ 22 | { 23 | name: "only code", 24 | giveCode: http.StatusCreated, 25 | giveOptions: nil, 26 | want: &ErrorInjector{ 27 | statusCode: http.StatusCreated, 28 | statusText: http.StatusText(http.StatusCreated), 29 | reporter: NewNoopReporter(), 30 | }, 31 | wantErr: nil, 32 | }, 33 | { 34 | name: "code with different text", 35 | giveCode: http.StatusCreated, 36 | giveOptions: []ErrorInjectorOption{ 37 | WithStatusText(http.StatusText(http.StatusAccepted)), 38 | }, 39 | want: &ErrorInjector{ 40 | statusCode: http.StatusCreated, 41 | statusText: http.StatusText(http.StatusAccepted), 42 | reporter: NewNoopReporter(), 43 | }, 44 | wantErr: nil, 45 | }, 46 | { 47 | name: "code with random text", 48 | giveCode: http.StatusTeapot, 49 | giveOptions: []ErrorInjectorOption{ 50 | WithStatusText("wow very random"), 51 | }, 52 | want: &ErrorInjector{ 53 | statusCode: http.StatusTeapot, 54 | statusText: "wow very random", 55 | reporter: NewNoopReporter(), 56 | }, 57 | wantErr: nil, 58 | }, 59 | { 60 | name: "custom reporter", 61 | giveCode: http.StatusOK, 62 | giveOptions: []ErrorInjectorOption{ 63 | WithReporter(newTestReporter()), 64 | }, 65 | want: &ErrorInjector{ 66 | statusCode: http.StatusOK, 67 | statusText: http.StatusText(http.StatusOK), 68 | reporter: newTestReporter(), 69 | }, 70 | wantErr: nil, 71 | }, 72 | { 73 | name: "invalid code", 74 | giveCode: 0, 75 | giveOptions: []ErrorInjectorOption{ 76 | WithStatusText("invalid code"), 77 | }, 78 | want: nil, 79 | wantErr: ErrInvalidHTTPCode, 80 | }, 81 | { 82 | name: "option error", 83 | giveCode: 200, 84 | giveOptions: []ErrorInjectorOption{ 85 | withError(), 86 | }, 87 | want: nil, 88 | wantErr: errErrorOption, 89 | }, 90 | } 91 | 92 | for _, tt := range tests { 93 | t.Run(tt.name, func(t *testing.T) { 94 | t.Parallel() 95 | 96 | ei, err := NewErrorInjector(tt.giveCode, tt.giveOptions...) 97 | 98 | assert.Equal(t, tt.wantErr, err) 99 | assert.Equal(t, tt.want, ei) 100 | }) 101 | } 102 | } 103 | 104 | // TestErrorInjectorHandler tests ErrorInjector.Handler. 105 | func TestErrorInjectorHandler(t *testing.T) { 106 | t.Parallel() 107 | 108 | tests := []struct { 109 | name string 110 | giveCode int 111 | giveOptions []ErrorInjectorOption 112 | wantCode int 113 | wantBody string 114 | }{ 115 | { 116 | name: "only code", 117 | giveCode: http.StatusInternalServerError, 118 | giveOptions: nil, 119 | wantCode: http.StatusInternalServerError, 120 | wantBody: http.StatusText(http.StatusInternalServerError), 121 | }, 122 | { 123 | name: "custom text", 124 | giveCode: http.StatusInternalServerError, 125 | giveOptions: []ErrorInjectorOption{ 126 | WithStatusText("very custom text"), 127 | }, 128 | wantCode: http.StatusInternalServerError, 129 | wantBody: "very custom text", 130 | }, 131 | } 132 | 133 | for _, tt := range tests { 134 | t.Run(tt.name, func(t *testing.T) { 135 | t.Parallel() 136 | 137 | ei, err := NewErrorInjector(tt.giveCode, tt.giveOptions...) 138 | assert.NoError(t, err) 139 | 140 | f, err := NewFault(ei, 141 | WithEnabled(true), 142 | WithParticipation(1.0), 143 | ) 144 | assert.NoError(t, err) 145 | 146 | rr := testRequest(t, f) 147 | 148 | assert.Equal(t, tt.wantCode, rr.Code) 149 | assert.Equal(t, tt.wantBody, strings.TrimSpace(rr.Body.String())) 150 | }) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /injector_random.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | "sync" 7 | ) 8 | 9 | // RandomInjector combines many Injectors into a single Injector that runs one randomly. 10 | type RandomInjector struct { 11 | middlewares []func(next http.Handler) http.Handler 12 | 13 | randSeed int64 14 | rand *rand.Rand 15 | randF func(int) int 16 | 17 | // *rand.Rand is not thread safe. This mutex protects our random source 18 | randMtx sync.Mutex 19 | } 20 | 21 | // RandomInjectorOption configures a RandomInjector. 22 | type RandomInjectorOption interface { 23 | applyRandomInjector(i *RandomInjector) error 24 | } 25 | 26 | func (o randSeedOption) applyRandomInjector(i *RandomInjector) error { 27 | i.randSeed = int64(o) 28 | return nil 29 | } 30 | 31 | type randIntFuncOption func(int) int 32 | 33 | func (o randIntFuncOption) applyRandomInjector(i *RandomInjector) error { 34 | i.randF = o 35 | return nil 36 | } 37 | 38 | // WithRandIntFunc sets the function that will be used to randomly get an int. Default rand.Intn. 39 | // Always returns an integer between [0,n) to avoid panics. 40 | func WithRandIntFunc(f func(int) int) RandomInjectorOption { 41 | return randIntFuncOption(f) 42 | } 43 | 44 | // NewRandomInjector combines many Injectors into a single Injector that runs one randomly. 45 | func NewRandomInjector(is []Injector, opts ...RandomInjectorOption) (*RandomInjector, error) { 46 | // set defaults 47 | ri := &RandomInjector{ 48 | randSeed: defaultRandSeed, 49 | randF: nil, 50 | } 51 | 52 | // apply options 53 | for _, opt := range opts { 54 | err := opt.applyRandomInjector(ri) 55 | if err != nil { 56 | return nil, err 57 | } 58 | } 59 | 60 | // set middleware 61 | for _, i := range is { 62 | ri.middlewares = append(ri.middlewares, i.Handler) 63 | } 64 | 65 | // set seeded rand source and function 66 | ri.rand = rand.New(rand.NewSource(ri.randSeed)) 67 | if ri.randF == nil { 68 | ri.randF = ri.rand.Intn 69 | } 70 | 71 | return ri, nil 72 | } 73 | 74 | // Handler executes a random Injector from RandomInjector.middlewares. 75 | func (i *RandomInjector) Handler(next http.Handler) http.Handler { 76 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 77 | if len(i.middlewares) > 0 { 78 | i.randMtx.Lock() 79 | randIdx := i.randF(len(i.middlewares)) 80 | i.randMtx.Unlock() 81 | 82 | i.middlewares[randIdx](next).ServeHTTP(w, r) 83 | } else { 84 | next.ServeHTTP(w, r) 85 | } 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /injector_random_test.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | // TestNewRandomInjector tests NewRandomInjector. 13 | func TestNewRandomInjector(t *testing.T) { 14 | t.Parallel() 15 | 16 | tests := []struct { 17 | name string 18 | giveInjector []Injector 19 | giveOptions []RandomInjectorOption 20 | wantRand *rand.Rand 21 | wantErr error 22 | }{ 23 | { 24 | name: "nil", 25 | giveInjector: nil, 26 | giveOptions: nil, 27 | wantRand: rand.New(rand.NewSource(defaultRandSeed)), 28 | wantErr: nil, 29 | }, 30 | { 31 | name: "empty", 32 | giveInjector: []Injector{}, 33 | giveOptions: nil, 34 | wantRand: rand.New(rand.NewSource(defaultRandSeed)), 35 | wantErr: nil, 36 | }, 37 | { 38 | name: "one", 39 | giveInjector: []Injector{ 40 | newTestInjectorNoop(), 41 | }, 42 | giveOptions: nil, 43 | wantRand: rand.New(rand.NewSource(defaultRandSeed)), 44 | wantErr: nil, 45 | }, 46 | { 47 | name: "two", 48 | giveInjector: []Injector{ 49 | newTestInjectorNoop(), 50 | newTestInjector500s(), 51 | }, 52 | giveOptions: nil, 53 | wantRand: rand.New(rand.NewSource(defaultRandSeed)), 54 | wantErr: nil, 55 | }, 56 | { 57 | name: "with seed", 58 | giveInjector: []Injector{ 59 | newTestInjectorNoop(), 60 | newTestInjector500s(), 61 | }, 62 | giveOptions: []RandomInjectorOption{ 63 | WithRandSeed(100), 64 | }, 65 | wantRand: rand.New(rand.NewSource(100)), 66 | wantErr: nil, 67 | }, 68 | { 69 | name: "with custom function", 70 | giveInjector: []Injector{ 71 | newTestInjectorNoop(), 72 | newTestInjector500s(), 73 | }, 74 | giveOptions: []RandomInjectorOption{ 75 | WithRandIntFunc(func(int) int { return 1 }), 76 | }, 77 | wantRand: rand.New(rand.NewSource(defaultRandSeed)), 78 | wantErr: nil, 79 | }, 80 | { 81 | name: "option error", 82 | giveInjector: []Injector{ 83 | newTestInjectorNoop(), 84 | }, 85 | giveOptions: []RandomInjectorOption{ 86 | withError(), 87 | }, 88 | wantRand: rand.New(rand.NewSource(defaultRandSeed)), 89 | wantErr: errErrorOption, 90 | }, 91 | } 92 | 93 | for _, tt := range tests { 94 | t.Run(tt.name, func(t *testing.T) { 95 | t.Parallel() 96 | 97 | ri, err := NewRandomInjector(tt.giveInjector, tt.giveOptions...) 98 | 99 | assert.Equal(t, tt.wantErr, err) 100 | 101 | if tt.wantErr == nil { 102 | assert.Equal(t, tt.wantRand, ri.rand) 103 | assert.Equal(t, len(tt.giveInjector), len(ri.middlewares)) 104 | } else { 105 | assert.Nil(t, ri) 106 | } 107 | }) 108 | } 109 | } 110 | 111 | // TestRandomInjectorHandler tests RandomInjector.Handler. 112 | func TestRandomInjectorHandler(t *testing.T) { 113 | t.Parallel() 114 | 115 | tests := []struct { 116 | name string 117 | give []Injector 118 | giveOptions []RandomInjectorOption 119 | wantCode int 120 | wantBody string 121 | }{ 122 | { 123 | name: "nil", 124 | give: nil, 125 | giveOptions: nil, 126 | wantCode: testHandlerCode, 127 | wantBody: testHandlerBody, 128 | }, 129 | { 130 | name: "empty", 131 | give: []Injector{}, 132 | giveOptions: nil, 133 | wantCode: testHandlerCode, 134 | wantBody: testHandlerBody, 135 | }, 136 | { 137 | name: "one", 138 | give: []Injector{ 139 | newTestInjectorOneOK(), 140 | }, 141 | giveOptions: nil, 142 | wantCode: http.StatusOK, 143 | wantBody: "one" + testHandlerBody, 144 | }, 145 | { 146 | name: "two", 147 | give: []Injector{ 148 | newTestInjectorOneOK(), 149 | newTestInjectorTwoTeapot(), 150 | }, 151 | giveOptions: nil, 152 | // defaultRandSeed will choose 1 153 | wantCode: http.StatusTeapot, 154 | wantBody: "two" + testHandlerBody, 155 | }, 156 | { 157 | name: "seven", 158 | give: []Injector{ 159 | newTestInjectorNoop(), 160 | newTestInjectorNoop(), 161 | newTestInjectorNoop(), 162 | newTestInjectorNoop(), 163 | newTestInjectorNoop(), 164 | newTestInjectorNoop(), 165 | newTestInjectorTwoTeapot(), 166 | }, 167 | giveOptions: nil, 168 | // defaultRandSeed will choose 6 169 | wantCode: http.StatusTeapot, 170 | wantBody: "two" + testHandlerBody, 171 | }, 172 | { 173 | name: "custom rand func", 174 | give: []Injector{ 175 | newTestInjectorNoop(), 176 | newTestInjectorNoop(), 177 | newTestInjectorTwoTeapot(), 178 | newTestInjectorNoop(), 179 | newTestInjectorNoop(), 180 | newTestInjectorNoop(), 181 | newTestInjectorNoop(), 182 | }, 183 | giveOptions: []RandomInjectorOption{ 184 | WithRandIntFunc(func(int) int { return 2 }), 185 | }, 186 | // defaultRandSeed will choose 6. Custom function should choose 2. 187 | wantCode: http.StatusTeapot, 188 | wantBody: "two" + testHandlerBody, 189 | }, 190 | } 191 | 192 | for _, tt := range tests { 193 | t.Run(tt.name, func(t *testing.T) { 194 | t.Parallel() 195 | 196 | ri, err := NewRandomInjector(tt.give, tt.giveOptions...) 197 | assert.NoError(t, err) 198 | 199 | f, err := NewFault(ri, 200 | WithEnabled(true), 201 | WithParticipation(1.0), 202 | ) 203 | assert.NoError(t, err) 204 | 205 | rr := testRequest(t, f) 206 | 207 | assert.Equal(t, tt.wantCode, rr.Code) 208 | assert.Equal(t, tt.wantBody, strings.TrimSpace(rr.Body.String())) 209 | }) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /injector_reject.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | ) 7 | 8 | // RejectInjector sends back an empty response. 9 | type RejectInjector struct { 10 | reporter Reporter 11 | } 12 | 13 | // RejectInjectorOption configures a RejectInjector. 14 | type RejectInjectorOption interface { 15 | applyRejectInjector(i *RejectInjector) error 16 | } 17 | 18 | func (o reporterOption) applyRejectInjector(i *RejectInjector) error { 19 | i.reporter = o.reporter 20 | return nil 21 | } 22 | 23 | // NewRejectInjector returns a RejectInjector. 24 | func NewRejectInjector(opts ...RejectInjectorOption) (*RejectInjector, error) { 25 | // set defaults 26 | ri := &RejectInjector{ 27 | reporter: NewNoopReporter(), 28 | } 29 | 30 | // apply options 31 | for _, opt := range opts { 32 | err := opt.applyRejectInjector(ri) 33 | if err != nil { 34 | return nil, err 35 | } 36 | } 37 | 38 | return ri, nil 39 | } 40 | 41 | // Handler rejects the request, returning an empty response. 42 | func (i *RejectInjector) Handler(next http.Handler) http.Handler { 43 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 44 | go i.reporter.Report(reflect.ValueOf(*i).Type().Name(), StateStarted) 45 | 46 | // This is a specialized and documented way of sending an interrupted response to 47 | // the client without printing the panic stack trace or erroring. 48 | // https://golang.org/pkg/net/http/#Handler 49 | panic(http.ErrAbortHandler) 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /injector_reject_test.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // TestNewRejectInjector tests NewRejectInjector. 10 | func TestNewRejectInjector(t *testing.T) { 11 | t.Parallel() 12 | 13 | tests := []struct { 14 | name string 15 | giveOptions []RejectInjectorOption 16 | want *RejectInjector 17 | wantErr error 18 | }{ 19 | { 20 | name: "no options", 21 | giveOptions: []RejectInjectorOption{}, 22 | want: &RejectInjector{ 23 | reporter: NewNoopReporter(), 24 | }, 25 | wantErr: nil, 26 | }, 27 | { 28 | name: "custom reporter", 29 | giveOptions: []RejectInjectorOption{ 30 | WithReporter(newTestReporter()), 31 | }, 32 | want: &RejectInjector{ 33 | reporter: newTestReporter(), 34 | }, 35 | wantErr: nil, 36 | }, 37 | { 38 | name: "option error", 39 | giveOptions: []RejectInjectorOption{ 40 | withError(), 41 | }, 42 | want: nil, 43 | wantErr: errErrorOption, 44 | }, 45 | } 46 | 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | t.Parallel() 50 | 51 | ri, err := NewRejectInjector(tt.giveOptions...) 52 | 53 | assert.Equal(t, tt.wantErr, err) 54 | assert.Equal(t, tt.want, ri) 55 | }) 56 | } 57 | } 58 | 59 | // TestRejectInjectorHandler tests RejectInjector.Handler. 60 | func TestRejectInjectorHandler(t *testing.T) { 61 | t.Parallel() 62 | 63 | tests := []struct { 64 | name string 65 | giveOptions []RejectInjectorOption 66 | }{ 67 | { 68 | name: "valid", 69 | giveOptions: []RejectInjectorOption{}, 70 | }, 71 | } 72 | 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | t.Parallel() 76 | 77 | ri, err := NewRejectInjector(tt.giveOptions...) 78 | assert.NoError(t, err) 79 | 80 | f, err := NewFault(ri, 81 | WithEnabled(true), 82 | WithParticipation(1.0), 83 | ) 84 | assert.NoError(t, err) 85 | 86 | rr := testRequestExpectPanic(t, f) 87 | assert.Nil(t, rr) 88 | }) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /injector_slow.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "time" 7 | ) 8 | 9 | // SlowInjector waits and then continues the request. 10 | type SlowInjector struct { 11 | duration time.Duration 12 | slowF func(t time.Duration) 13 | reporter Reporter 14 | } 15 | 16 | // SlowInjectorOption configures a SlowInjector. 17 | type SlowInjectorOption interface { 18 | applySlowInjector(i *SlowInjector) error 19 | } 20 | 21 | type slowFunctionOption func(t time.Duration) 22 | 23 | func (o slowFunctionOption) applySlowInjector(i *SlowInjector) error { 24 | i.slowF = o 25 | return nil 26 | } 27 | 28 | // WithSlowFunc sets the function that will be used to wait the time.Duration. 29 | func WithSlowFunc(f func(t time.Duration)) SlowInjectorOption { 30 | return slowFunctionOption(f) 31 | } 32 | 33 | func (o reporterOption) applySlowInjector(i *SlowInjector) error { 34 | i.reporter = o.reporter 35 | return nil 36 | } 37 | 38 | // NewSlowInjector returns a SlowInjector. 39 | func NewSlowInjector(d time.Duration, opts ...SlowInjectorOption) (*SlowInjector, error) { 40 | // set defaults 41 | si := &SlowInjector{ 42 | duration: d, 43 | slowF: time.Sleep, 44 | reporter: NewNoopReporter(), 45 | } 46 | 47 | // apply options 48 | for _, opt := range opts { 49 | err := opt.applySlowInjector(si) 50 | if err != nil { 51 | return nil, err 52 | } 53 | } 54 | 55 | return si, nil 56 | } 57 | 58 | // Handler runs i.slowF to wait the set duration and then continues. 59 | func (i *SlowInjector) Handler(next http.Handler) http.Handler { 60 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 61 | go i.reporter.Report(reflect.ValueOf(*i).Type().Name(), StateStarted) 62 | i.slowF(i.duration) 63 | go i.reporter.Report(reflect.ValueOf(*i).Type().Name(), StateFinished) 64 | 65 | next.ServeHTTP(w, r) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /injector_slow_test.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // TestNewSlowInjector tests NewSlowInjector. 12 | func TestNewSlowInjector(t *testing.T) { 13 | t.Parallel() 14 | 15 | tests := []struct { 16 | name string 17 | giveDuration time.Duration 18 | giveOptions []SlowInjectorOption 19 | want *SlowInjector 20 | wantErr error 21 | }{ 22 | { 23 | name: "nil", 24 | giveDuration: 0, 25 | giveOptions: nil, 26 | want: &SlowInjector{ 27 | duration: 0, 28 | slowF: time.Sleep, 29 | reporter: NewNoopReporter(), 30 | }, 31 | wantErr: nil, 32 | }, 33 | { 34 | name: "empty", 35 | giveDuration: 0, 36 | giveOptions: []SlowInjectorOption{}, 37 | want: &SlowInjector{ 38 | duration: 0, 39 | slowF: time.Sleep, 40 | reporter: NewNoopReporter(), 41 | }, 42 | wantErr: nil, 43 | }, 44 | { 45 | name: "custom duration", 46 | giveDuration: time.Minute, 47 | giveOptions: nil, 48 | want: &SlowInjector{ 49 | duration: time.Minute, 50 | slowF: time.Sleep, 51 | reporter: NewNoopReporter(), 52 | }, 53 | wantErr: nil, 54 | }, 55 | { 56 | name: "custom sleep", 57 | giveDuration: time.Minute, 58 | giveOptions: []SlowInjectorOption{ 59 | WithSlowFunc(func(time.Duration) {}), 60 | }, 61 | want: &SlowInjector{ 62 | duration: time.Minute, 63 | slowF: func(time.Duration) {}, 64 | reporter: NewNoopReporter(), 65 | }, 66 | wantErr: nil, 67 | }, 68 | { 69 | name: "custom reporter", 70 | giveDuration: time.Minute, 71 | giveOptions: []SlowInjectorOption{ 72 | WithReporter(newTestReporter()), 73 | }, 74 | want: &SlowInjector{ 75 | duration: time.Minute, 76 | slowF: time.Sleep, 77 | reporter: newTestReporter(), 78 | }, 79 | wantErr: nil, 80 | }, 81 | { 82 | name: "option error", 83 | giveDuration: time.Minute, 84 | giveOptions: []SlowInjectorOption{ 85 | withError(), 86 | }, 87 | want: nil, 88 | wantErr: errErrorOption, 89 | }, 90 | } 91 | 92 | for _, tt := range tests { 93 | t.Run(tt.name, func(t *testing.T) { 94 | t.Parallel() 95 | 96 | si, err := NewSlowInjector(tt.giveDuration, tt.giveOptions...) 97 | 98 | // Function equality cannot be determined so set to nil before comparing 99 | if tt.want != nil { 100 | si.slowF = nil 101 | tt.want.slowF = nil 102 | } 103 | 104 | assert.Equal(t, tt.wantErr, err) 105 | assert.Equal(t, tt.want, si) 106 | }) 107 | } 108 | } 109 | 110 | // TestSlowInjectorHandler tests SlowInjector.Handler. 111 | func TestSlowInjectorHandler(t *testing.T) { 112 | t.Parallel() 113 | 114 | tests := []struct { 115 | name string 116 | giveDuration time.Duration 117 | giveOptions []SlowInjectorOption 118 | wantCode int 119 | wantBody string 120 | }{ 121 | { 122 | name: "nil", 123 | giveDuration: 0, 124 | giveOptions: nil, 125 | wantCode: testHandlerCode, 126 | wantBody: testHandlerBody, 127 | }, 128 | { 129 | name: "empty", 130 | giveDuration: 0, 131 | giveOptions: []SlowInjectorOption{}, 132 | wantCode: testHandlerCode, 133 | wantBody: testHandlerBody, 134 | }, 135 | { 136 | name: "with time.Sleep", 137 | giveDuration: time.Microsecond, 138 | giveOptions: nil, 139 | wantCode: testHandlerCode, 140 | wantBody: testHandlerBody, 141 | }, 142 | { 143 | name: "with custom function", 144 | giveDuration: time.Hour, 145 | giveOptions: []SlowInjectorOption{ 146 | WithSlowFunc(func(time.Duration) {}), 147 | }, 148 | wantCode: testHandlerCode, 149 | wantBody: testHandlerBody, 150 | }, 151 | } 152 | 153 | for _, tt := range tests { 154 | t.Run(tt.name, func(t *testing.T) { 155 | t.Parallel() 156 | 157 | si, err := NewSlowInjector(tt.giveDuration, tt.giveOptions...) 158 | assert.NoError(t, err) 159 | 160 | f, err := NewFault(si, 161 | WithEnabled(true), 162 | WithParticipation(1.0), 163 | ) 164 | assert.NoError(t, err) 165 | 166 | rr := testRequest(t, f) 167 | 168 | assert.Equal(t, tt.wantCode, rr.Code) 169 | assert.Equal(t, tt.wantBody, strings.TrimSpace(rr.Body.String())) 170 | }) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /reporter.go: -------------------------------------------------------------------------------- 1 | package fault 2 | 3 | // Reporter receives events from faults to use for logging, stats, and other custom reporting. 4 | type Reporter interface { 5 | Report(name string, state InjectorState) 6 | } 7 | 8 | // NoopReporter is a reporter that does nothing. 9 | type NoopReporter struct{} 10 | 11 | // NewNoopReporter returns a new NoopReporter. 12 | func NewNoopReporter() *NoopReporter { 13 | return &NoopReporter{} 14 | } 15 | 16 | // Report does nothing. 17 | func (r *NoopReporter) Report(name string, state InjectorState) {} 18 | 19 | // ReporterOption configures structs that accept a Reporter. 20 | type ReporterOption interface { 21 | RejectInjectorOption 22 | ErrorInjectorOption 23 | SlowInjectorOption 24 | } 25 | 26 | // reporterOption holds our passed in Reporter. 27 | type reporterOption struct { 28 | reporter Reporter 29 | } 30 | 31 | // WithReporter sets the Reporter. 32 | func WithReporter(r Reporter) ReporterOption { 33 | return reporterOption{r} 34 | } 35 | --------------------------------------------------------------------------------