github.com/gofiber/fiber/v2@v2.47.0/middleware/idempotency/idempotency_test.go (about) 1 //nolint:bodyclose // Much easier to just ignore memory leaks in tests 2 package idempotency_test 3 4 import ( 5 "errors" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "strconv" 10 "sync" 11 "sync/atomic" 12 "testing" 13 "time" 14 15 "github.com/gofiber/fiber/v2" 16 "github.com/gofiber/fiber/v2/middleware/idempotency" 17 "github.com/gofiber/fiber/v2/utils" 18 19 "github.com/valyala/fasthttp" 20 ) 21 22 // go test -run Test_Idempotency 23 func Test_Idempotency(t *testing.T) { 24 t.Parallel() 25 26 app := fiber.New() 27 28 app.Use(func(c *fiber.Ctx) error { 29 if err := c.Next(); err != nil { 30 return err 31 } 32 33 isMethodSafe := fiber.IsMethodSafe(c.Method()) 34 isIdempotent := idempotency.IsFromCache(c) || idempotency.WasPutToCache(c) 35 hasReqHeader := c.Get("X-Idempotency-Key") != "" 36 37 if isMethodSafe { 38 if isIdempotent { 39 return errors.New("request with safe HTTP method should not be idempotent") 40 } 41 } else { 42 // Unsafe 43 if hasReqHeader { 44 if !isIdempotent { 45 return errors.New("request with unsafe HTTP method should be idempotent if X-Idempotency-Key request header is set") 46 } 47 } else { 48 // No request header 49 if isIdempotent { 50 return errors.New("request with unsafe HTTP method should not be idempotent if X-Idempotency-Key request header is not set") 51 } 52 } 53 } 54 55 return nil 56 }) 57 58 // Needs to be at least a second as the memory storage doesn't support shorter durations. 59 const lifetime = 1 * time.Second 60 61 app.Use(idempotency.New(idempotency.Config{ 62 Lifetime: lifetime, 63 })) 64 65 nextCount := func() func() int { 66 var count int32 67 return func() int { 68 return int(atomic.AddInt32(&count, 1)) 69 } 70 }() 71 72 { 73 handler := func(c *fiber.Ctx) error { 74 return c.SendString(strconv.Itoa(nextCount())) 75 } 76 77 app.Get("/", handler) 78 app.Post("/", handler) 79 } 80 81 app.Post("/slow", func(c *fiber.Ctx) error { 82 time.Sleep(2 * lifetime) 83 84 return c.SendString(strconv.Itoa(nextCount())) 85 }) 86 87 doReq := func(method, route, idempotencyKey string) string { 88 req := httptest.NewRequest(method, route, http.NoBody) 89 if idempotencyKey != "" { 90 req.Header.Set("X-Idempotency-Key", idempotencyKey) 91 } 92 resp, err := app.Test(req, 3*int(lifetime.Milliseconds())) 93 utils.AssertEqual(t, nil, err) 94 body, err := io.ReadAll(resp.Body) 95 utils.AssertEqual(t, nil, err) 96 utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, string(body)) 97 return string(body) 98 } 99 100 utils.AssertEqual(t, "1", doReq(fiber.MethodGet, "/", "")) 101 utils.AssertEqual(t, "2", doReq(fiber.MethodGet, "/", "")) 102 103 utils.AssertEqual(t, "3", doReq(fiber.MethodPost, "/", "")) 104 utils.AssertEqual(t, "4", doReq(fiber.MethodPost, "/", "")) 105 106 utils.AssertEqual(t, "5", doReq(fiber.MethodGet, "/", "00000000-0000-0000-0000-000000000000")) 107 utils.AssertEqual(t, "6", doReq(fiber.MethodGet, "/", "00000000-0000-0000-0000-000000000000")) 108 109 utils.AssertEqual(t, "7", doReq(fiber.MethodPost, "/", "00000000-0000-0000-0000-000000000000")) 110 utils.AssertEqual(t, "7", doReq(fiber.MethodPost, "/", "00000000-0000-0000-0000-000000000000")) 111 utils.AssertEqual(t, "8", doReq(fiber.MethodPost, "/", "")) 112 utils.AssertEqual(t, "9", doReq(fiber.MethodPost, "/", "11111111-1111-1111-1111-111111111111")) 113 114 utils.AssertEqual(t, "7", doReq(fiber.MethodPost, "/", "00000000-0000-0000-0000-000000000000")) 115 time.Sleep(2 * lifetime) 116 utils.AssertEqual(t, "10", doReq(fiber.MethodPost, "/", "00000000-0000-0000-0000-000000000000")) 117 utils.AssertEqual(t, "10", doReq(fiber.MethodPost, "/", "00000000-0000-0000-0000-000000000000")) 118 119 // Test raciness 120 { 121 var wg sync.WaitGroup 122 for i := 0; i < 100; i++ { 123 wg.Add(1) 124 go func() { 125 defer wg.Done() 126 utils.AssertEqual(t, "11", doReq(fiber.MethodPost, "/slow", "22222222-2222-2222-2222-222222222222")) 127 }() 128 } 129 wg.Wait() 130 utils.AssertEqual(t, "11", doReq(fiber.MethodPost, "/slow", "22222222-2222-2222-2222-222222222222")) 131 } 132 time.Sleep(2 * lifetime) 133 utils.AssertEqual(t, "12", doReq(fiber.MethodPost, "/slow", "22222222-2222-2222-2222-222222222222")) 134 } 135 136 // go test -v -run=^$ -bench=Benchmark_Idempotency -benchmem -count=4 137 func Benchmark_Idempotency(b *testing.B) { 138 app := fiber.New() 139 140 // Needs to be at least a second as the memory storage doesn't support shorter durations. 141 const lifetime = 1 * time.Second 142 143 app.Use(idempotency.New(idempotency.Config{ 144 Lifetime: lifetime, 145 })) 146 147 app.Post("/", func(c *fiber.Ctx) error { 148 return nil 149 }) 150 151 h := app.Handler() 152 153 b.Run("hit", func(b *testing.B) { 154 c := &fasthttp.RequestCtx{} 155 c.Request.Header.SetMethod(fiber.MethodPost) 156 c.Request.SetRequestURI("/") 157 c.Request.Header.Set("X-Idempotency-Key", "00000000-0000-0000-0000-000000000000") 158 159 b.ReportAllocs() 160 b.ResetTimer() 161 for n := 0; n < b.N; n++ { 162 h(c) 163 } 164 }) 165 166 b.Run("skip", func(b *testing.B) { 167 c := &fasthttp.RequestCtx{} 168 c.Request.Header.SetMethod(fiber.MethodPost) 169 c.Request.SetRequestURI("/") 170 171 b.ReportAllocs() 172 b.ResetTimer() 173 for n := 0; n < b.N; n++ { 174 h(c) 175 } 176 }) 177 }