diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..ad13382 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,67 @@ +package config + +import "testing" + +func TestGetValueOrDefaultBoolean(t *testing.T) { + tests := []struct { + name string + value string + defaultValue bool + want bool + }{ + {name: "default true", defaultValue: true, want: true}, + {name: "default false", defaultValue: false, want: false}, + {name: "explicit true", value: "true", want: true}, + {name: "explicit false", value: "false", defaultValue: true, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getValueOrDefaultBoolean(tt.value, tt.defaultValue); got != tt.want { + t.Fatalf("getValueOrDefaultBoolean() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetValueOrDefaultInt(t *testing.T) { + tests := []struct { + name string + value string + defaultValue int + want int + }{ + {name: "default", defaultValue: 3000, want: 3000}, + {name: "configured", value: "8080", defaultValue: 3000, want: 8080}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getValueOrDefaultInt(tt.value, tt.defaultValue); got != tt.want { + t.Fatalf("getValueOrDefaultInt() = %d, want %d", got, tt.want) + } + }) + } +} + +func TestDefaultConfigs(t *testing.T) { + for _, key := range []string{ + "ENV", "PORT", "IS_DB_SEEDED", "IS_RATE_LIMITER_ENABLED", + "THREADS_MAX_COUNT", "THREAD_BUMP_LIMIT", "IS_CAPTCHA_ACTIVE", + "GATEWAY_URL", "GATEWAY_API_KEY", "GATEWAY_BOARD_ID", + "GATEWAY_BOARD_URL", "GATEWAY_BOARD_DESCRIPTION", "POSTGRES_URL", + } { + t.Setenv(key, "") + } + + app := getAppConfig() + if app.Env != "release" || app.Port != 3000 || app.IsDbSeeded { + t.Fatalf("unexpected app defaults: %+v", app) + } + if !app.IsRateLimiterEnabled || app.ThreadsMaxCount != 50 || app.ThreadBumpLimit != 500 || !app.IsCaptchaActive { + t.Fatalf("unexpected feature defaults: %+v", app) + } + if db := getDbConfig(); db.Url != "postgresql://localhost/micrach" { + t.Fatalf("unexpected database default: %+v", db) + } +} diff --git a/controllers/threads_controller_test.go b/controllers/threads_controller_test.go new file mode 100644 index 0000000..2e6cff3 --- /dev/null +++ b/controllers/threads_controller_test.go @@ -0,0 +1,46 @@ +package controllers + +import ( + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" +) + +type testViews struct{} + +func (testViews) Load() error { return nil } + +func (testViews) Render(out io.Writer, name string, _ interface{}, _ ...string) error { + _, err := io.WriteString(out, name) + return err +} + +func TestGetThreadsRejectsInvalidPage(t *testing.T) { + app := fiber.New(fiber.Config{Views: testViews{}}) + app.Get("/", GetThreads) + + resp, err := app.Test(httptest.NewRequest("GET", "/?page=0", nil)) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != fiber.StatusNotFound { + t.Fatalf("status = %d, want %d", resp.StatusCode, fiber.StatusNotFound) + } +} + +func TestGetThreadRejectsInvalidID(t *testing.T) { + app := fiber.New(fiber.Config{Views: testViews{}}) + app.Get("/:threadID", GetThread) + + resp, err := app.Test(httptest.NewRequest("GET", "/invalid", nil)) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != fiber.StatusNotFound { + t.Fatalf("status = %d, want %d", resp.StatusCode, fiber.StatusNotFound) + } +} diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 0000000..1f58b11 --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,48 @@ +package utils + +import ( + "mime/multipart" + "net/textproto" + "strings" + "testing" + + "micrach/repositories" +) + +func fileHeader(contentType string, size int64) *multipart.FileHeader { + header := make(textproto.MIMEHeader) + header.Set("Content-Type", contentType) + return &multipart.FileHeader{Filename: "upload", Header: header, Size: size} +} + +func TestValidatePost(t *testing.T) { + validPNG := fileHeader("image/png", 1) + validJPEG := fileHeader("image/jpeg", FILE_SIZE_IN_BYTES) + + tests := []struct { + name string + title string + text string + files []*multipart.FileHeader + want string + }{ + {name: "text only", text: "hello"}, + {name: "file only", files: []*multipart.FileHeader{validPNG}}, + {name: "empty", want: repositories.InvalidTextOrFilesErrorMessage}, + {name: "title over limit", title: strings.Repeat("я", 101), text: "ok", want: repositories.InvalidTitleLengthErrorMessage}, + {name: "text over limit", text: strings.Repeat("я", 1001), want: repositories.InvalidTextLengthErrorMessage}, + {name: "four files", files: []*multipart.FileHeader{validPNG, validPNG, validJPEG, validJPEG}}, + {name: "five files", files: []*multipart.FileHeader{validPNG, validPNG, validPNG, validPNG, validPNG}, want: repositories.InvalidFilesLengthErrorMessage}, + {name: "unsupported content type", files: []*multipart.FileHeader{fileHeader("image/gif", 1)}, want: repositories.InvalidFileExtErrorMessage}, + {name: "size at limit", files: []*multipart.FileHeader{validJPEG}}, + {name: "size over limit", files: []*multipart.FileHeader{fileHeader("image/jpeg", FILE_SIZE_IN_BYTES+1)}, want: repositories.InvalidFileSizeErrorMessage}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ValidatePost(tt.title, tt.text, tt.files); got != tt.want { + t.Fatalf("ValidatePost() = %q, want %q", got, tt.want) + } + }) + } +}