From 11943960292b1dc1a425402cc0cee296b9712314 Mon Sep 17 00:00:00 2001 From: Yanislav Igonin Date: Thu, 16 Jul 2026 16:22:15 +0400 Subject: [PATCH] test: characterize Fiber behavior --- controllers/threads_controller_test.go | 39 +++++++++ main.go | 20 +++-- main_test.go | 111 +++++++++++++++++++++++++ templates/templates_test.go | 30 +++++++ 4 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 main_test.go create mode 100644 templates/templates_test.go diff --git a/controllers/threads_controller_test.go b/controllers/threads_controller_test.go index 2e6cff3..1ede73b 100644 --- a/controllers/threads_controller_test.go +++ b/controllers/threads_controller_test.go @@ -1,11 +1,16 @@ package controllers import ( + "bytes" "io" + "mime/multipart" + "net/http" "net/http/httptest" "testing" "github.com/gofiber/fiber/v2" + + "micrach/config" ) type testViews struct{} @@ -44,3 +49,37 @@ func TestGetThreadRejectsInvalidID(t *testing.T) { t.Fatalf("status = %d, want %d", resp.StatusCode, fiber.StatusNotFound) } } + +func TestCreateThreadRejectsInvalidMultipartBeforeDatabase(t *testing.T) { + previousConfig := config.App + t.Cleanup(func() { + config.App = previousConfig + }) + config.App.IsCaptchaActive = false + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + if err := writer.WriteField("title", ""); err != nil { + t.Fatal(err) + } + if err := writer.WriteField("text", ""); err != nil { + t.Fatal(err) + } + if err := writer.Close(); err != nil { + t.Fatal(err) + } + + app := fiber.New(fiber.Config{Views: testViews{}}) + app.Post("/", CreateThread) + + req := httptest.NewRequest(http.MethodPost, "/", &body) + req.Header.Set(fiber.HeaderContentType, writer.FormDataContentType()) + resp, err := app.Test(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != fiber.StatusBadRequest { + t.Fatalf("status = %d, want %d", resp.StatusCode, fiber.StatusBadRequest) + } +} diff --git a/main.go b/main.go index 746f6d9..7f304fc 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,18 @@ import ( "micrach/utils" ) +func skipRateLimit(isLocal bool, path string) bool { + isRequestForStatic := strings.Contains(path, "/static") || + strings.Contains(path, "/uploads") || + strings.Contains(path, "/captcha") + return isRequestForStatic || isLocal +} + +func registerStaticRoutes(app *fiber.App) { + app.Static("/uploads", "./uploads") + app.Static("/static", "./static") +} + func main() { ctx := context.Background() config.Init() @@ -54,18 +66,14 @@ func main() { if config.App.IsRateLimiterEnabled { app.Use(limiter.New(limiter.Config{ Next: func(c *fiber.Ctx) bool { - isDev := c.IsFromLocal() - path := c.Path() - isRequestForStatic := strings.Contains(path, "/static") || strings.Contains(path, "/uploads") || strings.Contains(path, "/captcha") - return isRequestForStatic || isDev + return skipRateLimit(c.IsFromLocal(), c.Path()) }, Max: 50, })) } app.Use(compress.New()) - app.Static("/uploads", "./uploads") - app.Static("/static", "./static") + registerStaticRoutes(app) app.Get("/", controllers.GetThreads) app.Post("/", controllers.CreateThread) diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..cc6a3ad --- /dev/null +++ b/main_test.go @@ -0,0 +1,111 @@ +package main + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/limiter" +) + +func TestRegisterStaticRoutes(t *testing.T) { + t.Chdir(t.TempDir()) + + files := map[string]string{ + "static/styles/app.css": "body { color: black; }", + "uploads/42/o/7.png": "png-body", + } + for name, content := range files { + if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil { + t.Fatalf("create fixture directory: %v", err) + } + if err := os.WriteFile(name, []byte(content), 0o644); err != nil { + t.Fatalf("write fixture: %v", err) + } + } + + app := fiber.New() + registerStaticRoutes(app) + app.Get("/static/fallback", func(c *fiber.Ctx) error { + return c.SendString("fallback") + }) + + tests := []struct { + name string + path string + wantCode int + wantBody string + }{ + {name: "checked-in static file", path: "/static/styles/app.css", wantCode: fiber.StatusOK, wantBody: files["static/styles/app.css"]}, + {name: "generated upload", path: "/uploads/42/o/7.png", wantCode: fiber.StatusOK, wantBody: files["uploads/42/o/7.png"]}, + {name: "missing file continues routing", path: "/static/fallback", wantCode: fiber.StatusOK, wantBody: "fallback"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := app.Test(httptest.NewRequest(http.MethodGet, tt.path, nil)) + if err != nil { + t.Fatalf("request %s: %v", tt.path, err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("read response: %v", err) + } + if resp.StatusCode != tt.wantCode || string(body) != tt.wantBody { + t.Fatalf("response = (%d, %q), want (%d, %q)", resp.StatusCode, body, tt.wantCode, tt.wantBody) + } + }) + } +} + +func TestSkipRateLimit(t *testing.T) { + tests := []struct { + name string + isLocal bool + path string + want bool + }{ + {name: "ordinary remote request", path: "/", want: false}, + {name: "local request", isLocal: true, path: "/", want: true}, + {name: "static", path: "/static/styles/index.css", want: true}, + {name: "upload", path: "/uploads/1/o/1.png", want: true}, + {name: "captcha", path: "/captcha/id", want: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := skipRateLimit(tt.isLocal, tt.path); got != tt.want { + t.Fatalf("skipRateLimit(%v, %q) = %v, want %v", tt.isLocal, tt.path, got, tt.want) + } + }) + } +} + +func TestLimiterRejectsOrdinaryRemoteRequests(t *testing.T) { + app := fiber.New() + app.Use(limiter.New(limiter.Config{ + Next: func(c *fiber.Ctx) bool { + return skipRateLimit(c.IsFromLocal(), c.Path()) + }, + Max: 2, + })) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + for requestNumber, want := range []int{fiber.StatusOK, fiber.StatusOK, fiber.StatusTooManyRequests} { + resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) + if err != nil { + t.Fatalf("request %d: %v", requestNumber+1, err) + } + resp.Body.Close() + if resp.StatusCode != want { + t.Fatalf("request %d status = %d, want %d", requestNumber+1, resp.StatusCode, want) + } + } +} diff --git a/templates/templates_test.go b/templates/templates_test.go new file mode 100644 index 0000000..6eba530 --- /dev/null +++ b/templates/templates_test.go @@ -0,0 +1,30 @@ +package templates + +import ( + "bytes" + "strings" + "testing" + + "github.com/gofiber/template/html/v2" + + "micrach/repositories" +) + +func TestHTMLTemplatesLoadAndRender(t *testing.T) { + engine := html.New("../templates", ".html") + engine.AddFunc("Iterate", Iterate) + engine.AddFunc("NotNil", NotNil) + + if err := engine.Load(); err != nil { + t.Fatalf("load templates: %v", err) + } + + const message = "template compatibility" + var rendered bytes.Buffer + if err := engine.Render(&rendered, "pages/400", repositories.BadRequestHtmlData{Message: message}); err != nil { + t.Fatalf("render template: %v", err) + } + if !strings.Contains(rendered.String(), message) { + t.Fatalf("rendered template does not contain %q", message) + } +}