Compare commits

..

7 Commits

Author SHA1 Message Date
04f1519f2c Merge branch 'dev' 2025-11-22 23:53:07 +03:00
132945342f feat: fixed lint errors 2025-11-22 23:52:35 +03:00
80ff5c501d fix: inderect require 2025-11-22 23:32:39 +03:00
659369c42b feat: ServerMux to gorilla/mux, time route 2025-11-22 23:25:00 +03:00
fe0bd0dddc feat: added license 2025-11-22 22:27:59 +03:00
16a6b5ed94 Merge branch 'dev-ai' 2025-11-22 22:26:39 +03:00
51f30b516e feat: some refactoring 2025-11-22 22:23:49 +03:00
32 changed files with 1306 additions and 127 deletions

11
.gitignore vendored
View File

@@ -30,4 +30,13 @@ go.work.sum
# Editor/IDE
# .idea/
.vscode/
!.vscode/settings.json
!.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

9
LICENSE.md Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2025 Sergey Elpashev
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

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,30 @@
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.
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
```

22
go.mod
View File

@@ -4,11 +4,29 @@ 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/gorilla/mux v1.8.1
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
)

63
go.sum
View File

@@ -1,21 +1,80 @@
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,40 @@
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)
if err := json.NewEncoder(w).Encode(response); err != nil {
// Handle encoding error - we can't write an error response after headers
return
}
}

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,46 @@
// It's just test file. I'll remove it later
package handlers
import (
"encoding/json"
"net/http"
"time"
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/models"
)
// TimeHandler handles the time endpoint
type TimeHandler struct{}
func NewTimeHandler() *TimeHandler {
return &TimeHandler{}
}
// ServeHTTP implements http.Handler
func (h *TimeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handleTime(w, r)
}
// @Summary Get server time
// @Description Возвращает текущее серверное время в формате ISO 8601
// @Tags Time
// @Success 200 {object} models.Response "Текущее время сервера"
// @Router /time [get]
func (h *TimeHandler) handleTime(w http.ResponseWriter, r *http.Request) {
// Get current time in UTC
currentTime := time.Now().UTC()
// Create response with time data
response := models.NewSuccessResponse(map[string]interface{}{
"server_time": currentTime.Format(time.RFC3339),
"unix_timestamp": currentTime.Unix(),
"timezone": "UTC",
})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
// Handle encoding error - we can't write an error response after headers
return
}
}

View File

@@ -0,0 +1,149 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/models"
)
func TestTimeHandler_ServeHTTP(t *testing.T) {
// Create a new time handler
handler := NewTimeHandler()
// Create a test request
req := httptest.NewRequest("GET", "/time", 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 structure
var response models.Response
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
t.Errorf("handler returned invalid JSON: %v", err)
return
}
// Check success field
if !response.Success {
t.Errorf("handler returned success=false, want success=true")
}
// Check that data exists and is a map
if response.Data == nil {
t.Errorf("handler returned nil data")
return
}
// Type assert to map[string]interface{}
dataMap, ok := response.Data.(map[string]interface{})
if !ok {
t.Errorf("handler returned data of unexpected type")
return
}
// Check required fields exist
requiredFields := []string{"server_time", "unix_timestamp", "timezone"}
for _, field := range requiredFields {
if _, exists := dataMap[field]; !exists {
t.Errorf("handler response missing required field: %s", field)
}
}
// Check timezone is UTC
if timezone, ok := dataMap["timezone"].(string); ok {
if timezone != "UTC" {
t.Errorf("handler returned wrong timezone: got %v want %v",
timezone, "UTC")
}
} else {
t.Errorf("handler returned timezone of unexpected type")
}
// Check server_time format (should be RFC3339)
if serverTime, ok := dataMap["server_time"].(string); ok {
if _, err := time.Parse(time.RFC3339, serverTime); err != nil {
t.Errorf("handler returned invalid server_time format: got %v, error: %v",
serverTime, err)
}
} else {
t.Errorf("handler returned server_time of unexpected type")
}
// Check unix_timestamp is a number
if unixTimestamp, ok := dataMap["unix_timestamp"].(float64); ok {
// Verify it's a reasonable timestamp (should be close to current time)
now := time.Now().UTC().Unix()
// Allow 10 seconds difference for test execution time
if diff := now - int64(unixTimestamp); diff > 10 || diff < -10 {
t.Errorf("handler returned unreasonable unix_timestamp: got %v, current time: %v",
int64(unixTimestamp), now)
}
} else {
// Try as string and convert
if unixTimestampStr, ok := dataMap["unix_timestamp"].(string); ok {
if _, err := strconv.ParseInt(unixTimestampStr, 10, 64); err != nil {
t.Errorf("handler returned unix_timestamp of unexpected format: %v", err)
}
} else {
t.Errorf("handler returned unix_timestamp of unexpected type")
}
}
}
func TestTimeHandler_ResponseStructure(t *testing.T) {
handler := NewTimeHandler()
req := httptest.NewRequest("GET", "/time", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
// Test that response is valid JSON and contains all expected top-level fields
var response models.Response
if err := json.Unmarshal(rr.Body.Bytes(), &response); err != nil {
t.Fatalf("Response is not valid JSON: %v", err)
}
// Test response structure
if response.Success != true {
t.Errorf("Expected success=true, got success=%v", response.Success)
}
if response.Error != "" {
t.Errorf("Expected no error, got error=%v", response.Error)
}
if response.Data == nil {
t.Errorf("Expected data to be present, got nil")
}
// Test that response body contains JSON structure
body := rr.Body.String()
if !strings.Contains(body, `"success":true`) {
t.Errorf("Response body missing success field: %s", body)
}
if !strings.Contains(body, `"data"`) {
t.Errorf("Response body missing data field: %s", body)
}
}

View File

@@ -0,0 +1,74 @@
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 (currently not implemented)
// TODO: Implement file output support
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,38 @@
package middleware
import (
"context"
"net/http"
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger"
"github.com/google/uuid"
)
// requestIDKey is a custom type for context key to avoid collisions
type requestIDKey struct{}
var _ requestIDKey
// 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 using custom type
ctx := r.Context()
ctx = context.WithValue(ctx, requestIDKey{}, 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,23 @@
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() {
// Apply global middleware to all routes
s.router.Use(middleware.RequestIDMiddleware)
s.router.Use(middleware.LoggingMiddleware)
// Register routes
s.router.Handle("/", handlers.NewHomeHandler()).Methods("GET")
s.router.Handle("/time", handlers.NewTimeHandler()).Methods("GET")
// Swagger UI (no middleware needed)
s.router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler)
}

View File

@@ -0,0 +1,71 @@
package server
import (
"context"
"net"
"net/http"
"git.nwaifu.su/sergey/MyGoServer/cmd/apiserver/config"
"git.nwaifu.su/sergey/MyGoServer/internal/apiserver/logger"
"github.com/gorilla/mux"
)
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 *mux.Router
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 = mux.NewRouter()
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() *mux.Router {
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"