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  }