Merge branch 'dev-ai'
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -30,4 +30,13 @@ go.work.sum
|
|||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
.vscode/
|
.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
|
||||||
147
Makefile
Normal file
147
Makefile
Normal 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
31
build/Dockerfile
Normal 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
23
build/docker-compose.yml
Normal 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
|
||||||
78
cmd/apiserver/config/config.go
Normal file
78
cmd/apiserver/config/config.go
Normal 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
|
||||||
|
}
|
||||||
@@ -1,23 +1,31 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"log"
|
"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 (
|
// @title Some GoLang server
|
||||||
configPath string
|
// @version 1.0
|
||||||
)
|
// @description This is some GoLang server.
|
||||||
|
// @termsOfService http://swagger.io/terms/
|
||||||
|
|
||||||
func init() {
|
// @contact.name Sergey Elpashev
|
||||||
flag.StringVar(&configPath, "config-path", "/go/bin/configs/server.toml", "path to config")
|
// @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() {
|
func main() {
|
||||||
flag.Parse()
|
cfg := config.Load()
|
||||||
if err := apiserver.Start(); err != nil {
|
|
||||||
|
srv := server.NewServer(cfg)
|
||||||
|
if err := srv.Start(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
configs/development.toml
Normal file
16
configs/development.toml
Normal 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
16
configs/server.toml
Normal 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
206
docs/testing_guide.md
Normal 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
21
go.mod
@@ -4,11 +4,28 @@ go 1.25.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/felixge/httpsnoop v1.0.4
|
github.com/felixge/httpsnoop v1.0.4
|
||||||
github.com/gorilla/mux v1.8.1
|
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.5.0
|
||||||
github.com/google/uuid v1.6.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
65
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
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/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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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.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/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-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=
|
||||||
|
|||||||
0
internal/apiserver/docs/.gitkeep
Normal file
0
internal/apiserver/docs/.gitkeep
Normal file
37
internal/apiserver/handlers/home.go
Normal file
37
internal/apiserver/handlers/home.go
Normal 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)
|
||||||
|
}
|
||||||
41
internal/apiserver/handlers/home_test.go
Normal file
41
internal/apiserver/handlers/home_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
76
internal/apiserver/logger/logger.go
Normal file
76
internal/apiserver/logger/logger.go
Normal 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)
|
||||||
|
}
|
||||||
42
internal/apiserver/middleware/logging.go
Normal file
42
internal/apiserver/middleware/logging.go
Normal 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 ""
|
||||||
|
}
|
||||||
33
internal/apiserver/middleware/request_id.go
Normal file
33
internal/apiserver/middleware/request_id.go
Normal 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))
|
||||||
|
})
|
||||||
|
}
|
||||||
25
internal/apiserver/models/response.go
Normal file
25
internal/apiserver/models/response.go
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
22
internal/apiserver/server/routes.go
Normal file
22
internal/apiserver/server/routes.go
Normal 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)
|
||||||
|
}
|
||||||
70
internal/apiserver/server/server.go
Normal file
70
internal/apiserver/server/server.go
Normal 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
|
||||||
|
}
|
||||||
0
internal/apiserver/services/.gitkeep
Normal file
0
internal/apiserver/services/.gitkeep
Normal file
0
internal/apiserver/utils/.gitkeep
Normal file
0
internal/apiserver/utils/.gitkeep
Normal 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()
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
internal/pkg/errors/.gitkeep
Normal file
0
internal/pkg/errors/.gitkeep
Normal file
0
internal/pkg/validators/.gitkeep
Normal file
0
internal/pkg/validators/.gitkeep
Normal file
@@ -1,4 +1,4 @@
|
|||||||
package apiserver
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -22,8 +22,13 @@ func (w *ResponseWriter) WriteHeader(statusCode int) {
|
|||||||
w.ResponseWriter.WriteHeader(statusCode)
|
w.ResponseWriter.WriteHeader(statusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetStatusCode returns the status code that was written
|
||||||
|
func (w *ResponseWriter) GetStatusCode() int {
|
||||||
|
return w.code
|
||||||
|
}
|
||||||
|
|
||||||
// Get new RW
|
// Get new RW
|
||||||
func newResponseWriter(w http.ResponseWriter) *ResponseWriter {
|
func NewResponseWriter(w http.ResponseWriter) *ResponseWriter {
|
||||||
hijacker, _ := w.(http.Hijacker)
|
hijacker, _ := w.(http.Hijacker)
|
||||||
return &ResponseWriter{
|
return &ResponseWriter{
|
||||||
ResponseWriter: httpsnoop.Wrap(w, httpsnoop.Hooks{}),
|
ResponseWriter: httpsnoop.Wrap(w, httpsnoop.Hooks{}),
|
||||||
15
scripts/build.sh
Executable file
15
scripts/build.sh
Executable 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
101
scripts/test.sh
Executable 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"
|
||||||
Reference in New Issue
Block a user