feat: some refactoring

This commit is contained in:
2025-11-22 22:23:49 +03:00
parent a247e6213e
commit 51f30b516e
29 changed files with 1094 additions and 129 deletions

9
.gitignore vendored
View File

@@ -31,3 +31,12 @@ go.work.sum
# .idea/
.vscode/
!.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

147
Makefile Normal file
View File

@@ -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

31
build/Dockerfile Normal file
View File

@@ -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"]

23
build/docker-compose.yml Normal file
View File

@@ -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

View File

@@ -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
}

View File

@@ -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)
}
}

16
configs/development.toml Normal file
View File

@@ -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

16
configs/server.toml Normal file
View File

@@ -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

206
docs/testing_guide.md Normal file
View File

@@ -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
```

21
go.mod
View File

@@ -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
)

65
go.sum
View File

@@ -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=

View File

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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 ""
}

View File

@@ -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))
})
}

View File

@@ -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(),
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

View File

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

View File

View File

@@ -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{}),

15
scripts/build.sh Executable file
View File

@@ -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"

101
scripts/test.sh Executable file
View File

@@ -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"