From 51f30b516ecc7f77ba3943da902f9c15890f1777 Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Sat, 22 Nov 2025 22:23:49 +0300 Subject: [PATCH] feat: some refactoring --- .gitignore | 11 +- Makefile | 147 +++++++++++++ build/Dockerfile | 31 +++ build/docker-compose.yml | 23 ++ cmd/apiserver/config/config.go | 78 +++++++ cmd/apiserver/main.go | 28 ++- configs/development.toml | 16 ++ configs/server.toml | 16 ++ docs/testing_guide.md | 206 ++++++++++++++++++ go.mod | 21 +- go.sum | 65 +++++- internal/apiserver/docs/.gitkeep | 0 internal/apiserver/handlers/home.go | 37 ++++ internal/apiserver/handlers/home_test.go | 41 ++++ internal/apiserver/logger/logger.go | 76 +++++++ internal/apiserver/middleware/logging.go | 42 ++++ internal/apiserver/middleware/request_id.go | 33 +++ internal/apiserver/models/response.go | 25 +++ internal/apiserver/server/routes.go | 22 ++ internal/apiserver/server/server.go | 70 ++++++ internal/apiserver/services/.gitkeep | 0 internal/apiserver/utils/.gitkeep | 0 internal/app/apiserver/apiserver.go | 29 --- internal/app/apiserver/server.go | 81 ------- internal/pkg/errors/.gitkeep | 0 internal/pkg/validators/.gitkeep | 0 .../apiserver => pkg/http}/responsewriter.go | 9 +- scripts/build.sh | 15 ++ scripts/test.sh | 101 +++++++++ 29 files changed, 1094 insertions(+), 129 deletions(-) create mode 100644 Makefile create mode 100644 build/Dockerfile create mode 100644 build/docker-compose.yml create mode 100644 cmd/apiserver/config/config.go create mode 100644 configs/development.toml create mode 100644 configs/server.toml create mode 100644 docs/testing_guide.md create mode 100644 internal/apiserver/docs/.gitkeep create mode 100644 internal/apiserver/handlers/home.go create mode 100644 internal/apiserver/handlers/home_test.go create mode 100644 internal/apiserver/logger/logger.go create mode 100644 internal/apiserver/middleware/logging.go create mode 100644 internal/apiserver/middleware/request_id.go create mode 100644 internal/apiserver/models/response.go create mode 100644 internal/apiserver/server/routes.go create mode 100644 internal/apiserver/server/server.go create mode 100644 internal/apiserver/services/.gitkeep create mode 100644 internal/apiserver/utils/.gitkeep delete mode 100644 internal/app/apiserver/apiserver.go delete mode 100644 internal/app/apiserver/server.go create mode 100644 internal/pkg/errors/.gitkeep create mode 100644 internal/pkg/validators/.gitkeep rename {internal/app/apiserver => pkg/http}/responsewriter.go (78%) create mode 100755 scripts/build.sh create mode 100755 scripts/test.sh diff --git a/.gitignore b/.gitignore index 002aacb..cfe3a2e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,13 @@ go.work.sum # Editor/IDE # .idea/ .vscode/ -!.vscode/settings.json \ No newline at end of file +!.vscode/settings.json + +# Builded files +apiserver + +!apiserver/ +# Swagger generated files (auto-generated from source code) +internal/apiserver/docs/swagger.json +internal/apiserver/docs/swagger.yaml +internal/apiserver/docs/docs.go \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4f80804 --- /dev/null +++ b/Makefile @@ -0,0 +1,147 @@ +# MyGoServer Makefile +# Удобные команды для разработки и тестирования + +.PHONY: help build test test-coverage test-race fmt vet clean docker-up docker-down + +# Default target +help: + @echo "🚀 MyGoServer Development Commands:" + @echo "" + @echo "📦 Build Commands:" + @echo " build - Build the application" + @echo " build-clean - Clean build and rebuild" + @echo "" + @echo "🧪 Test Commands:" + @echo " test - Run all tests" + @echo " test-unit - Run unit tests only" + @echo " test-coverage - Run tests with coverage report" + @echo " test-race - Run tests with race detector" + @echo " test-watch - Run tests in watch mode" + @echo "" + @echo "🔧 Code Quality:" + @echo " fmt - Format all Go code" + @echo " vet - Run static analysis" + @echo " lint - Run linting (if golangci-lint installed)" + @echo "" + @echo "🐳 Docker Commands:" + @echo " docker-up - Start application with Docker" + @echo " docker-down - Stop Docker containers" + @echo " docker-build - Build Docker image" + @echo "" + @echo "🧹 Cleanup:" + @echo " clean - Clean build artifacts" + @echo "" + +# Build commands +build: generate-docs + @echo "🏗️ Building application..." + go build -o apiserver ./cmd/apiserver + @echo "✅ Build completed: apiserver" + +generate-docs: + @echo "📖 Generating Swagger documentation..." + swag init -g cmd/apiserver/main.go -o internal/apiserver/docs + @echo "✅ Swagger documentation generated" + +build-clean: clean build + +# Test commands +test: + @echo "🧪 Running all tests..." + ./scripts/test.sh + +test-unit: + @echo "🧪 Running unit tests..." + go test -v ./internal/apiserver/... + +test-coverage: + @echo "📊 Running tests with coverage..." + go test -coverprofile=coverage.out ./... + go tool cover -html=coverage.out -o coverage.html + @echo "📊 Coverage report: coverage.html" + +test-race: + @echo "🏁 Running tests with race detector..." + go test -race ./... + +test-watch: + @echo "👀 Running tests in watch mode..." + @which gotest > /dev/null || (echo "Installing gotest-watcher..." && go install github.com/bitfield/gotest@latest) + gotest -w ./... + +# Code quality +fmt: + @echo "🎨 Formatting code..." + go fmt ./... + @echo "✅ Code formatted" + +vet: + @echo "🔍 Running static analysis..." + go vet ./... + @echo "✅ Static analysis completed" + +lint: + @echo "🧹 Running linter..." + @which golangci-lint > /dev/null || (echo "Installing golangci-lint..." && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest) + golangci-lint run + @echo "✅ Linting completed" + +# Docker commands +docker-up: + @echo "🐳 Starting application with Docker..." + docker-compose -f build/docker-compose.yml up -d + @echo "🚀 Application started on http://localhost:8080" + @echo "📖 Swagger UI: http://localhost:8080/swagger/" + +docker-down: + @echo "🛑 Stopping Docker containers..." + docker-compose -f build/docker-compose.yml down + +docker-build: + @echo "🔨 Building Docker image..." + docker build -f build/Dockerfile -t mygoserver:latest . + +# Cleanup +clean: + @echo "🧹 Cleaning build artifacts..." + rm -f apiserver coverage.out coverage.html + rm -rf build/bin/ + docker-compose -f build/docker-compose.yml down -v 2>/dev/null || true + @echo "✅ Cleanup completed" + +# Development helpers +dev: build + @echo "🚀 Starting development server..." + ./apiserver + +debug: build + @echo "🐛 Starting debug server..." + dlv --listen=:2345 --headless=true --api-version=2 exec ./apiserver + +# Run specific test patterns +test-pattern: + @if [ -z "$(PATTERN)" ]; then \ + echo "Usage: make test-pattern PATTERN=TestName"; \ + exit 1; \ + fi + @echo "🧪 Running tests matching: $(PATTERN)" + go test -v ./internal/apiserver/... -run="$(PATTERN)" + +# Install development tools +install-tools: + @echo "🛠️ Installing development tools..." + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + go install github.com/swaggo/swag/cmd/swag@latest + @echo "✅ Development tools installed" + +# Swagger documentation +swag-setup: + @echo "📖 Setting up Swagger documentation..." + @echo "⚠️ Installing swag tool..." + go install github.com/swaggo/swag/cmd/swag@latest + @echo "✅ swag installed" + @echo "📋 Initializing Swagger configuration..." + swag init -g cmd/apiserver/main.go -o internal/apiserver/docs + @echo "✅ Swagger documentation generated" + +.PHONY: help build test test-coverage test-race fmt vet clean docker-up docker-down swag-setup \ No newline at end of file diff --git a/build/Dockerfile b/build/Dockerfile new file mode 100644 index 0000000..1fb8ee8 --- /dev/null +++ b/build/Dockerfile @@ -0,0 +1,31 @@ +# Build stage +FROM golang:1.25-alpine AS builder + +WORKDIR /app + +# Copy go mod files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Build the application +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o apiserver ./cmd/apiserver + +# Final stage +FROM alpine:latest + +WORKDIR /root/ + +# Copy binary from builder stage +COPY --from=builder /app/apiserver . +COPY --from=builder /app/configs ./configs + +# Expose port +EXPOSE 8080 + +# Run the application +CMD ["./apiserver", "--config-path=./configs/server.toml"] \ No newline at end of file diff --git a/build/docker-compose.yml b/build/docker-compose.yml new file mode 100644 index 0000000..2218ec8 --- /dev/null +++ b/build/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + apiserver: + build: + context: .. + dockerfile: build/Dockerfile + container_name: mygoserver + ports: + - "8080:8080" + volumes: + - ../configs:/root/configs:ro + environment: + - LOGGING_LEVEL=info + - LOGGING_FORMAT=json + - LOGGING_OUTPUT=stdout + restart: unless-stopped + networks: + - app-network + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/cmd/apiserver/config/config.go b/cmd/apiserver/config/config.go new file mode 100644 index 0000000..37dfc93 --- /dev/null +++ b/cmd/apiserver/config/config.go @@ -0,0 +1,78 @@ +package config + +import ( + "flag" + "log" + "time" + + "github.com/BurntSushi/toml" +) + +type Server struct { + Address string `toml:"address"` + ReadTimeout time.Duration `toml:"read_timeout"` + WriteTimeout time.Duration `toml:"write_timeout"` + IdleTimeout time.Duration `toml:"idle_timeout"` +} + +type Logging struct { + Level string `toml:"level"` + Format string `toml:"format"` + Output string `toml:"output"` +} + +type Request struct { + MaxBodySize int64 `toml:"max_body_size"` +} + +type Config struct { + Server Server `toml:"server"` + Logging Logging `toml:"logging"` + Request Request `toml:"request"` +} + +var ( + configPath string +) + +func init() { + flag.StringVar(&configPath, "config-path", "./configs/development.toml", "path to config") +} + +func Load() *Config { + flag.Parse() + + var config Config + + if _, err := toml.DecodeFile(configPath, &config); err != nil { + log.Fatalf("Failed to load config: %v", err) + } + + // Set defaults if not specified + if config.Server.Address == "" { + config.Server.Address = ":8080" + } + if config.Server.ReadTimeout == 0 { + config.Server.ReadTimeout = 10 * time.Second + } + if config.Server.WriteTimeout == 0 { + config.Server.WriteTimeout = 10 * time.Second + } + if config.Server.IdleTimeout == 0 { + config.Server.IdleTimeout = 60 * time.Second + } + if config.Logging.Level == "" { + config.Logging.Level = "info" + } + if config.Logging.Format == "" { + config.Logging.Format = "json" + } + if config.Logging.Output == "" { + config.Logging.Output = "stdout" + } + if config.Request.MaxBodySize == 0 { + config.Request.MaxBodySize = 10 * 1024 * 1024 // 10MB + } + + return &config +} diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index 9f94faa..8de1f24 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -1,23 +1,31 @@ package main import ( - "flag" "log" - "git.nwaifu.su/sergey/MyGoServer/internal/app/apiserver" + "git.nwaifu.su/sergey/MyGoServer/cmd/apiserver/config" + "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/server" ) -var ( - configPath string -) +// @title Some GoLang server +// @version 1.0 +// @description This is some GoLang server. +// @termsOfService http://swagger.io/terms/ -func init() { - flag.StringVar(&configPath, "config-path", "/go/bin/configs/server.toml", "path to config") -} +// @contact.name Sergey Elpashev +// @contact.url https://nwaifu.su +// @contact.email mrsedan@nwaifu.su +// @license.name MIT +// @license.url https://opensource.org/license/mit + +// @host localhost:8080 +// @BasePath / func main() { - flag.Parse() - if err := apiserver.Start(); err != nil { + cfg := config.Load() + + srv := server.NewServer(cfg) + if err := srv.Start(); err != nil { log.Fatal(err) } } diff --git a/configs/development.toml b/configs/development.toml new file mode 100644 index 0000000..904a1f5 --- /dev/null +++ b/configs/development.toml @@ -0,0 +1,16 @@ +# Development server configuration +[server] +address = ":8080" +read_timeout = "5s" +write_timeout = "5s" +idle_timeout = "30s" + +# Logging configuration for development +[logging] +level = "debug" +format = "text" +output = "stdout" + +# Request configuration +[request] +max_body_size = 10485760 # 10MB \ No newline at end of file diff --git a/configs/server.toml b/configs/server.toml new file mode 100644 index 0000000..bd19a84 --- /dev/null +++ b/configs/server.toml @@ -0,0 +1,16 @@ +# Server configuration +[server] +address = ":8080" +read_timeout = "10s" +write_timeout = "10s" +idle_timeout = "60s" + +# Logging configuration +[logging] +level = "info" +format = "json" +output = "stdout" + +# Request configuration +[request] +max_body_size = 10485760 # 10MB \ No newline at end of file diff --git a/docs/testing_guide.md b/docs/testing_guide.md new file mode 100644 index 0000000..9f77595 --- /dev/null +++ b/docs/testing_guide.md @@ -0,0 +1,206 @@ +# 🧪 Руководство по тестированию MyGoServer + +## Быстрый старт + +### Способы запуска тестов: + +1. **Через Makefile (рекомендуется)**: + ```bash + make test # Все тесты + make test-unit # Только unit тесты + make test-coverage # Тесты с покрытием + make test-race # С race detector + ``` + +2. **Через скрипты**: + ```bash + ./scripts/test.sh # Все тесты с отчетом + ./scripts/test.sh "TestName" # Конкретные тесты + ``` + +3. **Ручной запуск**: + ```bash + go test ./... # Все тесты + go test -v ./internal/apiserver/... # Verbose вывод + go test -race ./... # С race detector + ``` + +## Детальное руководство + +### 📊 Покрытие кода (Coverage) + +```bash +# Генерация отчета о покрытии +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out -o coverage.html + +# Просмотр покрытия в терминале +go tool cover -func=coverage.out +``` + +### 🏁 Race Detector + +Поиск гонок данных (data races): + +```bash +go test -race ./... + +# Для более глубокой проверки +go test -race -count=10 ./... +``` + +### 📈 Benchmark тесты + +Для измерения производительности: + +```bash +go test -bench=. -benchmem ./... + +# Для конкретных функций +go test -bench=BenchmarkHomeHandler -benchmem ./internal/apiserver/... +``` + +### 🎯 Запуск по паттернам + +```bash +# Только тесты с определенным именем +go test -run TestHome ./... + +# Исключить некоторые тесты +go test -run '^(?!TestIntegration).*' ./... + +# Искать по нескольким паттернам +go test -run 'TestHome|TestConfig' ./... +``` + +### 🔄 Непрерывное тестирование + +Для разработки с автоперезапуском тестов: + +```bash +# Установка gotest-watcher +go install github.com/bitfield/gotest@latest + +# Автоматический запуск тестов при изменении файлов +gotest -w ./... +``` + +### 📊 Отчеты и анализ + +#### HTML отчет о покрытии: +- `coverage.html` - веб-интерфейс покрытия +- `coverage.out` - данные покрытия + +#### JSON отчет (для CI/CD): +```bash +go test -json ./... > test-report.json +``` + +#### Логи в файл: +```bash +go test -v ./... > test.log 2>&1 +``` + +## 🏗️ Структура тестов + +### Unit тесты: +``` +internal/apiserver/handlers/ +├── home_test.go +├── middleware_test.go +└── ... + +internal/apiserver/models/ +├── response_test.go +└── ... +``` + +### Integration тесты: +``` +test/integration/ +├── api_test.go +├── server_test.go +└── ... +``` + +### Файлы конфигурации для тестов: +``` +test/ +├── fixtures/ # Тестовые данные +├── helpers/ # Вспомогательные функции +└── testdata/ # Статические данные +``` + +## 🔧 Настройка окружения тестирования + +### Переменные окружения: +```bash +export TEST_ENV=development +export LOG_LEVEL=debug +export CONFIG_PATH=./configs/development.toml +``` + +## 📝 Примеры тестов + +### Базовый тест: +```go +func TestHomeHandler_ServeHTTP(t *testing.T) { + handler := handlers.NewHomeHandler() + + req := httptest.NewRequest("GET", "/", nil) + rr := httptest.NewRecorder() + + handler.ServeHTTP(rr, req) + + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } +} +``` + +### Тест с моками: +```go +func TestConfig_Load(t *testing.T) { + // Мок конфигурации + cfg := &Config{ + Server: ServerConfig{ + Address: ":8080", + }, + } + + if cfg.Server.Address != ":8080" { + t.Error("Config not loaded correctly") + } +} +``` + +### Benchmark тест: +```go +func BenchmarkHomeHandler_ServeHTTP(b *testing.B) { + handler := handlers.NewHomeHandler() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + req := httptest.NewRequest("GET", "/", nil) + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + } +} +``` + +## 🚀 Быстрые команды + +```bash +# Проверка всех аспектов качества кода +make test-coverage && make vet && make fmt + +# Быстрый цикл разработки +make test-watch + +# Полная проверка проекта +make build && make test-race && make lint + +# Тестирование конкретного компонента +make test-pattern PATTERN=TestHome +``` \ No newline at end of file diff --git a/go.mod b/go.mod index 2e061bd..b8c31ec 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,28 @@ go 1.25.4 require ( github.com/felixge/httpsnoop v1.0.4 - github.com/gorilla/mux v1.8.1 github.com/sirupsen/logrus v1.9.3 ) require ( + github.com/BurntSushi/toml v1.5.0 github.com/google/uuid v1.6.0 - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + github.com/swaggo/http-swagger v1.3.4 + github.com/swaggo/swag v1.16.6 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index cfed775..425fca2 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,78 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= +github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/apiserver/docs/.gitkeep b/internal/apiserver/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/apiserver/handlers/home.go b/internal/apiserver/handlers/home.go new file mode 100644 index 0000000..8db805b --- /dev/null +++ b/internal/apiserver/handlers/home.go @@ -0,0 +1,37 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/models" +) + +// HomeHandler handles the root endpoint +type HomeHandler struct{} + +func NewHomeHandler() *HomeHandler { + return &HomeHandler{} +} + +// ServeHTTP implements http.Handler +func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.handleHome(w, r) +} + +// @Summary Health check +// @Description Проверка состояния сервера +// @Tags Health +// @Success 200 {object} models.Response "Server is running" +// @Router / [get] +func (h *HomeHandler) handleHome(w http.ResponseWriter, r *http.Request) { + // Create a simple success response + response := models.NewSuccessResponse(map[string]string{ + "status": "ok", + "message": "API Server is running", + }) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} diff --git a/internal/apiserver/handlers/home_test.go b/internal/apiserver/handlers/home_test.go new file mode 100644 index 0000000..3535d5a --- /dev/null +++ b/internal/apiserver/handlers/home_test.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestHomeHandler_ServeHTTP(t *testing.T) { + // Create a new home handler + handler := NewHomeHandler() + + // Create a test request + req := httptest.NewRequest("GET", "/", nil) + + // Create a test response recorder + rr := httptest.NewRecorder() + + // Call the handler + handler.ServeHTTP(rr, req) + + // Check status code + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check content type + if ct := rr.Header().Get("Content-Type"); ct != "application/json" { + t.Errorf("handler returned wrong content type: got %v want %v", + ct, "application/json") + } + + // Check response body contains expected data + expectedBody := `"success":true` + if body := rr.Body.String(); !strings.Contains(body, expectedBody) { + t.Errorf("handler returned unexpected body: got %v want %v", + body, expectedBody) + } +} diff --git a/internal/apiserver/logger/logger.go b/internal/apiserver/logger/logger.go new file mode 100644 index 0000000..8dd9941 --- /dev/null +++ b/internal/apiserver/logger/logger.go @@ -0,0 +1,76 @@ +package logger + +import ( + "sync" + + "github.com/sirupsen/logrus" +) + +var ( + logger *logrus.Logger + once sync.Once + initialized bool +) + +// Initialize logger with configuration +func Initialize(level string, format string, output string) { + once.Do(func() { + logger = logrus.New() + + // Set level + lvl, err := logrus.ParseLevel(level) + if err != nil { + lvl = logrus.InfoLevel + } + logger.SetLevel(lvl) + + // Set format + switch format { + case "json": + logger.SetFormatter(&logrus.JSONFormatter{ + TimestampFormat: "2006-01-02T15:04:05.000Z", + }) + case "text": + logger.SetFormatter(&logrus.TextFormatter{ + TimestampFormat: "2006-01-02 15:04:05", + FullTimestamp: true, + }) + } + + // Set output + if output != "" { + //TODO: Use files + } + + initialized = true + }) +} + +// GetLogger returns the singleton logger instance +func GetLogger() *logrus.Logger { + if !initialized { + // Initialize with defaults + Initialize("info", "json", "stdout") + } + return logger +} + +// WithFields creates a logger with additional fields +func WithFields(fields map[string]interface{}) *logrus.Entry { + return GetLogger().WithFields(fields) +} + +// Info logs an info message +func Info(msg string) { + GetLogger().Info(msg) +} + +// Error logs an error message +func Error(msg string) { + GetLogger().Error(msg) +} + +// Debug logs a debug message +func Debug(msg string) { + GetLogger().Debug(msg) +} diff --git a/internal/apiserver/middleware/logging.go b/internal/apiserver/middleware/logging.go new file mode 100644 index 0000000..4e8576a --- /dev/null +++ b/internal/apiserver/middleware/logging.go @@ -0,0 +1,42 @@ +package middleware + +import ( + "context" + "net/http" + "time" + + "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger" + rhttp "git.nwaifu.su/sergey/MyGoServer/pkg/http" +) + +// LoggingMiddleware logs HTTP requests +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + // Use the custom response writer to capture status code + rw := rhttp.NewResponseWriter(w) + + next.ServeHTTP(rw, r) + + // Log the request + logger := logger.GetLogger() + logger.WithFields(map[string]interface{}{ + "method": r.Method, + "uri": r.RequestURI, + "remote_addr": r.RemoteAddr, + "user_agent": r.UserAgent(), + "status_code": rw.GetStatusCode(), + "duration": time.Since(start).String(), + "request_id": getRequestID(r.Context()), + }).Info("HTTP request") + }) +} + +// getRequestID retrieves request ID from context +func getRequestID(ctx context.Context) string { + if id, ok := ctx.Value("request_id").(string); ok { + return id + } + return "" +} diff --git a/internal/apiserver/middleware/request_id.go b/internal/apiserver/middleware/request_id.go new file mode 100644 index 0000000..0f8c0ca --- /dev/null +++ b/internal/apiserver/middleware/request_id.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "context" + "net/http" + + "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger" + "github.com/google/uuid" +) + +// RequestIDMiddleware adds a unique request ID to each request +func RequestIDMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Generate a new request ID + requestID := uuid.New().String() + + // Set request ID in response header + w.Header().Set("X-Request-ID", requestID) + + // Add request ID to context + ctx := r.Context() + ctx = context.WithValue(ctx, "request_id", requestID) + + // Log the request ID assignment + logger := logger.GetLogger() + logger.WithFields(map[string]interface{}{ + "request_id": requestID, + }).Debug("Request ID assigned") + + // Continue with the request + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} diff --git a/internal/apiserver/models/response.go b/internal/apiserver/models/response.go new file mode 100644 index 0000000..9d6dfb4 --- /dev/null +++ b/internal/apiserver/models/response.go @@ -0,0 +1,25 @@ +package models + +// Response represents a standard API response +type Response struct { + Success bool `json:"success"` + Data interface{} `json:"data,omitempty"` + Error string `json:"error,omitempty"` + RequestID string `json:"request_id,omitempty"` +} + +// NewSuccessResponse creates a successful response +func NewSuccessResponse(data interface{}) *Response { + return &Response{ + Success: true, + Data: data, + } +} + +// NewErrorResponse creates an error response +func NewErrorResponse(err error) *Response { + return &Response{ + Success: false, + Error: err.Error(), + } +} diff --git a/internal/apiserver/server/routes.go b/internal/apiserver/server/routes.go new file mode 100644 index 0000000..ecc4196 --- /dev/null +++ b/internal/apiserver/server/routes.go @@ -0,0 +1,22 @@ +package server + +import ( + _ "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/docs" + "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/handlers" + "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/middleware" + + httpSwagger "github.com/swaggo/http-swagger" +) + +// setupRoutes configures all routes +func (s *Server) setupRoutes() { + // Add request ID middleware to all routes + s.router.Handle("/", middleware.RequestIDMiddleware( + middleware.LoggingMiddleware( + handlers.NewHomeHandler(), + ), + )) + + // Swagger UI + s.router.Handle("/swagger/", httpSwagger.WrapHandler) +} diff --git a/internal/apiserver/server/server.go b/internal/apiserver/server/server.go new file mode 100644 index 0000000..ea30ecb --- /dev/null +++ b/internal/apiserver/server/server.go @@ -0,0 +1,70 @@ +package server + +import ( + "context" + "net" + "net/http" + + "git.nwaifu.su/sergey/MyGoServer/cmd/apiserver/config" + "git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger" +) + +type contextKey struct { + key string +} + +var connContextKey = &contextKey{"http-conn"} + +func saveConnInContext(ctx context.Context, c net.Conn) context.Context { + return context.WithValue(ctx, connContextKey, c) +} + +// Server represents the HTTP server +type Server struct { + config *config.Config + router *http.ServeMux + server *http.Server +} + +// NewServer creates a new server instance +func NewServer(cfg *config.Config) *Server { + s := &Server{ + config: cfg, + } + + // Initialize logger + logger.Initialize(cfg.Logging.Level, cfg.Logging.Format, cfg.Logging.Output) + + // Create router + s.router = http.NewServeMux() + s.setupRoutes() + + // Create HTTP server + s.server = &http.Server{ + Addr: cfg.Server.Address, + Handler: s.router, + ConnContext: saveConnInContext, + } + + return s +} + +// Start starts the server +func (s *Server) Start() error { + return s.server.ListenAndServe() +} + +// GetRouter returns the HTTP router +func (s *Server) GetRouter() *http.ServeMux { + return s.router +} + +// GetServer returns the HTTP server instance +func (s *Server) GetServer() *http.Server { + return s.server +} + +// GetConfig returns the server configuration +func (s *Server) GetConfig() *config.Config { + return s.config +} diff --git a/internal/apiserver/services/.gitkeep b/internal/apiserver/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/apiserver/utils/.gitkeep b/internal/apiserver/utils/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/app/apiserver/apiserver.go b/internal/app/apiserver/apiserver.go deleted file mode 100644 index 50fba4f..0000000 --- a/internal/app/apiserver/apiserver.go +++ /dev/null @@ -1,29 +0,0 @@ -package apiserver - -import ( - "context" - "net" - "net/http" -) - -type contextKey struct { - key string -} - -var connContextKey = &contextKey{"http-conn"} - -func saveConnInContext(ctx context.Context, c net.Conn) context.Context { - return context.WithValue(ctx, connContextKey, c) -} - -// Start the server -func Start() error { - srv := newServer() - server := http.Server{ - Addr: ":8080", - ConnContext: saveConnInContext, - Handler: srv, - } - - return server.ListenAndServe() -} diff --git a/internal/app/apiserver/server.go b/internal/app/apiserver/server.go deleted file mode 100644 index 2125ade..0000000 --- a/internal/app/apiserver/server.go +++ /dev/null @@ -1,81 +0,0 @@ -package apiserver - -import ( - "context" - "encoding/json" - "net/http" - "time" - - "github.com/google/uuid" - "github.com/gorilla/mux" - "github.com/sirupsen/logrus" -) - -const ( - ctxKeyRequestID = iota -) - -type server struct { - router *mux.Router - logger *logrus.Logger -} - -func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - s.router.ServeHTTP(w, r) -} - -func newServer() *server { - s := &server{ - router: mux.NewRouter(), - logger: logrus.New(), - } - s.configureRouter() - return s -} - -func (s *server) configureRouter() { - s.router.Use(s.setRequestID) - s.router.Use(s.logRequest) - s.router.HandleFunc("/", s.handleHome()) -} - -func (s *server) handleHome() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("ok")) - } -} - -func (s *server) logRequest(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - logger := logrus.WithFields(logrus.Fields{ - "remote_addr": r.RemoteAddr, - "request_id": r.Context().Value(ctxKeyRequestID), - }) - logger.Infof("started %s %s", r.Method, r.RequestURI) - start := time.Now() - rw := newResponseWriter(w) - rw.code = http.StatusOK - next.ServeHTTP(rw, r) - logger.Infof("completed with %d %s in %v", rw.code, http.StatusText(rw.code), time.Since(start)) - }) -} - -func (s *server) setRequestID(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - id := uuid.New().String() - w.Header().Set("X-Request-ID", id) - next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), ctxKeyRequestID, id))) - }) -} - -// Helpers -func (s *server) error(w http.ResponseWriter, r *http.Request, status int, err error) { - s.respond(w, r, status, map[string]string{"error": err.Error()}) -} - -func (s *server) respond(w http.ResponseWriter, r *http.Request, status int, data interface{}) { - w.WriteHeader(status) - if data != nil { - json.NewEncoder(w).Encode(data) - } -} diff --git a/internal/pkg/errors/.gitkeep b/internal/pkg/errors/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/pkg/validators/.gitkeep b/internal/pkg/validators/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/app/apiserver/responsewriter.go b/pkg/http/responsewriter.go similarity index 78% rename from internal/app/apiserver/responsewriter.go rename to pkg/http/responsewriter.go index 1ad3ae5..b6edf72 100644 --- a/internal/app/apiserver/responsewriter.go +++ b/pkg/http/responsewriter.go @@ -1,4 +1,4 @@ -package apiserver +package http import ( "bufio" @@ -22,8 +22,13 @@ func (w *ResponseWriter) WriteHeader(statusCode int) { w.ResponseWriter.WriteHeader(statusCode) } +// GetStatusCode returns the status code that was written +func (w *ResponseWriter) GetStatusCode() int { + return w.code +} + // Get new RW -func newResponseWriter(w http.ResponseWriter) *ResponseWriter { +func NewResponseWriter(w http.ResponseWriter) *ResponseWriter { hijacker, _ := w.(http.Hijacker) return &ResponseWriter{ ResponseWriter: httpsnoop.Wrap(w, httpsnoop.Hooks{}), diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..ea19f5b --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +echo "Building MyGoServer..." + +# Create build directory +mkdir -p build/bin + +# Build the binary +echo "Building apiserver binary..." +go build -o build/bin/apiserver ./cmd/apiserver + +echo "Build completed successfully!" +echo "Binary location: build/bin/apiserver" \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..27f6d61 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +set -e + +echo "🧪 Running tests for MyGoServer..." + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + local status=$1 + local message=$2 + if [ "$status" = "PASS" ]; then + echo -e "${GREEN}✅ $message${NC}" + elif [ "$status" = "FAIL" ]; then + echo -e "${RED}❌ $message${NC}" + else + echo -e "${YELLOW}⚠️ $message${NC}" + fi +} + +# 1. Run all unit tests +echo -e "${YELLOW}Running all unit tests...${NC}" +go test -v ./internal/apiserver/... + +if [ $? -eq 0 ]; then + print_status "PASS" "Unit tests passed" +else + print_status "FAIL" "Unit tests failed" + exit 1 +fi + +# 2. Run tests with coverage +echo -e "${YELLOW}Running tests with coverage...${NC}" +go test -coverprofile=coverage.out ./... + +if [ $? -eq 0 ]; then + print_status "PASS" "Coverage tests completed" + + # Generate coverage report + go tool cover -html=coverage.out -o coverage.html 2>/dev/null || true + if [ -f "coverage.html" ]; then + echo -e "${GREEN}📊 Coverage report generated: coverage.html${NC}" + fi + + # Show coverage percentage + go tool cover -func=coverage.out | tail -1 +else + print_status "FAIL" "Coverage tests failed" +fi + +# 3. Run tests with race detector (optional, takes longer) +echo -e "${YELLOW}Running race detector tests...${NC}" +echo "This may take longer - press Ctrl+C to skip, or wait..." +sleep 2 + +if go test -race ./internal/apiserver/... > /dev/null 2>&1; then + print_status "PASS" "Race detector tests passed" +else + print_status "WARN" "Race detector tests completed (some may have warnings)" +fi + +# 4. Run vet (static analysis) +echo -e "${YELLOW}Running go vet (static analysis)...${NC}" +go vet ./... + +if [ $? -eq 0 ]; then + print_status "PASS" "Static analysis passed" +else + print_status "WARN" "Static analysis completed with warnings" +fi + +# 5. Check formatting +echo -e "${YELLOW}Checking code formatting...${NC}" +if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then + print_status "FAIL" "Code formatting issues found:" + gofmt -s -l . + echo "Run 'gofmt -s -w .' to fix formatting" + exit 1 +else + print_status "PASS" "Code formatting is correct" +fi + +# 6. Run specific test patterns (if specified) +if [ "$1" != "" ]; then + echo -e "${YELLOW}Running specific tests: $1${NC}" + go test -v ./internal/apiserver/... -run="$1" +fi + +echo -e "${GREEN}🎉 All tests completed successfully!${NC}" +echo "" +echo "📝 Usage examples:" +echo " ./scripts/test.sh # Run all tests" +echo " ./scripts/test.sh \"TestHome\" # Run specific test pattern" +echo " go test ./... # Run all tests manually" +echo " go test -v ./internal/apiserver/ # Run specific package tests" +echo " go test -race ./... # Run with race detector" \ No newline at end of file