diff --git a/.github/workflows/branch_merges.yml b/.github/workflows/branch_merges.yml index 5abbe0f..3d38ffd 100644 --- a/.github/workflows/branch_merges.yml +++ b/.github/workflows/branch_merges.yml @@ -54,11 +54,11 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '1.25' - cache: true + go-version: '1.25.4' + cache: false - name: Run golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v9 with: #install-mode: goinstall version: latest @@ -74,7 +74,7 @@ jobs: strategy: matrix: - go-version: ['1.25'] + go-version: ['1.25.4'] os: [ubuntu-latest] steps: @@ -91,7 +91,7 @@ jobs: - name: Run comprehensive tests run: | - go test -v -race -coverprofile=coverage.out ./... + go test -v -race -coverprofile=coverage.out ./internal/... go tool cover -func=coverage.out go tool cover -html=coverage.out -o coverage.html @@ -110,7 +110,7 @@ jobs: strategy: matrix: - go-version: ['1.25'] + go-version: ['1.25.4'] os: [ubuntu-latest] steps: @@ -123,7 +123,7 @@ jobs: uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} - cache: true + cache: false - name: Build application run: | @@ -132,10 +132,12 @@ jobs: # Собираем основное приложение echo "🔨 Building main application..." - go build -o dist/notes -v -ldflags="-s -w" ./cmd/notes + go build -o dist/grpc-client -v -ldflags="-s -w" ./cmd/grpc-client + go build -o dist/grpc-server -v -ldflags="-s -w" ./cmd/grpc-server + go build -o dist/web-server -v -ldflags="-s -w" ./cmd/web-server - # Проверяем что бинарник создан - if [ -f dist/notes ]; then + # Проверяем что все бинарники созданы + if [ -f dist/grpc-client ] && [ -f dist/grpc-server ] && [ -f dist/web-server ]; then echo "✅ Build successful!" ls -la dist/ else diff --git a/.gitignore b/.gitignore index dd30fc7..ae7f9b2 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ coverage.html bin/ dist/ +# Local data for testing +data/ + # OS specific .DS_Store Thumbs.db diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..38bef53 --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +.DEFAULT_GOAL := help + +.PHONY: proto +proto: check-deps + @echo "🚀 Launch script for Protocol Buffer code generation..." + @chmod +x scripts/generate-proto.sh + @./scripts/generate-proto.sh + +.PHONY: proto-deps +proto-deps: + @echo "🛠️ Installing protobuf dependencies..." + @which protoc > /dev/null || (echo "⚠️ Note: protoc not found. Please install protobuf-compiler" && sleep 2) + @go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + @go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + +.PHONY: check-deps +check-deps: + @echo "🔍 Checking build dependencies..." + @which protoc > /dev/null || (echo "❌ Error: protoc not installed.\n On Ubuntu: sudo apt-get install protobuf-compiler\n On macOS: brew install protobuf" && exit 1) + @[ -f "$(shell go env GOPATH)/bin/protoc-gen-go" ] || (echo "❌ Error: protoc-gen-go not installed. Run 'make proto-deps'" && exit 1) + @[ -f "$(shell go env GOPATH)/bin/protoc-gen-go-grpc" ] || (echo "❌ Error: protoc-gen-go-grpc not installed. Run 'make proto-deps'" && exit 1) + @echo "✅ All dependencies are satisfied" + +.PHONY: all +all: proto-deps proto + @echo "✅ Build setup completed!" + +.PHONY: clean-proto +clean-proto: + @echo "🧹 Cleaning generated protobuf code..." + @rm -rf pkg/proto/* + +.PHONY: swag +swag: check-swag-deps + @echo "🚀 Launch script for Swagger documentation generation..." + @chmod +x scripts/generate-swag.sh + @./scripts/generate-swag.sh + +.PHONY: swag-deps +swag-deps: + @echo "🛠️ Installing Swaggo dependencies..." + @go install github.com/swaggo/swag/cmd/swag@latest + +.PHONY: check-swag-deps +check-swag-deps: + @echo "🔍 Checking Swaggo dependencies..." + @which swag > /dev/null || (echo "❌ Error: swag not installed. Run 'make swag-deps'" && exit 1) + @echo "✅ Swaggo dependencies are satisfied" + +.PHONY: test +test: + @go test ./... + +.PHONY: build +build: proto swag + @go build ./... + +.PHONY: help +help: + @echo "Available targets:" + @echo " proto - Generate protobuf code" + @echo " proto-deps - Install Go protobuf dependencies (requires protoc)" + @echo " swag - Generate Swagger documentation" + @echo " swag-deps - Install Swaggo dependencies" + @echo " all - Install proto-deps and generate proto" + @echo " test - Run tests" + @echo " clean-proto - Remove generated protobuf code" + @echo "" + @echo "⚠️ Note: protoc must be installed separately via system package manager" diff --git a/README.md b/README.md index b6cdac5..ca7adbe 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,201 @@ -# go-notes -Golang learning repo +# Go Notes + +Репозиторий для изучения языка Go, демонстрирующий создание веб-сервера и gRPC-сервера с возможностью управления заметками и пользователями. + +[![Go Version](https://img.shields.io/badge/Go-1.25+-blue.svg)](https://golang.org) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) + +## Особенности проекта + +- **Веб-API**: RESTful API с использованием фреймворка Gin +- **gRPC-сервер**: Реализация gRPC-сервисов для заметок и пользователей +- **Аутентификация**: JWT-токены для защиты маршрутов +- **Хранение данных**: Поддержка различных хранилищ (в памяти и в JSON-файлах) +- **Документация API**: Swagger UI для веб-API +- **Protocol Buffers**: Для определения gRPC-сервисов +- **Тестирование**: Модульные тесты + +## Структура проекта + +``` +go-notes/ +├── api/ # Определения API (protobuf) +├── cmd/ # Основные приложения +│ ├── grpc-client/ # Клиент gRPC +│ ├── grpc-server/ # Сервер gRPC +│ └── web-server/ # Веб-сервер (REST API) +├── docs/ # Документация Swagger +├── internal/ # Внутренний код приложения +│ ├── grpc/ # Реализация gRPC-сервера +│ ├── handler/ # Обработчики HTTP-запросов +│ ├── middleware/ # HTTP-мидлвары (например, аутентификация) +│ ├── model/ # Определения структур данных +│ ├── repository/ # Интерфейсы и фабрики репозиториев +│ └── util/ # Вспомогательные утилиты +├── pkg/ # Публичные пакеты (сгенерированный protobuf-код) +├── scripts/ # Скрипты для генерации кода +└── Makefile # Сборочные команды +``` + +## Функциональность + +### Веб-сервер (REST API) +- Аутентификация пользователей через JWT +- CRUD-операции для заметок и пользователей +- Swagger UI доступен по адресу `/swagger/index.html` +- Защищенные маршруты для изменения данных +- Открытые маршруты для чтения данных + +### gRPC-сервер +- Сервис для управления заметками +- Сервис для управления пользователями +- Поддержка всех CRUD-операций через gRPC + +### Хранение данных +- RAM-хранилище для временных данных +- JSON-хранилище для сохранения данных между запусками + +## Запуск приложения + +### Предварительные требования +- Go 1.25.4 или выше +- protoc (компилятор Protocol Buffers) +- make + +### Установка зависимостей + +```bash +# Установка зависимостей для protobuf +make proto-deps + +# Генерация protobuf-кода +make proto + +# Установка зависимостей для Swagger +make swag-deps + +# Генерация документации Swagger +make swag +``` + +### Запуск веб-сервера + +```bash +go run cmd/web-server/main.go +``` + +Сервер будет доступен по адресу `http://localhost:8080`, Swagger UI по адресу `http://localhost:8080/swagger/index.html`. + +### Запуск gRPC-сервера + +```bash +go run cmd/grpc-server/main.go +``` + +Сервер будет доступен по адресу `localhost:50051`. + +### Запуск gRPC-клиента + +```bash +go run cmd/grpc-client/main.go +``` + +Клиент выполнит тестовые операции с gRPC-сервером. + +## Примеры использования API + +### Работа с пользователями + +#### Регистрация пользователя +```bash +curl -X POST http://localhost:8080/api/users \ + -H "Content-Type: application/json" \ + -d '{"username": "testuser", "email": "test@example.com", "password": "password123"}' +``` + +#### Аутентификация пользователя (получение JWT-токена) +```bash +curl -X POST http://localhost:8080/api/login \ + -H "Content-Type: application/json" \ + -d '{"username": "testuser", "password": "password123"}' +``` + +После успешной аутентификации вы получите JWT-токен. При использовании токена в других запросах не включайте фигурные скобки `{}` - они используются только для обозначения плейсхолдера в примерах. + +#### Получение всех пользователей +```bash +curl -X GET http://localhost:8080/api/users +``` + +#### Получение пользователя по ID +```bash +curl -X GET http://localhost:8080/api/users/{user_id} +``` + +#### Обновление пользователя (требует JWT-токен) +```bash +curl -X PUT http://localhost:8080/api/users/{user_id} \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {jwt_token}" \ + -d '{"username": "updateduser", "email": "updated@example.com", "password": ""}' +``` + +#### Удаление пользователя (требует JWT-токен) +```bash +curl -X DELETE http://localhost:8080/api/users/{user_id} \ + -H "Authorization: Bearer {jwt_token}" +``` + +### Работа с заметками + +#### Создание заметки (требует JWT-токен) +```bash +curl -X POST http://localhost:8080/api/notes \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {jwt_token}" \ + -d '{"title": "Моя заметка", "content": "Содержимое заметки"}' +``` + +#### Получение всех заметок (открытый маршрут) +```bash +curl -X GET http://localhost:8080/api/notes +``` + +#### Получение заметки по ID (требует JWT-токен) +```bash +curl -X GET http://localhost:8080/api/notes/{note_id} \ + -H "Authorization: Bearer {jwt_token}" +``` + +#### Обновление заметки (требует JWT-токен) +```bash +curl -X PUT http://localhost:8080/api/notes/{note_id} \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer {jwt_token}" \ + -d '{"id": "{note_id}", "title": "Обновленная заметка", "content": "Обновленное содержимое"}' +``` + +#### Удаление заметки (требует JWT-токен) +```bash +curl -X DELETE http://localhost:8080/api/notes/{note_id} \ + -H "Authorization: Bearer {jwt_token}" +``` + +## Используемые технологии + +- [Gin](https://github.com/gin-gonic/gin) - веб-фреймворк +- [gRPC](https://grpc.io/) - фреймворк для RPC +- [Protocol Buffers](https://developers.google.com/protocol-buffers) - язык описания схемы данных +- [JWT](https://jwt.io/) - токены для аутентификации +- [Swaggo](https://github.com/swaggo/swag) - генерация документации Swagger +- [Testify](https://github.com/stretchr/testify) - библиотека для тестирования + +## Make-цели + +- `make proto` - генерация protobuf-кода +- `make proto-deps` - установка зависимостей protobuf +- `make swag` - генерация документации Swagger +- `make swag-deps` - установка зависимостей Swagger +- `make test` - запуск тестов +- `make build` - сборка приложения +- `make help` - список всех целей diff --git a/api/proto/note/notes.proto b/api/proto/note/notes.proto new file mode 100644 index 0000000..565ad1d --- /dev/null +++ b/api/proto/note/notes.proto @@ -0,0 +1,60 @@ +syntax = "proto3"; + +package notes; + +option go_package = "./pkg/proto/note"; + +// Message для заметки +message Note { + string id = 1; + string title = 2; + string content = 3; + int64 created_at = 4; + int64 updated_at = 5; +} + +// Message для запроса создания заметки +message CreateNoteRequest { + string title = 1; + string content = 2; +} + +// Message для запроса получения сущности по ID +message GetRequest { + string id = 1; +} + +// Message для запроса обновления заметки +message UpdateNoteRequest { + string id = 1; + string title = 2; + string content = 3; +} + +// Message для ответа с заметкой +message NoteResponse { + Note note = 1; +} + +// Message для ответа с несколькими заметками +message NotesListResponse { + repeated Note notes = 1; +} + +// Message для ответа об успешности операции +message SuccessResponse { + bool success = 1; + string message = 2; +} + +// Пустое сообщение для запросов без параметров +message Empty {} + +// Сервис для работы с заметками +service NotesService { + rpc CreateNote(CreateNoteRequest) returns (NoteResponse); + rpc GetNote(GetRequest) returns (NoteResponse); + rpc UpdateNote(UpdateNoteRequest) returns (NoteResponse); + rpc DeleteNote(GetRequest) returns (SuccessResponse); + rpc ListNotes(Empty) returns (NotesListResponse); +} \ No newline at end of file diff --git a/api/proto/user/users.proto b/api/proto/user/users.proto new file mode 100644 index 0000000..bb72b7b --- /dev/null +++ b/api/proto/user/users.proto @@ -0,0 +1,61 @@ +syntax = "proto3"; + +package users; + +option go_package = "./pkg/proto/user"; + +// Message для пользователя +message User { + string id = 1; + string username = 2; + string email = 3; + int64 created_at = 4; + int64 updated_at = 5; +} + +// Message для запроса создания пользователя +message CreateUserRequest { + string username = 1; + string email = 2; + string password = 3; +} + +// Message для запроса получения сущности по ID +message GetRequest { + string id = 1; +} + +// Message для запроса обновления пользователя +message UpdateUserRequest { + string id = 1; + string username = 2; + string email = 3; +} + +// Message для ответа с пользователем +message UserResponse { + User user = 1; +} + +// Message для ответа с несколькими пользователями +message UsersListResponse { + repeated User users = 1; +} + +// Message для ответа об успешности операции +message SuccessResponse { + bool success = 1; + string message = 2; +} + +// Пустое сообщение для запросов без параметров +message Empty {} + +// Сервис для работы с пользователями +service UserService { + rpc CreateUser(CreateUserRequest) returns (UserResponse); + rpc GetUser(GetRequest) returns (UserResponse); + rpc UpdateUser(UpdateUserRequest) returns (UserResponse); + rpc DeleteUser(GetRequest) returns (SuccessResponse); + rpc ListUsers(Empty) returns (UsersListResponse); +} \ No newline at end of file diff --git a/cmd/grpc-client/main.go b/cmd/grpc-client/main.go new file mode 100644 index 0000000..9755cae --- /dev/null +++ b/cmd/grpc-client/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + note "github.com/rd2w/go-notes/pkg/proto/note" + user "github.com/rd2w/go-notes/pkg/proto/user" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +const ( + address = "localhost:50051" +) + +func main() { + // Устанавливаем соединение с gRPC сервером + conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + log.Fatalf("Не удалось подключиться к gRPC серверу: %v", err) + } + defer func() { + if err := conn.Close(); err != nil { + log.Printf("Ошибка при закрытии соединения: %v", err) + } + }() + + client := note.NewNotesServiceClient(conn) + + // Тестирование операций с заметками + fmt.Println("=== Тестирование операций с заметками ===") + + // Создание заметки + fmt.Println("\n1. Создание заметки:") + createNoteResp, err := client.CreateNote(context.Background(), ¬e.CreateNoteRequest{ + Title: "Тестовая заметка", + Content: "Это содержимое тестовой заметки", + }) + if err != nil { + log.Printf("Ошибка при создании заметки: %v", err) + } else { + fmt.Printf("Создана заметка: ID=%s, Заголовок=%s\n", createNoteResp.Note.Id, createNoteResp.Note.Title) + } + + // Получение списка заметок + fmt.Println("\n2. Получение списка заметок:") + listNotesResp, err := client.ListNotes(context.Background(), ¬e.Empty{}) + if err != nil { + log.Printf("Ошибка при получении списка заметок: %v", err) + } else { + fmt.Printf("Найдено %d заметок:\n", len(listNotesResp.Notes)) + for _, noteItem := range listNotesResp.Notes { + fmt.Printf(" - ID: %s, Заголовок: %s\n", noteItem.Id, noteItem.Title) + } + } + + // Если есть хотя бы одна заметка, получаем её по ID и обновляем + if len(listNotesResp.Notes) > 0 { + firstNote := listNotesResp.Notes[0] + fmt.Printf("\n3. Получение заметки по ID (%s):\n", firstNote.Id) + getNoteResp, err := client.GetNote(context.Background(), ¬e.GetRequest{Id: firstNote.Id}) + if err != nil { + log.Printf("Ошибка при получении заметки: %v", err) + } else { + fmt.Printf("Получена заметка: ID=%s, Заголовок=%s, Содержимое=%s\n", + getNoteResp.Note.Id, getNoteResp.Note.Title, getNoteResp.Note.Content) + } + + fmt.Printf("\n4. Обновление заметки (%s):\n", firstNote.Id) + updateNoteResp, err := client.UpdateNote(context.Background(), ¬e.UpdateNoteRequest{ + Id: firstNote.Id, + Title: "Обновленная тестовая заметка", + Content: "Это обновленное содержимое тестовой заметки", + }) + if err != nil { + log.Printf("Ошибка при обновлении заметки: %v", err) + } else { + fmt.Printf("Обновлена заметка: ID=%s, Заголовок=%s\n", updateNoteResp.Note.Id, updateNoteResp.Note.Title) + } + } + + // Тестирование операций с пользователями + fmt.Println("\n=== Тестирование операций с пользователями ===") + + // Создание пользователя + fmt.Println("\n1. Создание пользователя:") + userClient := user.NewUserServiceClient(conn) + createUserResp, err := userClient.CreateUser(context.Background(), &user.CreateUserRequest{ + Username: "testuser", + Email: "test@example.com", + Password: "password123", + }) + if err != nil { + log.Printf("Ошибка при создании пользователя: %v", err) + } else { + fmt.Printf("Создан пользователь: ID=%s, Имя=%s, Email=%s\n", createUserResp.User.Id, createUserResp.User.Username, createUserResp.User.Email) + } + + // Получение списка пользователей + fmt.Println("\n2. Получение списка пользователей:") + listUsersResp, err := userClient.ListUsers(context.Background(), &user.Empty{}) + if err != nil { + log.Printf("Ошибка при получении списка пользователей: %v", err) + } else { + fmt.Printf("Найдено %d пользователей:\n", len(listUsersResp.Users)) + for _, userItem := range listUsersResp.Users { + fmt.Printf(" - ID: %s, Имя: %s, Email: %s\n", userItem.Id, userItem.Username, userItem.Email) + } + } + + // Если есть хотя бы один пользователь, получаем его по ID и обновляем + if len(listUsersResp.Users) > 0 { + firstUser := listUsersResp.Users[0] + fmt.Printf("\n3. Получение пользователя по ID (%s):\n", firstUser.Id) + getUserResp, err := userClient.GetUser(context.Background(), &user.GetRequest{Id: firstUser.Id}) + if err != nil { + log.Printf("Ошибка при получении пользователя: %v", err) + } else { + fmt.Printf("Получен пользователь: ID=%s, Имя=%s, Email=%s\n", + getUserResp.User.Id, getUserResp.User.Username, getUserResp.User.Email) + } + + fmt.Printf("\n4. Обновление пользователя (%s):\n", firstUser.Id) + updateUserResp, err := userClient.UpdateUser(context.Background(), &user.UpdateUserRequest{ + Id: firstUser.Id, + Username: "updateduser", + Email: "updated@example.com", + }) + if err != nil { + log.Printf("Ошибка при обновлении пользователя: %v", err) + } else { + fmt.Printf("Обновлен пользователь: ID=%s, Имя=%s, Email=%s\n", updateUserResp.User.Id, updateUserResp.User.Username, updateUserResp.User.Email) + } + } + + // Небольшая задержка перед удалением + time.Sleep(1 * time.Second) + + // Удаление последней созданной заметки + if createNoteResp != nil { + fmt.Printf("\n5. Удаление заметки (%s):\n", createNoteResp.Note.Id) + deleteNoteResp, err := client.DeleteNote(context.Background(), ¬e.GetRequest{Id: createNoteResp.Note.Id}) + if err != nil { + log.Printf("Ошибка при удалении заметки: %v", err) + } else { + fmt.Printf("Результат удаления: %t, Сообщение: %s\n", deleteNoteResp.Success, deleteNoteResp.Message) + } + } + + // Удаление последнего созданного пользователя + if createUserResp != nil { + fmt.Printf("\n6. Удаление пользователя (%s):\n", createUserResp.User.Id) + deleteUserResp, err := userClient.DeleteUser(context.Background(), &user.GetRequest{Id: createUserResp.User.Id}) + if err != nil { + log.Printf("Ошибка при удалении пользователя: %v", err) + } else { + fmt.Printf("Результат удаления: %t, Сообщение: %s\n", deleteUserResp.Success, deleteUserResp.Message) + } + } + + fmt.Println("\nТестирование gRPC клиента завершено.") +} diff --git a/cmd/grpc-server/main.go b/cmd/grpc-server/main.go new file mode 100644 index 0000000..d6199fa --- /dev/null +++ b/cmd/grpc-server/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "log" + "net" + "os" + "os/signal" + "syscall" + + grpcServer "github.com/rd2w/go-notes/internal/grpc" + "github.com/rd2w/go-notes/internal/repository" + "github.com/rd2w/go-notes/internal/repository/storage/fs" + "github.com/rd2w/go-notes/internal/repository/storage/ram" + "github.com/rd2w/go-notes/pkg/proto/note" + "github.com/rd2w/go-notes/pkg/proto/user" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" +) + +const ( + port = ":50051" +) + +func main() { + // Обработка сигналов ОС + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // Регистрируем реализации репозитория + repository.Register(repository.RAM, ram.NewRamRepository) + repository.Register(repository.JSON, fs.NewJSONRepository) + + // Инициализируем компоненты + repo := repository.NewRepositoryByType(repository.JSON) + + // Создаем наш gRPC сервер + grpcService := grpcServer.NewServer(repo) + + // Создаем сетевой слушатель + lis, err := net.Listen("tcp", port) + if err != nil { + log.Fatalf("Ошибка при создании сетевого слушателя: %v", err) + } + + // Создаем gRPC сервер с помощью библиотеки + grpcServerLib := grpc.NewServer() + + // Регистрируем gRPC сервис + note.RegisterNotesServiceServer(grpcServerLib, grpcService) + user.RegisterUserServiceServer(grpcServerLib, grpcService) + + // Добавляем reflection для инструментов gRPC + reflection.Register(grpcServerLib) + + // Запускаем gRPC сервер в отдельной горутине + go func() { + log.Printf("gRPC сервер запущен на порту %s", port) + if err := grpcServerLib.Serve(lis); err != nil { + log.Fatalf("Ошибка при запуске gRPC сервера: %v", err) + } + }() + + // Ждем сигнал завершения + <-sigChan + log.Println("Получен сигнал завершения, инициируем graceful shutdown...") + + // Останавливаем gRPC сервер + grpcServerLib.GracefulStop() + log.Println("gRPC сервер остановлен") +} diff --git a/cmd/notes/main.go b/cmd/notes/main.go deleted file mode 100644 index 34b58c2..0000000 --- a/cmd/notes/main.go +++ /dev/null @@ -1,108 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - "os/signal" - "syscall" - "time" - - "github.com/rd2w/go-notes/internal/logger" - "github.com/rd2w/go-notes/internal/model" - "github.com/rd2w/go-notes/internal/repository" - "github.com/rd2w/go-notes/internal/repository/storage/fs" - "github.com/rd2w/go-notes/internal/repository/storage/ram" - "github.com/rd2w/go-notes/internal/service" -) - -// Константы приложения -const ( - LoggerInterval = 200 * time.Millisecond - DataGenInterval = 500 * time.Millisecond - GracefulShutdownDelay = 100 * time.Millisecond - - TimeFormat = "2006-01-02 15:04:05" -) - -// Строковые константы -const ( - AppStartMsg = "Запуск приложения с горутинами и каналами..." - AppShutdownMsg = "Приложение \"Заметки\" успешно завершило выполнение программы!" - ShutdownStartMsg = "Получен сигнал завершения, инициируем graceful shutdown..." - ResultsHeader = "\n=== РЕЗУЛЬТАТЫ ===\n" - NoteCountMsg = "Всего заметок в хранилище данных: %d\n" - NoteDoesNotExistMsg = "Ошибка: заметка не существует" - NoteHeaderMsg = "Заметка %d:\n" - NoteIDMsg = " ID: %s\n" - NoteTitleMsg = " Заголовок: %s\n" - NoteContentMsg = " Содержимое: %s\n" - NoteCreatedAtMsg = " Создана: %s\n" - NoteUpdatedAtMsg = " Обновлена: %s\n" -) - -func main() { - log.Println(AppStartMsg) - - // Создаем контекст с отменой для graceful shutdown - ctx, cancel := context.WithCancel(context.Background()) - - // Обработка сигналов ОС - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) - - // Явно регистрируем реализации - repository.Register(repository.RAM, ram.NewRamRepository) - repository.Register(repository.JSON, fs.NewJSONRepository) - - // Инициализируем компоненты - repo := repository.NewRepositoryByType(repository.JSON) - svc := service.NewService(repo, ctx, DataGenInterval) - newLogger := logger.NewLogger(repo, ctx, LoggerInterval) - - // Запускаем горутины - go newLogger.Start() // Логгер мониторит изменения - go svc.Start() // Сервис запускает генерацию и сохранение данных - - // Ждем сигнал завершения - <-sigChan - log.Println(ShutdownStartMsg) - - // Отменяем контекст для завершения всех горутин - cancel() - - // Даем время на корректное завершение - time.Sleep(GracefulShutdownDelay) - - fmt.Print(ResultsHeader) - fmt.Printf(NoteCountMsg, repo.GetNotesCount()) - - notes := repo.GetAllNotes() - for i, note := range notes { - displayNoteInfo(i, note) - } - - log.Println(AppShutdownMsg) -} - -// displayNoteInfo отображает информацию о заметке в форматированном виде -func displayNoteInfo(count int, note *model.Note) { - if note == nil { - fmt.Println(NoteDoesNotExistMsg) - return - } - - fmt.Printf(NoteHeaderMsg, count+1) - fmt.Printf(NoteIDMsg, note.GetID()) - fmt.Printf(NoteTitleMsg, note.GetTitle()) - fmt.Printf(NoteContentMsg, note.GetContent()) - fmt.Printf(NoteCreatedAtMsg, formatTime(note.GetCreatedAt())) - fmt.Printf(NoteUpdatedAtMsg, formatTime(note.GetUpdatedAt())) - fmt.Println() -} - -// formatTime форматирует время в едином стиле -func formatTime(t time.Time) string { - return t.Format(TimeFormat) -} diff --git a/cmd/web-server/main.go b/cmd/web-server/main.go new file mode 100644 index 0000000..a2bf436 --- /dev/null +++ b/cmd/web-server/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + + "github.com/rd2w/go-notes/internal/handler" + "github.com/rd2w/go-notes/internal/middleware" + "github.com/rd2w/go-notes/internal/repository" + "github.com/rd2w/go-notes/internal/repository/storage/fs" + "github.com/rd2w/go-notes/internal/repository/storage/ram" + + _ "github.com/rd2w/go-notes/docs" +) + +// @title Go Notes API +// @version 1.0 +// @description API для управления заметками и пользователями +// @host localhost:8080 +// @BasePath /api +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description JWT Authorization header using the Bearer scheme +func main() { + // Устанавливаем Gin в режиме release + gin.SetMode(gin.ReleaseMode) + + // Создаем Gin роутер + r := gin.Default() + + // Регистрируем реализации репозитория + repository.Register(repository.RAM, ram.NewRamRepository) + repository.Register(repository.JSON, fs.NewJSONRepository) + + // Инициализируем репозиторий + repo := repository.NewRepositoryByType(repository.JSON) + + // Создаем обработчики + noteHandler := handler.NewNoteHandler(repo) + userHandler := handler.NewUserHandler(repo) + + // Создаем группу маршрутов для API + api := r.Group("/api") + { + // Маршруты для аутентификации + api.POST("/login", userHandler.Login) + + // Защищенные маршруты для заметок + notes := api.Group("/notes") + notes.Use(middleware.AuthMiddleware()) + { + notes.POST("", noteHandler.CreateNote) + notes.GET("/:id", noteHandler.GetNote) + notes.PUT("/:id", noteHandler.UpdateNote) + notes.DELETE("/:id", noteHandler.DeleteNote) + } + + // Открытый маршрут для получения всех заметок + api.GET("/notes", noteHandler.GetAllNotes) + + // Защищенные маршруты для пользователей + users := api.Group("/users") + users.Use(middleware.AuthMiddleware()) + { + users.GET("/:id", userHandler.GetUser) + users.PUT("/:id", userHandler.UpdateUser) + users.DELETE("/:id", userHandler.DeleteUser) + } + + // Открытый маршрут для создания пользователя + api.POST("/users", userHandler.CreateUser) + // Открытый маршрут для получения всех пользователей + api.GET("/users", userHandler.GetAllUsers) + } + + // Добавляем маршрут для Swagger UI + r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + + // Health check + r.GET("/health", func(c *gin.Context) { + c.JSON(200, gin.H{"status": "ok"}) + }) + + // Создаем HTTP сервер + srv := &http.Server{ + Addr: ":8080", + Handler: r, + } + + // Канал для получения сигнала завершения + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // Запускаем сервер в отдельной горутине + go func() { + log.Printf("Веб-сервер запущен на порту %s", srv.Addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Ошибка при запуске веб-сервера: %v", err) + } + }() + + // Ждем сигнал завершения + <-sigChan + log.Println("Получен сигнал завершения, инициируем graceful shutdown...") + + // Создаем контекст с таймаутом для graceful shutdown + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Останавливаем сервер с graceful shutdown + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Ошибка при graceful shutdown веб-сервера: %v", err) + } + + log.Println("Веб-сервер остановлен") +} diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..c2c2a73 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,558 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/login": { + "post": { + "description": "Аутентифицирует пользователя и возвращает JWT токен", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Аутентификация пользователя", + "parameters": [ + { + "description": "Учетные данные", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.loginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.loginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/notes": { + "get": { + "description": "Возвращает список всех заметок", + "produces": [ + "application/json" + ], + "tags": [ + "notes" + ], + "summary": "Получить все заметки", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Note" + } + } + } + } + }, + "post": { + "description": "Создает новую заметку с указанными заголовком и содержимым", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notes" + ], + "summary": "Создать новую заметку", + "parameters": [ + { + "description": "Заметка", + "name": "note", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.createNoteRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/model.Note" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/notes/{id}": { + "get": { + "description": "Возвращает заметку по указанному ID", + "produces": [ + "application/json" + ], + "tags": [ + "notes" + ], + "summary": "Получить заметку по ID", + "parameters": [ + { + "type": "string", + "description": "ID заметки", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.Note" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Обновляет заметку с указанным ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notes" + ], + "summary": "Обновить заметку", + "parameters": [ + { + "type": "string", + "description": "ID заметки", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Обновленная заметка", + "name": "note", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Note" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.Note" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "description": "Удаляет заметку с указанным ID", + "produces": [ + "application/json" + ], + "tags": [ + "notes" + ], + "summary": "Удалить заметку", + "parameters": [ + { + "type": "string", + "description": "ID заметки", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/users": { + "get": { + "description": "Возвращает список всех пользователей", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Получить всех пользователей", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.User" + } + } + } + } + }, + "post": { + "description": "Создает нового пользователя с указанными данными", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Создать нового пользователя", + "parameters": [ + { + "description": "Пользователь", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.createUserRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/model.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/users/{id}": { + "get": { + "description": "Возвращает пользователя по указанному ID", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Получить пользователя по ID", + "parameters": [ + { + "type": "string", + "description": "ID пользователя", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.User" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Обновляет пользователя с указанным ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Обновить пользователя", + "parameters": [ + { + "type": "string", + "description": "ID пользователя", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Обновленный пользователь", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.User" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "description": "Удаляет пользователя с указанным ID", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Удалить пользователя", + "parameters": [ + { + "type": "string", + "description": "ID пользователя", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "handler.createNoteRequest": { + "type": "object", + "required": [ + "content", + "title" + ], + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "handler.createUserRequest": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.loginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.loginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "model.Note": { + "type": "object" + }, + "model.User": { + "type": "object" + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "JWT Authorization header using the Bearer scheme", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/api", + Schemes: []string{}, + Title: "Go Notes API", + Description: "API для управления заметками и пользователями", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..2cd1998 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,534 @@ +{ + "swagger": "2.0", + "info": { + "description": "API для управления заметками и пользователями", + "title": "Go Notes API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/api", + "paths": { + "/api/login": { + "post": { + "description": "Аутентифицирует пользователя и возвращает JWT токен", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Аутентификация пользователя", + "parameters": [ + { + "description": "Учетные данные", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.loginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.loginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/notes": { + "get": { + "description": "Возвращает список всех заметок", + "produces": [ + "application/json" + ], + "tags": [ + "notes" + ], + "summary": "Получить все заметки", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Note" + } + } + } + } + }, + "post": { + "description": "Создает новую заметку с указанными заголовком и содержимым", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notes" + ], + "summary": "Создать новую заметку", + "parameters": [ + { + "description": "Заметка", + "name": "note", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.createNoteRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/model.Note" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/notes/{id}": { + "get": { + "description": "Возвращает заметку по указанному ID", + "produces": [ + "application/json" + ], + "tags": [ + "notes" + ], + "summary": "Получить заметку по ID", + "parameters": [ + { + "type": "string", + "description": "ID заметки", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.Note" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Обновляет заметку с указанным ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notes" + ], + "summary": "Обновить заметку", + "parameters": [ + { + "type": "string", + "description": "ID заметки", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Обновленная заметка", + "name": "note", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.Note" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.Note" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "description": "Удаляет заметку с указанным ID", + "produces": [ + "application/json" + ], + "tags": [ + "notes" + ], + "summary": "Удалить заметку", + "parameters": [ + { + "type": "string", + "description": "ID заметки", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/users": { + "get": { + "description": "Возвращает список всех пользователей", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Получить всех пользователей", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.User" + } + } + } + } + }, + "post": { + "description": "Создает нового пользователя с указанными данными", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Создать нового пользователя", + "parameters": [ + { + "description": "Пользователь", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.createUserRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/model.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/users/{id}": { + "get": { + "description": "Возвращает пользователя по указанному ID", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Получить пользователя по ID", + "parameters": [ + { + "type": "string", + "description": "ID пользователя", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.User" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Обновляет пользователя с указанным ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Обновить пользователя", + "parameters": [ + { + "type": "string", + "description": "ID пользователя", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Обновленный пользователь", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/model.User" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "description": "Удаляет пользователя с указанным ID", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Удалить пользователя", + "parameters": [ + { + "type": "string", + "description": "ID пользователя", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "handler.createNoteRequest": { + "type": "object", + "required": [ + "content", + "title" + ], + "properties": { + "content": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "handler.createUserRequest": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.loginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.loginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "model.Note": { + "type": "object" + }, + "model.User": { + "type": "object" + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "JWT Authorization header using the Bearer scheme", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..0c1d1a6 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,354 @@ +basePath: /api +definitions: + handler.createNoteRequest: + properties: + content: + type: string + title: + type: string + required: + - content + - title + type: object + handler.createUserRequest: + properties: + email: + type: string + password: + type: string + username: + type: string + required: + - email + - password + - username + type: object + handler.loginRequest: + properties: + password: + type: string + username: + type: string + required: + - password + - username + type: object + handler.loginResponse: + properties: + token: + type: string + type: object + model.Note: + type: object + model.User: + type: object +host: localhost:8080 +info: + contact: {} + description: API для управления заметками и пользователями + title: Go Notes API + version: "1.0" +paths: + /api/login: + post: + consumes: + - application/json + description: Аутентифицирует пользователя и возвращает JWT токен + parameters: + - description: Учетные данные + in: body + name: credentials + required: true + schema: + $ref: '#/definitions/handler.loginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.loginResponse' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "401": + description: Unauthorized + schema: + additionalProperties: + type: string + type: object + summary: Аутентификация пользователя + tags: + - auth + /api/notes: + get: + description: Возвращает список всех заметок + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/model.Note' + type: array + summary: Получить все заметки + tags: + - notes + post: + consumes: + - application/json + description: Создает новую заметку с указанными заголовком и содержимым + parameters: + - description: Заметка + in: body + name: note + required: true + schema: + $ref: '#/definitions/handler.createNoteRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/model.Note' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + summary: Создать новую заметку + tags: + - notes + /api/notes/{id}: + delete: + description: Удаляет заметку с указанным ID + parameters: + - description: ID заметки + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + summary: Удалить заметку + tags: + - notes + get: + description: Возвращает заметку по указанному ID + parameters: + - description: ID заметки + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.Note' + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + summary: Получить заметку по ID + tags: + - notes + put: + consumes: + - application/json + description: Обновляет заметку с указанным ID + parameters: + - description: ID заметки + in: path + name: id + required: true + type: string + - description: Обновленная заметка + in: body + name: note + required: true + schema: + $ref: '#/definitions/model.Note' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.Note' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + summary: Обновить заметку + tags: + - notes + /api/users: + get: + description: Возвращает список всех пользователей + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/model.User' + type: array + summary: Получить всех пользователей + tags: + - users + post: + consumes: + - application/json + description: Создает нового пользователя с указанными данными + parameters: + - description: Пользователь + in: body + name: user + required: true + schema: + $ref: '#/definitions/handler.createUserRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/model.User' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + summary: Создать нового пользователя + tags: + - users + /api/users/{id}: + delete: + description: Удаляет пользователя с указанным ID + parameters: + - description: ID пользователя + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + summary: Удалить пользователя + tags: + - users + get: + description: Возвращает пользователя по указанному ID + parameters: + - description: ID пользователя + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.User' + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + summary: Получить пользователя по ID + tags: + - users + put: + consumes: + - application/json + description: Обновляет пользователя с указанным ID + parameters: + - description: ID пользователя + in: path + name: id + required: true + type: string + - description: Обновленный пользователь + in: body + name: user + required: true + schema: + $ref: '#/definitions/model.User' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.User' + "400": + description: Bad Request + schema: + additionalProperties: + type: string + type: object + "404": + description: Not Found + schema: + additionalProperties: + type: string + type: object + summary: Обновить пользователя + tags: + - users +securityDefinitions: + BearerAuth: + description: JWT Authorization header using the Bearer scheme + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/go.mod b/go.mod index 3773309..742fb44 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,65 @@ module github.com/rd2w/go-notes -go 1.25.3 +go 1.25.4 require ( + github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.11.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.1 + github.com/swaggo/swag v1.16.6 + golang.org/x/crypto v0.43.0 + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.14.2 // indirect + github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.11 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-openapi/jsonpointer v0.22.1 // indirect + github.com/go-openapi/jsonreference v0.21.3 // indirect + github.com/go-openapi/spec v0.22.0 // indirect + github.com/go-openapi/swag/conv v0.25.1 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // indirect + github.com/go-openapi/swag/jsonutils v0.25.1 // indirect + github.com/go-openapi/swag/loading v0.25.1 // indirect + github.com/go-openapi/swag/stringutils v0.25.1 // indirect + github.com/go-openapi/swag/typeutils v0.25.1 // indirect + github.com/go-openapi/swag/yamlutils v0.25.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.28.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.56.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.uber.org/mock v0.6.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.23.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.38.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 42976e0..bac1d1c 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,228 @@ +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk= +github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM= +github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= +github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= +github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw= +github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0= +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/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0= +github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= +github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8= +github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg= +github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw= +github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc= +github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw= +github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg= +github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA= +github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8= +github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk= +github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= +github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= +golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/internal/grpc/server.go b/internal/grpc/server.go new file mode 100644 index 0000000..77e9803 --- /dev/null +++ b/internal/grpc/server.go @@ -0,0 +1,238 @@ +package grpc + +import ( + "context" + "fmt" + "log" + + "github.com/rd2w/go-notes/internal/model" + "github.com/rd2w/go-notes/internal/repository" + "github.com/rd2w/go-notes/pkg/proto/note" + "github.com/rd2w/go-notes/pkg/proto/user" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Server реализует gRPC-сервер для сервиса заметок +type Server struct { + note.UnimplementedNotesServiceServer + user.UnimplementedUserServiceServer + repo repository.Repository +} + +// NewServer создает новый экземпляр gRPC-сервера +func NewServer(r repository.Repository) *Server { + return &Server{ + repo: r, + } +} + +// CreateNote создает новую заметку +func (s *Server) CreateNote(ctx context.Context, req *note.CreateNoteRequest) (*note.NoteResponse, error) { + newNote := model.NewNote(req.Title, req.Content) + s.repo.Save(newNote) + + return ¬e.NoteResponse{ + Note: ¬e.Note{ + Id: newNote.GetID(), + Title: newNote.GetTitle(), + Content: newNote.GetContent(), + CreatedAt: newNote.GetCreatedAt().Unix(), + UpdatedAt: newNote.GetUpdatedAt().Unix(), + }, + }, nil +} + +// GetNote возвращает заметку по ID +func (s *Server) GetNote(ctx context.Context, req *note.GetRequest) (*note.NoteResponse, error) { + entity := s.repo.GetByID("note", req.Id) + if entity == nil { + return nil, status.Error(codes.NotFound, "note not found") + } + + noteEntity, ok := entity.(*model.Note) + if !ok { + return nil, status.Error(codes.Internal, "failed to cast entity to note") + } + + return ¬e.NoteResponse{ + Note: ¬e.Note{ + Id: noteEntity.GetID(), + Title: noteEntity.GetTitle(), + Content: noteEntity.GetContent(), + CreatedAt: noteEntity.GetCreatedAt().Unix(), + UpdatedAt: noteEntity.GetUpdatedAt().Unix(), + }, + }, nil +} + +// UpdateNote обновляет заметку +func (s *Server) UpdateNote(ctx context.Context, req *note.UpdateNoteRequest) (*note.NoteResponse, error) { + entity := s.repo.GetByID("note", req.Id) + if entity == nil { + return nil, status.Error(codes.NotFound, "note not found") + } + + noteEntity, ok := entity.(*model.Note) + if !ok { + return nil, status.Error(codes.Internal, "failed to cast entity to note") + } + + noteEntity.SetTitle(req.Title) + noteEntity.SetContent(req.Content) + + s.repo.Save(noteEntity) + + return ¬e.NoteResponse{ + Note: ¬e.Note{ + Id: noteEntity.GetID(), + Title: noteEntity.GetTitle(), + Content: noteEntity.GetContent(), + CreatedAt: noteEntity.GetCreatedAt().Unix(), + UpdatedAt: noteEntity.GetUpdatedAt().Unix(), + }, + }, nil +} + +// DeleteNote удаляет заметку +func (s *Server) DeleteNote(ctx context.Context, req *note.GetRequest) (*note.SuccessResponse, error) { + deleted := s.repo.DeleteByID("note", req.Id) + if !deleted { + return nil, status.Error(codes.NotFound, "note not found") + } + + return ¬e.SuccessResponse{ + Success: true, + Message: "note deleted successfully", + }, nil +} + +// ListNotes возвращает список всех заметок +func (s *Server) ListNotes(ctx context.Context, req *note.Empty) (*note.NotesListResponse, error) { + notes := s.repo.GetAllNotes() + protoNotes := make([]*note.Note, len(notes)) + + for i, noteEntity := range notes { + protoNotes[i] = ¬e.Note{ + Id: noteEntity.GetID(), + Title: noteEntity.GetTitle(), + Content: noteEntity.GetContent(), + CreatedAt: noteEntity.GetCreatedAt().Unix(), + UpdatedAt: noteEntity.GetUpdatedAt().Unix(), + } + } + + return ¬e.NotesListResponse{ + Notes: protoNotes, + }, nil +} + +// CreateUser создает нового пользователя +func (s *Server) CreateUser(ctx context.Context, req *user.CreateUserRequest) (*user.UserResponse, error) { + newUser, err := model.NewUser(req.Username, req.Email, req.Password) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to create user: %v", err)) + } + + s.repo.Save(newUser) + + return &user.UserResponse{ + User: &user.User{ + Id: newUser.GetID(), + Username: newUser.GetUsername(), + Email: newUser.GetEmail(), + CreatedAt: newUser.GetCreatedAt().Unix(), + UpdatedAt: newUser.GetUpdatedAt().Unix(), + }, + }, nil +} + +// GetUser возвращает пользователя по ID +func (s *Server) GetUser(ctx context.Context, req *user.GetRequest) (*user.UserResponse, error) { + entity := s.repo.GetByID("user", req.Id) + if entity == nil { + return nil, status.Error(codes.NotFound, "user not found") + } + + userEntity, ok := entity.(*model.User) + if !ok { + return nil, status.Error(codes.Internal, "failed to cast entity to user") + } + + return &user.UserResponse{ + User: &user.User{ + Id: userEntity.GetID(), + Username: userEntity.GetUsername(), + Email: userEntity.GetEmail(), + CreatedAt: userEntity.GetCreatedAt().Unix(), + UpdatedAt: userEntity.GetUpdatedAt().Unix(), + }, + }, nil +} + +// UpdateUser обновляет пользователя +func (s *Server) UpdateUser(ctx context.Context, req *user.UpdateUserRequest) (*user.UserResponse, error) { + entity := s.repo.GetByID("user", req.Id) + if entity == nil { + return nil, status.Error(codes.NotFound, "user not found") + } + + userEntity, ok := entity.(*model.User) + if !ok { + return nil, status.Error(codes.Internal, "failed to cast entity to user") + } + + userEntity.SetUsername(req.Username) + userEntity.SetEmail(req.Email) + + s.repo.Save(userEntity) + + return &user.UserResponse{ + User: &user.User{ + Id: userEntity.GetID(), + Username: userEntity.GetUsername(), + Email: userEntity.GetEmail(), + CreatedAt: userEntity.GetCreatedAt().Unix(), + UpdatedAt: userEntity.GetUpdatedAt().Unix(), + }, + }, nil +} + +// DeleteUser удаляет пользователя +func (s *Server) DeleteUser(ctx context.Context, req *user.GetRequest) (*user.SuccessResponse, error) { + deleted := s.repo.DeleteByID("user", req.Id) + if !deleted { + return nil, status.Error(codes.NotFound, "user not found") + } + + return &user.SuccessResponse{ + Success: true, + Message: "user deleted successfully", + }, nil +} + +// ListUsers возвращает список всех пользователей +func (s *Server) ListUsers(ctx context.Context, req *user.Empty) (*user.UsersListResponse, error) { + entities := s.repo.GetAllByType("user") + protoUsers := make([]*user.User, len(entities)) + + for i, entity := range entities { + userEntity, ok := entity.(*model.User) + if !ok { + log.Printf("Failed to cast entity to user at index %d", i) + continue + } + + protoUsers[i] = &user.User{ + Id: userEntity.GetID(), + Username: userEntity.GetUsername(), + Email: userEntity.GetEmail(), + CreatedAt: userEntity.GetCreatedAt().Unix(), + UpdatedAt: userEntity.GetUpdatedAt().Unix(), + } + } + + return &user.UsersListResponse{ + Users: protoUsers, + }, nil +} diff --git a/internal/grpc/server_test.go b/internal/grpc/server_test.go new file mode 100644 index 0000000..d584c13 --- /dev/null +++ b/internal/grpc/server_test.go @@ -0,0 +1,468 @@ +package grpc + +import ( + "context" + "testing" + + "github.com/rd2w/go-notes/internal/model" + "github.com/rd2w/go-notes/internal/repository" + "github.com/rd2w/go-notes/pkg/proto/note" + "github.com/rd2w/go-notes/pkg/proto/user" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// MockRepository - тестовая реализация репозитория +type MockRepository struct { + entities map[string]repository.Entity +} + +func NewMockRepository() *MockRepository { + return &MockRepository{ + entities: make(map[string]repository.Entity), + } +} + +func (m *MockRepository) Save(entity repository.Entity) { + m.entities[entity.GetID()] = entity +} + +func (m *MockRepository) GetAllNotes() []*model.Note { + var notes []*model.Note + for _, entity := range m.entities { + if noteEntity, ok := entity.(*model.Note); ok { + notes = append(notes, noteEntity) + } + } + return notes +} + +func (m *MockRepository) GetNotesCount() int { + count := 0 + for _, entity := range m.entities { + if _, ok := entity.(*model.Note); ok { + count++ + } + } + return count +} + +func (m *MockRepository) GetNewNotes(lastIndex int) []*model.Note { + return m.GetAllNotes() +} + +func (m *MockRepository) GetAllByType(entityType string) []repository.Entity { + var entities []repository.Entity + for _, entity := range m.entities { + if entity.GetType() == entityType { + entities = append(entities, entity) + } + } + return entities +} + +func (m *MockRepository) GetByID(entityType, id string) repository.Entity { + entity := m.entities[id] + if entity != nil && entity.GetType() == entityType { + return entity + } + return nil +} + +func (m *MockRepository) DeleteByID(entityType, id string) bool { + entity := m.entities[id] + if entity != nil && entity.GetType() == entityType { + delete(m.entities, id) + return true + } + return false +} + +func TestNewServer(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + assert.NotNil(t, server) + assert.Equal(t, mockRepo, server.repo) +} + +func TestCreateNote(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + req := ¬e.CreateNoteRequest{ + Title: "Test Note", + Content: "Test Content", + } + + resp, err := server.CreateNote(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.NotNil(t, resp.Note) + assert.Equal(t, "Test Note", resp.Note.Title) + assert.Equal(t, "Test Content", resp.Note.Content) + assert.NotEmpty(t, resp.Note.Id) + + // Проверяем, что заметка была сохранена в репозитории + entity := mockRepo.GetByID("note", resp.Note.Id) + assert.NotNil(t, entity) + assert.IsType(t, &model.Note{}, entity) +} + +func TestGetNote(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + // Создаем тестовую заметку + testNote := model.NewNote("Test Title", "Test Content") + mockRepo.Save(testNote) + + req := ¬e.GetRequest{ + Id: testNote.GetID(), + } + + resp, err := server.GetNote(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.NotNil(t, resp.Note) + assert.Equal(t, testNote.GetID(), resp.Note.Id) + assert.Equal(t, testNote.GetTitle(), resp.Note.Title) + assert.Equal(t, testNote.GetContent(), resp.Note.Content) +} + +func TestGetNoteNotFound(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + req := ¬e.GetRequest{ + Id: "nonexistent-id", + } + + resp, err := server.GetNote(context.Background(), req) + + assert.Nil(t, resp) + assert.NotNil(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) +} + +func TestUpdateNote(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + // Создаем тестовую заметку + testNote := model.NewNote("Old Title", "Old Content") + mockRepo.Save(testNote) + + req := ¬e.UpdateNoteRequest{ + Id: testNote.GetID(), + Title: "New Title", + Content: "New Content", + } + + resp, err := server.UpdateNote(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.NotNil(t, resp.Note) + assert.Equal(t, testNote.GetID(), resp.Note.Id) + assert.Equal(t, "New Title", resp.Note.Title) + assert.Equal(t, "New Content", resp.Note.Content) + + // Проверяем, что заметка была обновлена в репозитории + entity := mockRepo.GetByID("note", testNote.GetID()) + assert.NotNil(t, entity) + assert.IsType(t, &model.Note{}, entity) + assert.Equal(t, "New Title", entity.(*model.Note).GetTitle()) + assert.Equal(t, "New Content", entity.(*model.Note).GetContent()) +} + +func TestUpdateNoteNotFound(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + req := ¬e.UpdateNoteRequest{ + Id: "nonexistent-id", + Title: "New Title", + Content: "New Content", + } + + resp, err := server.UpdateNote(context.Background(), req) + + assert.Nil(t, resp) + assert.NotNil(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) +} + +func TestDeleteNote(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + // Создаем тестовую заметку + testNote := model.NewNote("Test Title", "Test Content") + mockRepo.Save(testNote) + + req := ¬e.GetRequest{ + Id: testNote.GetID(), + } + + resp, err := server.DeleteNote(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.True(t, resp.Success) + assert.Equal(t, "note deleted successfully", resp.Message) + + // Проверяем, что заметка была удалена из репозитория + entity := mockRepo.GetByID("note", testNote.GetID()) + assert.Nil(t, entity) +} + +func TestDeleteNoteNotFound(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + req := ¬e.GetRequest{ + Id: "nonexistent-id", + } + + resp, err := server.DeleteNote(context.Background(), req) + + assert.Nil(t, resp) + assert.NotNil(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) +} + +func TestListNotes(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + // Создаем несколько тестовых заметок + note1 := model.NewNote("Note 1", "Content 1") + note2 := model.NewNote("Note 2", "Content 2") + mockRepo.Save(note1) + mockRepo.Save(note2) + + req := ¬e.Empty{} + + resp, err := server.ListNotes(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Len(t, resp.Notes, 2) + + // Проверяем, что заметки содержатся в ответе + var foundNote1, foundNote2 bool + for _, n := range resp.Notes { + if n.Id == note1.GetID() { + assert.Equal(t, "Note 1", n.Title) + foundNote1 = true + } + if n.Id == note2.GetID() { + assert.Equal(t, "Note 2", n.Title) + foundNote2 = true + } + } + assert.True(t, foundNote1) + assert.True(t, foundNote2) +} + +func TestCreateUser(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + req := &user.CreateUserRequest{ + Username: "testuser", + Email: "test@example.com", + Password: "password123", + } + + resp, err := server.CreateUser(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.NotNil(t, resp.User) + assert.Equal(t, "testuser", resp.User.Username) + assert.Equal(t, "test@example.com", resp.User.Email) + assert.NotEmpty(t, resp.User.Id) + + // Проверяем, что пользователь был сохранен в репозитории + entity := mockRepo.GetByID("user", resp.User.Id) + assert.NotNil(t, entity) + assert.IsType(t, &model.User{}, entity) +} + +func TestGetUser(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + // Создаем тестового пользователя + testUser, err := model.NewUser("testuser", "test@example.com", "password123") + if err != nil { + t.Fatalf("Failed to create test user: %v", err) + } + mockRepo.Save(testUser) + + req := &user.GetRequest{ + Id: testUser.GetID(), + } + + resp, err := server.GetUser(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.NotNil(t, resp.User) + assert.Equal(t, testUser.GetID(), resp.User.Id) + assert.Equal(t, testUser.GetUsername(), resp.User.Username) + assert.Equal(t, testUser.GetEmail(), resp.User.Email) +} + +func TestGetUserNotFound(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + req := &user.GetRequest{ + Id: "nonexistent-id", + } + + resp, err := server.GetUser(context.Background(), req) + + assert.Nil(t, resp) + assert.NotNil(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) +} + +func TestUpdateUser(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + // Создаем тестового пользователя + testUser, err := model.NewUser("olduser", "old@example.com", "password123") + if err != nil { + t.Fatalf("Failed to create test user: %v", err) + } + mockRepo.Save(testUser) + + req := &user.UpdateUserRequest{ + Id: testUser.GetID(), + Username: "newuser", + Email: "new@example.com", + } + + resp, err := server.UpdateUser(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.NotNil(t, resp.User) + assert.Equal(t, testUser.GetID(), resp.User.Id) + assert.Equal(t, "newuser", resp.User.Username) + assert.Equal(t, "new@example.com", resp.User.Email) + + // Проверяем, что пользователь был обновлен в репозитории + entity := mockRepo.GetByID("user", testUser.GetID()) + assert.NotNil(t, entity) + assert.IsType(t, &model.User{}, entity) + assert.Equal(t, "newuser", entity.(*model.User).GetUsername()) + assert.Equal(t, "new@example.com", entity.(*model.User).GetEmail()) +} + +func TestUpdateUserNotFound(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + req := &user.UpdateUserRequest{ + Id: "nonexistent-id", + Username: "newuser", + Email: "new@example.com", + } + + resp, err := server.UpdateUser(context.Background(), req) + + assert.Nil(t, resp) + assert.NotNil(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) +} + +func TestDeleteUser(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + // Создаем тестового пользователя + testUser, err := model.NewUser("testuser", "test@example.com", "password123") + if err != nil { + t.Fatalf("Failed to create test user: %v", err) + } + mockRepo.Save(testUser) + + req := &user.GetRequest{ + Id: testUser.GetID(), + } + + resp, err := server.DeleteUser(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.True(t, resp.Success) + assert.Equal(t, "user deleted successfully", resp.Message) + + // Проверяем, что пользователь был удален из репозитория + entity := mockRepo.GetByID("user", testUser.GetID()) + assert.Nil(t, entity) +} + +func TestDeleteUserNotFound(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + req := &user.GetRequest{ + Id: "nonexistent-id", + } + + resp, err := server.DeleteUser(context.Background(), req) + + assert.Nil(t, resp) + assert.NotNil(t, err) + assert.Equal(t, codes.NotFound, status.Code(err)) +} + +func TestListUsers(t *testing.T) { + mockRepo := NewMockRepository() + server := NewServer(mockRepo) + + // Создаем несколько тестовых пользователей + user1, err := model.NewUser("user1", "user1@example.com", "password1") + if err != nil { + t.Fatalf("Failed to create test user: %v", err) + } + user2, err := model.NewUser("user2", "user2@example.com", "password2") + if err != nil { + t.Fatalf("Failed to create test user: %v", err) + } + mockRepo.Save(user1) + mockRepo.Save(user2) + + req := &user.Empty{} + + resp, err := server.ListUsers(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Len(t, resp.Users, 2) + + // Проверяем, что пользователи содержатся в ответе + var foundUser1, foundUser2 bool + for _, u := range resp.Users { + if u.Id == user1.GetID() { + assert.Equal(t, "user1", u.Username) + foundUser1 = true + } + if u.Id == user2.GetID() { + assert.Equal(t, "user2", u.Username) + foundUser2 = true + } + } + assert.True(t, foundUser1) + assert.True(t, foundUser2) +} diff --git a/internal/handler/note_handler.go b/internal/handler/note_handler.go new file mode 100644 index 0000000..95802b6 --- /dev/null +++ b/internal/handler/note_handler.go @@ -0,0 +1,153 @@ +package handler + +import ( + "log" + "net/http" + _ "strconv" + + "github.com/gin-gonic/gin" + "github.com/rd2w/go-notes/internal/model" + "github.com/rd2w/go-notes/internal/repository" +) + +// NoteHandler структура для обработки HTTP запросов, связанных с заметками +type NoteHandler struct { + repo repository.Repository +} + +// NewNoteHandler создает новый экземпляр NoteHandler +func NewNoteHandler(repo repository.Repository) *NoteHandler { + return &NoteHandler{ + repo: repo, + } +} + +// CreateNote создает новую заметку +// @Summary Создать новую заметку +// @Description Создает новую заметку с указанными заголовком и содержимым +// @Tags notes +// @Accept json +// @Produce json +// @Param note body createNoteRequest true "Заметка" +// @Success 201 {object} model.Note +// @Failure 400 {object} map[string]string +// @Router /api/notes [post] +func (h *NoteHandler) CreateNote(c *gin.Context) { + log.Printf("CreateNote handler вызван") + var req createNoteRequest + if err := c.ShouldBindJSON(&req); err != nil { + log.Printf("Ошибка при привязке JSON: %v", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + log.Printf("Получен запрос на создание заметки: title='%s', content='%s'", req.Title, req.Content) + note := model.NewNote(req.Title, req.Content) + log.Printf("Создана новая заметка с ID: %s", note.GetID()) + h.repo.Save(note) + log.Printf("Заметка успешно сохранена в репозиторий") + c.JSON(http.StatusCreated, note) +} + +// createNoteRequest структура для запроса создания заметки +type createNoteRequest struct { + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` +} + +// GetNote возвращает заметку по ID +// @Summary Получить заметку по ID +// @Description Возвращает заметку по указанному ID +// @Tags notes +// @Produce json +// @Param id path string true "ID заметки" +// @Success 200 {object} model.Note +// @Failure 404 {object} map[string]string +// @Router /api/notes/{id} [get] +func (h *NoteHandler) GetNote(c *gin.Context) { + id := c.Param("id") + entity := h.repo.GetByID("note", id) + if entity == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Note not found"}) + return + } + + note, ok := entity.(*model.Note) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cast entity to note"}) + return + } + + c.JSON(http.StatusOK, note) +} + +// UpdateNote обновляет заметку +// @Summary Обновить заметку +// @Description Обновляет заметку с указанным ID +// @Tags notes +// @Accept json +// @Produce json +// @Param id path string true "ID заметки" +// @Param note body model.Note true "Обновленная заметка" +// @Success 200 {object} model.Note +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Router /api/notes/{id} [put] +func (h *NoteHandler) UpdateNote(c *gin.Context) { + id := c.Param("id") + entity := h.repo.GetByID("note", id) + if entity == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Note not found"}) + return + } + + note, ok := entity.(*model.Note) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cast entity to note"}) + return + } + + var updatedNote model.Note + if err := c.ShouldBindJSON(&updatedNote); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + note.SetTitle(updatedNote.GetTitle()) + note.SetContent(updatedNote.GetContent()) + + h.repo.Save(note) + c.JSON(http.StatusOK, note) +} + +// DeleteNote удаляет заметку +// @Summary Удалить заметку +// @Description Удаляет заметку с указанным ID +// @Tags notes +// @Produce json +// @Param id path string true "ID заметки" +// @Success 204 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Router /api/notes/{id} [delete] +func (h *NoteHandler) DeleteNote(c *gin.Context) { + id := c.Param("id") + deleted := h.repo.DeleteByID("note", id) + if !deleted { + c.JSON(http.StatusNotFound, gin.H{"error": "Note not found"}) + return + } + + c.JSON(http.StatusNoContent, gin.H{"message": "Note deleted successfully"}) +} + +// GetAllNotes возвращает все заметки +// @Summary Получить все заметки +// @Description Возвращает список всех заметок +// @Tags notes +// @Produce json +// @Success 200 {array} model.Note +// @Router /api/notes [get] +func (h *NoteHandler) GetAllNotes(c *gin.Context) { + notes := h.repo.GetAllNotes() + c.JSON(http.StatusOK, notes) +} diff --git a/internal/handler/note_handler_test.go b/internal/handler/note_handler_test.go new file mode 100644 index 0000000..12620b0 --- /dev/null +++ b/internal/handler/note_handler_test.go @@ -0,0 +1,305 @@ +package handler + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/rd2w/go-notes/internal/model" + "github.com/rd2w/go-notes/internal/repository" + "github.com/stretchr/testify/assert" +) + +// MockRepository - мок-репозиторий для тестирования +type MockRepository struct { + notes map[string]repository.Entity +} + +func NewMockRepository() *MockRepository { + return &MockRepository{ + notes: make(map[string]repository.Entity), + } +} + +func (m *MockRepository) Save(entity repository.Entity) { + m.notes[entity.GetID()] = entity +} + +func (m *MockRepository) GetByID(entityType, id string) repository.Entity { + entity, exists := m.notes[id] + if !exists { + return nil + } + return entity +} + +func (m *MockRepository) DeleteByID(entityType, id string) bool { + _, exists := m.notes[id] + if !exists { + return false + } + delete(m.notes, id) + return true +} + +func (m *MockRepository) GetAllNotes() []*model.Note { + notes := make([]*model.Note, 0, len(m.notes)) + for _, entity := range m.notes { + if note, ok := entity.(*model.Note); ok { + notes = append(notes, note) + } + } + return notes +} + +func (m *MockRepository) GetNotesCount() int { + count := 0 + for _, entity := range m.notes { + if _, ok := entity.(*model.Note); ok { + count++ + } + } + return count +} + +func (m *MockRepository) GetNewNotes(lastIndex int) []*model.Note { + // В мок-репозитории возвращаем все заметки, так как у нас нет временной метки + notes := m.GetAllNotes() + if lastIndex >= len(notes) { + return []*model.Note{} + } + return notes[lastIndex:] +} + +func (m *MockRepository) GetAllByType(entityType string) []repository.Entity { + entities := make([]repository.Entity, 0, len(m.notes)) + for _, entity := range m.notes { + if entity.GetType() == entityType { + entities = append(entities, entity) + } + } + return entities +} + +func init() { + gin.SetMode(gin.TestMode) +} + +func TestCreateNote(t *testing.T) { + repo := NewMockRepository() + handler := NewNoteHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.POST("/api/notes", handler.CreateNote) + + // Подготовка тестовых данных + requestBody := createNoteRequest{ + Title: "Test Note", + Content: "Test Content", + } + jsonData, _ := json.Marshal(requestBody) + + req, _ := http.NewRequest(http.MethodPost, "/api/notes", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code) + + var responseNote model.Note + err := json.Unmarshal(w.Body.Bytes(), &responseNote) + assert.NoError(t, err) + assert.Equal(t, "Test Note", responseNote.GetTitle()) + assert.Equal(t, "Test Content", responseNote.GetContent()) + assert.NotEmpty(t, responseNote.GetID()) +} + +func TestGetNote(t *testing.T) { + // Создаем мок-репозиторий с тестовой заметкой + repo := NewMockRepository() + testNote := model.NewNote("Test Title", "Test Content") + repo.Save(testNote) + + handler := NewNoteHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.GET("/api/notes/:id", handler.GetNote) + + req, _ := http.NewRequest(http.MethodGet, "/api/notes/"+testNote.GetID(), nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var responseNote model.Note + err := json.Unmarshal(w.Body.Bytes(), &responseNote) + assert.NoError(t, err) + assert.Equal(t, testNote.GetID(), responseNote.GetID()) + assert.Equal(t, testNote.GetTitle(), responseNote.GetTitle()) + assert.Equal(t, testNote.GetContent(), responseNote.GetContent()) +} + +func TestGetNoteNotFound(t *testing.T) { + repo := NewMockRepository() + handler := NewNoteHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.GET("/api/notes/:id", handler.GetNote) + + req, _ := http.NewRequest(http.MethodGet, "/api/notes/nonexistent", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestUpdateNote(t *testing.T) { + // Создаем мок-репозиторий с тестовой заметкой + repo := NewMockRepository() + testNote := model.NewNote("Original Title", "Original Content") + repo.Save(testNote) + + handler := NewNoteHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.PUT("/api/notes/:id", handler.UpdateNote) + + // Подготовка обновленных данных + updatedData := map[string]interface{}{ + "title": "Updated Title", + "content": "Updated Content", + } + jsonData, _ := json.Marshal(updatedData) + + req, _ := http.NewRequest(http.MethodPut, "/api/notes/"+testNote.GetID(), bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var responseNote model.Note + err := json.Unmarshal(w.Body.Bytes(), &responseNote) + assert.NoError(t, err) + assert.Equal(t, testNote.GetID(), responseNote.GetID()) + assert.Equal(t, "Updated Title", responseNote.GetTitle()) + assert.Equal(t, "Updated Content", responseNote.GetContent()) +} + +func TestUpdateNoteNotFound(t *testing.T) { + repo := NewMockRepository() + handler := NewNoteHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.PUT("/api/notes/:id", handler.UpdateNote) + + updatedData := map[string]interface{}{ + "title": "Updated Title", + "content": "Updated Content", + } + jsonData, _ := json.Marshal(updatedData) + + req, _ := http.NewRequest(http.MethodPut, "/api/notes/nonexistent", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestDeleteNote(t *testing.T) { + // Создаем мок-репозиторий с тестовой заметкой + repo := NewMockRepository() + testNote := model.NewNote("Test Title", "Test Content") + repo.Save(testNote) + + handler := NewNoteHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.DELETE("/api/notes/:id", handler.DeleteNote) + + req, _ := http.NewRequest(http.MethodDelete, "/api/notes/"+testNote.GetID(), nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNoContent, w.Code) + + // Проверяем, что заметка действительно удалена + assert.Nil(t, repo.GetByID("note", testNote.GetID())) +} + +func TestDeleteNoteNotFound(t *testing.T) { + repo := NewMockRepository() + handler := NewNoteHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.DELETE("/api/notes/:id", handler.DeleteNote) + + req, _ := http.NewRequest(http.MethodDelete, "/api/notes/nonexistent", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestGetAllNotes(t *testing.T) { + // Создаем мок-репозиторий с несколькими заметками + repo := NewMockRepository() + note1 := model.NewNote("Title 1", "Content 1") + note2 := model.NewNote("Title 2", "Content 2") + repo.Save(note1) + repo.Save(note2) + + handler := NewNoteHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.GET("/api/notes", handler.GetAllNotes) + + req, _ := http.NewRequest(http.MethodGet, "/api/notes", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var responseNotes []model.Note + err := json.Unmarshal(w.Body.Bytes(), &responseNotes) + assert.NoError(t, err) + assert.Len(t, responseNotes, 2) + + // Проверяем, что обе заметки присутствуют в ответе + foundNote1 := false + foundNote2 := false + for _, note := range responseNotes { + if note.GetID() == note1.GetID() { + foundNote1 = true + assert.Equal(t, "Title 1", note.GetTitle()) + assert.Equal(t, "Content 1", note.GetContent()) + } + if note.GetID() == note2.GetID() { + foundNote2 = true + assert.Equal(t, "Title 2", note.GetTitle()) + assert.Equal(t, "Content 2", note.GetContent()) + } + } + assert.True(t, foundNote1) + assert.True(t, foundNote2) +} diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go new file mode 100644 index 0000000..eb6511e --- /dev/null +++ b/internal/handler/user_handler.go @@ -0,0 +1,219 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/rd2w/go-notes/internal/middleware" + "github.com/rd2w/go-notes/internal/model" + "github.com/rd2w/go-notes/internal/repository" +) + +// UserHandler структура для обработки HTTP запросов, связанных с пользователями +type UserHandler struct { + repo repository.Repository +} + +// NewUserHandler создает новый экземпляр UserHandler +func NewUserHandler(repo repository.Repository) *UserHandler { + return &UserHandler{ + repo: repo, + } +} + +// CreateUser создает нового пользователя +// @Summary Создать нового пользователя +// @Description Создает нового пользователя с указанными данными +// @Tags users +// @Accept json +// @Produce json +// @Param user body createUserRequest true "Пользователь" +// @Success 201 {object} model.User +// @Failure 400 {object} map[string]string +// @Router /api/users [post] +func (h *UserHandler) CreateUser(c *gin.Context) { + var req createUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user, err := model.NewUser(req.Username, req.Email, req.Password) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) + return + } + + h.repo.Save(user) + c.JSON(http.StatusCreated, user) +} + +// createUserRequest структура для запроса создания пользователя +type createUserRequest struct { + Username string `json:"username" binding:"required"` + Email string `json:"email" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// GetUser возвращает пользователя по ID +// @Summary Получить пользователя по ID +// @Description Возвращает пользователя по указанному ID +// @Tags users +// @Produce json +// @Param id path string true "ID пользователя" +// @Success 200 {object} model.User +// @Failure 404 {object} map[string]string +// @Router /api/users/{id} [get] +func (h *UserHandler) GetUser(c *gin.Context) { + id := c.Param("id") + entity := h.repo.GetByID("user", id) + if entity == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + user, ok := entity.(*model.User) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cast entity to user"}) + return + } + + c.JSON(http.StatusOK, user) +} + +// UpdateUser обновляет пользователя +// @Summary Обновить пользователя +// @Description Обновляет пользователя с указанным ID +// @Tags users +// @Accept json +// @Produce json +// @Param id path string true "ID пользователя" +// @Param user body model.User true "Обновленный пользователь" +// @Success 200 {object} model.User +// @Failure 400 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Router /api/users/{id} [put] +func (h *UserHandler) UpdateUser(c *gin.Context) { + id := c.Param("id") + entity := h.repo.GetByID("user", id) + if entity == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + user, ok := entity.(*model.User) + if !ok { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cast entity to user"}) + return + } + + var updatedUser model.User + if err := c.ShouldBindJSON(&updatedUser); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + user.SetUsername(updatedUser.GetUsername()) + user.SetEmail(updatedUser.GetEmail()) + + h.repo.Save(user) + c.JSON(http.StatusOK, user) +} + +// DeleteUser удаляет пользователя +// @Summary Удалить пользователя +// @Description Удаляет пользователя с указанным ID +// @Tags users +// @Produce json +// @Param id path string true "ID пользователя" +// @Success 204 {object} map[string]string +// @Failure 404 {object} map[string]string +// @Router /api/users/{id} [delete] +func (h *UserHandler) DeleteUser(c *gin.Context) { + id := c.Param("id") + deleted := h.repo.DeleteByID("user", id) + if !deleted { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + c.JSON(http.StatusNoContent, gin.H{"message": "User deleted successfully"}) +} + +// GetAllUsers возвращает всех пользователей +// @Summary Получить всех пользователей +// @Description Возвращает список всех пользователей +// @Tags users +// @Produce json +// @Success 200 {array} model.User +// @Router /api/users [get] +func (h *UserHandler) GetAllUsers(c *gin.Context) { + entities := h.repo.GetAllByType("user") + users := make([]*model.User, 0) + + for _, entity := range entities { + user, ok := entity.(*model.User) + if !ok { + continue + } + users = append(users, user) + } + + c.JSON(http.StatusOK, users) +} + +// Login обрабатывает аутентификацию пользователя +// @Summary Аутентификация пользователя +// @Description Аутентифицирует пользователя и возвращает JWT токен +// @Tags auth +// @Accept json +// @Produce json +// @Param credentials body loginRequest true "Учетные данные" +// @Success 200 {object} loginResponse +// @Failure 400 {object} map[string]string +// @Failure 401 {object} map[string]string +// @Router /api/login [post] +func (h *UserHandler) Login(c *gin.Context) { + var req loginRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Находим пользователя по имени + entities := h.repo.GetAllByType("user") + var user *model.User + for _, entity := range entities { + u, ok := entity.(*model.User) + if ok && u.GetUsername() == req.Username { + user = u + break + } + } + + if user == nil || !user.CheckPassword(req.Password) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) + return + } + + token, err := middleware.GenerateJWT(user.GetUsername()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) + return + } + + c.JSON(http.StatusOK, loginResponse{ + Token: token, + }) +} + +// loginRequest структура для запроса аутентификации +type loginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// loginResponse структура для ответа аутентификации +type loginResponse struct { + Token string `json:"token"` +} diff --git a/internal/handler/user_handler_test.go b/internal/handler/user_handler_test.go new file mode 100644 index 0000000..aa71a14 --- /dev/null +++ b/internal/handler/user_handler_test.go @@ -0,0 +1,302 @@ +package handler + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/rd2w/go-notes/internal/model" + "github.com/stretchr/testify/assert" +) + +func init() { + gin.SetMode(gin.TestMode) +} + +func TestCreateUser(t *testing.T) { + repo := NewMockRepository() + handler := NewUserHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.POST("/api/users", handler.CreateUser) + + // Подготовка тестовых данных + requestBody := createUserRequest{ + Username: "testuser", + Email: "test@example.com", + Password: "password123", + } + jsonData, _ := json.Marshal(requestBody) + + req, _ := http.NewRequest(http.MethodPost, "/api/users", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code) + + var responseUser model.User + err := json.Unmarshal(w.Body.Bytes(), &responseUser) + assert.NoError(t, err) + assert.Equal(t, "testuser", responseUser.GetUsername()) + assert.Equal(t, "test@example.com", responseUser.GetEmail()) + assert.NotEmpty(t, responseUser.GetID()) +} + +func TestGetUser(t *testing.T) { + // Создаем мок-репозиторий с тестовым пользователем + repo := NewMockRepository() + testUser, err := model.NewUser("testuser", "test@example.com", "password123") + assert.NoError(t, err) + repo.Save(testUser) + + handler := NewUserHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.GET("/api/users/:id", handler.GetUser) + + req, _ := http.NewRequest(http.MethodGet, "/api/users/"+testUser.GetID(), nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var responseUser model.User + err = json.Unmarshal(w.Body.Bytes(), &responseUser) + assert.NoError(t, err) + assert.Equal(t, testUser.GetID(), responseUser.GetID()) + assert.Equal(t, testUser.GetUsername(), responseUser.GetUsername()) + assert.Equal(t, testUser.GetEmail(), responseUser.GetEmail()) +} + +func TestGetUserNotFound(t *testing.T) { + repo := NewMockRepository() + handler := NewUserHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.GET("/api/users/:id", handler.GetUser) + + req, _ := http.NewRequest(http.MethodGet, "/api/users/nonexistent", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestUpdateUser(t *testing.T) { + // Создаем мок-репозиторий с тестовым пользователем + repo := NewMockRepository() + testUser, err := model.NewUser("originaluser", "original@example.com", "password123") + assert.NoError(t, err) + repo.Save(testUser) + + handler := NewUserHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.PUT("/api/users/:id", handler.UpdateUser) + + // Подготовка обновленных данных + updatedData := map[string]interface{}{ + "username": "updateduser", + "email": "updated@example.com", + } + jsonData, _ := json.Marshal(updatedData) + + req, _ := http.NewRequest(http.MethodPut, "/api/users/"+testUser.GetID(), bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var responseUser model.User + err = json.Unmarshal(w.Body.Bytes(), &responseUser) + assert.NoError(t, err) + assert.Equal(t, testUser.GetID(), responseUser.GetID()) + assert.Equal(t, "updateduser", responseUser.GetUsername()) + assert.Equal(t, "updated@example.com", responseUser.GetEmail()) +} + +func TestUpdateUserNotFound(t *testing.T) { + repo := NewMockRepository() + handler := NewUserHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.PUT("/api/users/:id", handler.UpdateUser) + + updatedData := map[string]interface{}{ + "username": "updateduser", + "email": "updated@example.com", + } + jsonData, _ := json.Marshal(updatedData) + + req, _ := http.NewRequest(http.MethodPut, "/api/users/nonexistent", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestDeleteUser(t *testing.T) { + // Создаем мок-репозиторий с тестовым пользователем + repo := NewMockRepository() + testUser, err := model.NewUser("testuser", "test@example.com", "password123") + assert.NoError(t, err) + repo.Save(testUser) + + handler := NewUserHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.DELETE("/api/users/:id", handler.DeleteUser) + + req, _ := http.NewRequest(http.MethodDelete, "/api/users/"+testUser.GetID(), nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNoContent, w.Code) + + // Проверяем, что пользователь действительно удален + assert.Nil(t, repo.GetByID("user", testUser.GetID())) +} + +func TestDeleteUserNotFound(t *testing.T) { + repo := NewMockRepository() + handler := NewUserHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.DELETE("/api/users/:id", handler.DeleteUser) + + req, _ := http.NewRequest(http.MethodDelete, "/api/users/nonexistent", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) +} + +func TestGetAllUsers(t *testing.T) { + // Создаем мок-репозиторий с несколькими пользователями + repo := NewMockRepository() + user1, err := model.NewUser("user1", "user1@example.com", "password123") + assert.NoError(t, err) + user2, err := model.NewUser("user2", "user2@example.com", "password456") + assert.NoError(t, err) + repo.Save(user1) + repo.Save(user2) + + handler := NewUserHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.GET("/api/users", handler.GetAllUsers) + + req, _ := http.NewRequest(http.MethodGet, "/api/users", nil) + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var responseUsers []*model.User + err = json.Unmarshal(w.Body.Bytes(), &responseUsers) + assert.NoError(t, err) + assert.Len(t, responseUsers, 2) + + // Проверяем, что оба пользователя присутствуют в ответе + foundUser1 := false + foundUser2 := false + for _, user := range responseUsers { + if user.GetID() == user1.GetID() { + foundUser1 = true + assert.Equal(t, "user1", user.GetUsername()) + assert.Equal(t, "user1@example.com", user.GetEmail()) + } + if user.GetID() == user2.GetID() { + foundUser2 = true + assert.Equal(t, "user2", user.GetUsername()) + assert.Equal(t, "user2@example.com", user.GetEmail()) + } + } + assert.True(t, foundUser1) + assert.True(t, foundUser2) +} + +func TestLogin(t *testing.T) { + // Создаем мок-репозиторий с тестовым пользователем + repo := NewMockRepository() + testUser, err := model.NewUser("testuser", "test@example.com", "password123") + assert.NoError(t, err) + repo.Save(testUser) + + handler := NewUserHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.POST("/api/login", handler.Login) + + // Подготовка данных для входа + loginData := loginRequest{ + Username: "testuser", + Password: "password123", + } + jsonData, _ := json.Marshal(loginData) + + req, _ := http.NewRequest(http.MethodPost, "/api/login", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var response loginResponse + err = json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.NotEmpty(t, response.Token) +} + +func TestLoginInvalidCredentials(t *testing.T) { + // Создаем мок-репозиторий с тестовым пользователем + repo := NewMockRepository() + testUser, err := model.NewUser("testuser", "test@example.com", "password123") + assert.NoError(t, err) + repo.Save(testUser) + + handler := NewUserHandler(repo) + + gin.SetMode(gin.TestMode) + router := gin.New() + router.POST("/api/login", handler.Login) + + // Подготовка неверных данных для входа + loginData := loginRequest{ + Username: "testuser", + Password: "wrongpassword", + } + jsonData, _ := json.Marshal(loginData) + + req, _ := http.NewRequest(http.MethodPost, "/api/login", bytes.NewBuffer(jsonData)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusUnauthorized, w.Code) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go deleted file mode 100644 index 607c0ef..0000000 --- a/internal/logger/logger.go +++ /dev/null @@ -1,59 +0,0 @@ -package logger - -import ( - "context" - "log" - "time" - - "github.com/rd2w/go-notes/internal/repository" -) - -// Logger отвечает за логирование изменений в данных -type Logger struct { - repo repository.Repository - ctx context.Context - interval time.Duration -} - -// NewLogger создает новый экземпляр логгера -func NewLogger(repo repository.Repository, ctx context.Context, interval time.Duration) *Logger { - return &Logger{ - repo: repo, - ctx: ctx, - interval: interval, - } -} - -// Start запускает процесс логирования изменений -func (l *Logger) Start() { - go func() { - lastNoteCount := 0 - ticker := time.NewTicker(l.interval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - currentNoteCount := l.repo.GetNotesCount() - - if currentNoteCount > lastNoteCount { - // Получаем только новые заметки, добавленные после последней проверки - newNotes := l.repo.GetNewNotes(lastNoteCount) - log.Printf("Логгер: обнаружено %d новых заметок", len(newNotes)) - - for _, note := range newNotes { - log.Printf("Логгер: НОВАЯ ЗАМЕТКА - ID: %s, Заголовок: %s, Создана: %s", - note.GetID(), - note.GetTitle(), - note.GetCreatedAt().Format("15:04:05")) - } - - lastNoteCount = currentNoteCount - } - case <-l.ctx.Done(): - log.Println("Логгер: завершение работы") - return - } - } - }() -} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go deleted file mode 100644 index 557dd2a..0000000 --- a/internal/logger/logger_test.go +++ /dev/null @@ -1,460 +0,0 @@ -package logger - -import ( - "bytes" - "context" - "log" - "strings" - "sync" - "testing" - "time" - - "github.com/rd2w/go-notes/internal/model" - "github.com/rd2w/go-notes/internal/repository/storage/ram" - "github.com/rd2w/go-notes/internal/service" - "github.com/stretchr/testify/assert" -) - -// safeBuffer потокобезопасный буфер для логов -type safeBuffer struct { - buf bytes.Buffer - mu sync.RWMutex -} - -func (s *safeBuffer) Write(p []byte) (n int, err error) { - s.mu.Lock() - defer s.mu.Unlock() - return s.buf.Write(p) -} - -func (s *safeBuffer) String() string { - s.mu.RLock() - defer s.mu.RUnlock() - return s.buf.String() -} - -func TestLogger_IntegrationWithService(t *testing.T) { - var buf safeBuffer - oldOutput := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(oldOutput) - - // Создаем компоненты как в main() - ctx, cancel := context.WithCancel(context.Background()) - - repo := ram.NewRamRepository() - svc := service.NewService(repo, ctx, 50*time.Millisecond) - logger := NewLogger(repo, ctx, 30*time.Millisecond) - - // Запускаем компоненты - go logger.Start() - svc.Start() - - // Ждем достаточно времени для обработки нескольких итераций - time.Sleep(100 * time.Millisecond) - - // Завершаем работу через контекст - cancel() - - // Ждем немного, чтобы логгер успел завершить работу и вывести сообщения - time.Sleep(10 * time.Millisecond) - - output := buf.String() - - // Проверяем базовую функциональность - assert.Contains(t, output, "Логгер: обнаружено", "Должны быть сообщения о обнаружении заметок") - assert.Contains(t, output, "НОВАЯ ЗАМЕТКА - ID:", "Должны быть сообщения о новых заметках") - - // Проверяем, что были созданы заметки - notesCount := repo.GetNotesCount() - assert.True(t, notesCount > 0, "Должны быть созданы заметки") - - t.Logf("Создано %d заметок", notesCount) - t.Logf("Вывод логгера:\n%s", output) -} - -func TestLogger_StopWithContext(t *testing.T) { - var buf safeBuffer - oldOutput := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(oldOutput) - - ctx, cancel := context.WithCancel(context.Background()) - - repo := ram.NewRamRepository() - - // Увеличиваем интервал логгера чтобы он реже проверял - logger := NewLogger(repo, ctx, 100*time.Millisecond) - - // Запускаем компоненты - go logger.Start() - - // Даем время на старт логгера - time.Sleep(10 * time.Millisecond) - - // Вручную отправляем заметки напрямую в репозиторий, минуя сервис - // Это гарантирует, что заметки будут сохранены до запуска логгера - note1 := model.NewNote("Test Note 1", "Content 1") - note2 := model.NewNote("Test Note 2", "Content 2") - - repo.Save(note1) - repo.Save(note2) - - // Даем время на сохранение в репозиторий - time.Sleep(20 * time.Millisecond) - - // Теперь останавливаем ДО того как логгер успеет проверить - cancel() - - // Даем время на завершение - time.Sleep(50 * time.Millisecond) - - output := buf.String() - - // Проверяем сообщение о завершении - assert.Contains(t, output, "Логгер: завершение работы", "Должно быть сообщение о завершении работы") - - // В этом тесте мы специально останавливаем логгер ДО того как он проверит заметки - // Поэтому он может не успеть залогировать заметки - это нормальное поведение - t.Logf("Тест завершен: логгер корректно остановился по сигналу контекста") -} - -func TestLogger_MultipleNoteGeneration(t *testing.T) { - var buf safeBuffer - oldOutput := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(oldOutput) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - logger := NewLogger(repo, ctx, 40*time.Millisecond) - - // Запускаем компоненты - go logger.Start() - - // Даем время на старт - time.Sleep(20 * time.Millisecond) - - // Вручную сохраняем несколько заметок с разными интервалами - go func() { - notes := []*model.Note{ - model.NewNote("First Note", "First content"), - model.NewNote("Second Note", "Second content"), - model.NewNote("Third Note", "Third content"), - } - - for i, note := range notes { - // Увеличиваем задержку между отправками - time.Sleep(time.Duration(i*80) * time.Millisecond) - repo.Save(note) - t.Logf("Сохранена заметка %d: %s", i+1, note.GetTitle()) - } - }() - - // Ждем обработки всех заметок (увеличиваем время ожидания) - time.Sleep(400 * time.Millisecond) - - output := buf.String() - - // Проверяем, что все заметки были обработаны - // Используем более мягкие проверки - hasFirstNote := strings.Contains(output, "First Note") - hasSecondNote := strings.Contains(output, "Second Note") - hasThirdNote := strings.Contains(output, "Third Note") - - // Логируем что было найдено - t.Logf("Найдены заметки: First=%t, Second=%t, Third=%t", - hasFirstNote, hasSecondNote, hasThirdNote) - - // Проверяем структуру вывода - loggerLines := strings.Count(output, "Логгер:") - newNoteLines := strings.Count(output, "НОВАЯ ЗАМЕТКА") - - t.Logf("Всего строк логгера: %d, строк о новых заметках: %d", - loggerLines, newNoteLines) - - // Убеждаемся что логгер вообще работал - assert.True(t, loggerLines > 0, "Логгер должен был записать хотя бы одну строку") - assert.True(t, newNoteLines > 0, "Должна быть хотя бы одна запись о новой заметке") -} - -func TestLogger_NoNotesScenario(t *testing.T) { - var buf safeBuffer - oldOutput := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(oldOutput) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - logger := NewLogger(repo, ctx, 30*time.Millisecond) - - // Запускаем только логгер, но не отправляем заметки - go logger.Start() - - // Ждем несколько интервалов - time.Sleep(100 * time.Millisecond) - - output := buf.String() - - // Не должно быть сообщений о новых заметках - assert.NotContains(t, output, "обнаружено", "Не должно быть сообщений об обнаружении без заметок") - assert.NotContains(t, output, "НОВАЯ ЗАМЕТКА", "Не должно быть сообщений о новых заметках без данных") -} - -func TestLogger_ConcurrentAccess(t *testing.T) { - var buf safeBuffer - oldOutput := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(oldOutput) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - // Увеличиваем интервал для стабильности - logger := NewLogger(repo, ctx, 30*time.Millisecond) - - // Запускаем компоненты - go logger.Start() - - // Даем время на старт - time.Sleep(20 * time.Millisecond) - - // Сохраняем много заметок быстро - go func() { - for i := 0; i < 5; i++ { // Уменьшаем количество для надежности - note := model.NewNote( - "Concurrent Note "+string(rune('A'+i)), - "Content for concurrent note", - ) - repo.Save(note) - time.Sleep(10 * time.Millisecond) // Увеличиваем задержку между отправками - } - }() - - // Ждем обработки (увеличиваем время ожидания) - time.Sleep(300 * time.Millisecond) - - output := buf.String() - finalNoteCount := repo.GetNotesCount() - - // Проверяем, что все заметки были обработаны - assert.Equal(t, 5, finalNoteCount, "Должны быть созданы все 5 заметок") - - newNoteCount := strings.Count(output, "НОВАЯ ЗАМЕТКА") - t.Logf("Создано %d заметок, найдено %d записей в логе", - finalNoteCount, newNoteCount) - - // Мягкая проверка - хотя бы некоторые заметки должны быть залогированы - assert.True(t, newNoteCount > 0, - "Должны быть логи хотя бы для некоторых заметок") -} - -func TestLogger_TimeFormatConsistency(t *testing.T) { - var buf safeBuffer - oldOutput := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(oldOutput) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - logger := NewLogger(repo, ctx, 50*time.Millisecond) - - go logger.Start() - - // Даем время на старт - time.Sleep(20 * time.Millisecond) - - // Сохраняем одну заметку - repo.Save(model.NewNote("Time Test", "Testing time format")) - - // Ждем обработки (увеличиваем время) - time.Sleep(150 * time.Millisecond) - - output := buf.String() - - // Проверяем формат времени (должен быть как в main: 15:04:05) - if strings.Contains(output, "Создана: ") { - // Ищем время после "Создана: " - timePart := strings.Split(strings.Split(output, "Создана: ")[1], "\n")[0] - - // Парсим время чтобы убедиться в корректности формата - _, err := time.Parse("15:04:05", timePart) - assert.NoError(t, err, "Время должно быть в формате HH:MM:SS, получено: %s", timePart) - - t.Logf("Время в корректном формате: %s", timePart) - } else { - t.Log("Сообщение о времени создания не найдено в выводе") - } -} - -// TestLogger_SimpleCase тестирует простой случай с одной заметкой -func TestLogger_SimpleCase(t *testing.T) { - var buf safeBuffer - oldOutput := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(oldOutput) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - // Очень короткий интервал для быстрого обнаружения - logger := NewLogger(repo, ctx, 10*time.Millisecond) - - go logger.Start() - - // Даем время на полный старт - time.Sleep(15 * time.Millisecond) - - // Сохраняем одну заметку - note := model.NewNote("Simple Test Note", "Simple content") - repo.Save(note) - - // Ждем гарантированной обработки - time.Sleep(50 * time.Millisecond) - - output := buf.String() - - // Простая проверка - логгер должен что-то залогировать - assert.Contains(t, output, "Логгер:", "Должны быть сообщения от логгера") - - // Дополнительная проверка если есть новые заметки - if strings.Contains(output, "обнаружено") { - assert.Contains(t, output, "НОВАЯ ЗАМЕТКА", - "Если есть сообщение об обнаружении, должна быть информация о заметке") - } -} - -// TestLogger_SeesNotesBeforeStop тестирует что логгер успевает увидеть заметки перед остановкой -func TestLogger_SeesNotesBeforeStop(t *testing.T) { - var buf safeBuffer - oldOutput := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(oldOutput) - - ctx, cancel := context.WithCancel(context.Background()) - - repo := ram.NewRamRepository() - - // Очень короткий интервал для быстрого обнаружения - logger := NewLogger(repo, ctx, 10*time.Millisecond) - - // Запускаем компоненты - go logger.Start() - - // Даем время на старт логгера - time.Sleep(5 * time.Millisecond) - - // Сохраняем заметки - note1 := model.NewNote("Test Note 1", "Content 1") - note2 := model.NewNote("Test Note 2", "Content 2") - - repo.Save(note1) - repo.Save(note2) - - // Ждем пока логгер гарантированно проверит (2 интервала + запас) - time.Sleep(30 * time.Millisecond) - - // Теперь останавливаем - cancel() - - // Даем время на завершение - time.Sleep(20 * time.Millisecond) - - output := buf.String() - - // Проверяем что логгер успел обработать заметки - assert.Contains(t, output, "Логгер: обнаружено", "Логгер должен был обнаружить заметки") - assert.Contains(t, output, "НОВАЯ ЗАМЕТКА", "Логгер должен был залогировать заметки") - assert.Contains(t, output, "Логгер: завершение работы", "Должно быть сообщение о завершении") - - t.Logf("Логгер успел обработать заметки перед остановкой") -} - -// TestLogger_ImmediateStop тестирует немедленную остановку -func TestLogger_ImmediateStop(t *testing.T) { - var buf safeBuffer - oldOutput := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(oldOutput) - - ctx, cancel := context.WithCancel(context.Background()) - - repo := ram.NewRamRepository() - logger := NewLogger(repo, ctx, 10*time.Millisecond) - - // Останавливаем СРАЗУ ЖЕ - cancel() - - // Запускаем компоненты после остановки - go logger.Start() - - // Даем время на обработку завершения - time.Sleep(30 * time.Millisecond) - - output := buf.String() - - // Должно быть только сообщение о завершении, без заметок - assert.Contains(t, output, "Логгер: завершение работы") - - // Не должно быть сообщений о заметках т.к. остановили сразу - if strings.Contains(output, "Логгер: обнаружено") { - t.Logf("Предупреждение: логгер обнаружил заметки после остановки, но это возможно в условиях гонки") - } -} - -// TestLogger_GracefulStop тестирует плавную остановку -func TestLogger_GracefulStop(t *testing.T) { - var buf safeBuffer - oldOutput := log.Writer() - log.SetOutput(&buf) - defer log.SetOutput(oldOutput) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() // На этот раз используем defer - - repo := ram.NewRamRepository() - - // Нормальный интервал - logger := NewLogger(repo, ctx, 50*time.Millisecond) - - // Запускаем компоненты - go logger.Start() - - // Даем время на старт - time.Sleep(10 * time.Millisecond) - - // Сохраняем несколько заметок в разных моментах времени - go func() { - notes := []*model.Note{ - model.NewNote("Note 1", "Content 1"), - model.NewNote("Note 2", "Content 2"), - model.NewNote("Note 3", "Content 3"), - } - - for i, note := range notes { - time.Sleep(time.Duration(i*40) * time.Millisecond) - repo.Save(note) - } - }() - - // Ждем пока все обработается - time.Sleep(200 * time.Millisecond) - - output := buf.String() - - // Проверяем что логгер работал нормально - hasLoggerOutput := strings.Contains(output, "Логгер: обнаружено") || - strings.Contains(output, "НОВАЯ ЗАМЕТКА") - - assert.True(t, hasLoggerOutput, "Логгер должен был обработать заметки. Вывод: %s", output) - - t.Logf("Логгер корректно работал до завершения теста") -} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go new file mode 100644 index 0000000..8d127b0 --- /dev/null +++ b/internal/middleware/auth.go @@ -0,0 +1,72 @@ +package middleware + +import ( + "log" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +var jwtKey = []byte("my_secret_key") + +// Claims структура для хранения данных в JWT токене +type Claims struct { + Username string `json:"username"` + jwt.RegisteredClaims +} + +// GenerateJWT генерирует JWT токен для пользователя +func GenerateJWT(username string) (string, error) { + expirationTime := time.Now().Add(24 * time.Hour) + claims := &Claims{ + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtKey) +} + +// AuthMiddleware проверяет наличие и валидность JWT токена +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + log.Printf("Получен заголовок Authorization: %s", authHeader) + if authHeader == "" { + log.Printf("Заголовок Authorization отсутствует") + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"}) + c.Abort() + return + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == authHeader { + log.Printf("Заголовок Authorization не содержит префикс Bearer") + c.JSON(http.StatusUnauthorized, gin.H{"error": "Bearer token is required"}) + c.Abort() + return + } + + log.Printf("Извлечен JWT токен: %s", tokenString) + claims := &Claims{} + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return jwtKey, nil + }) + + if err != nil || !token.Valid { + log.Printf("Ошибка при проверке JWT токена: %v", err) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() + return + } + + log.Printf("JWT токен действителен, пользователь: %s", claims.Username) + c.Set("username", claims.Username) + c.Next() + } +} diff --git a/internal/middleware/auth_test.go b/internal/middleware/auth_test.go new file mode 100644 index 0000000..e12e5d4 --- /dev/null +++ b/internal/middleware/auth_test.go @@ -0,0 +1,192 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" +) + +func init() { + // Устанавливаем тестовый ключ для JWT + jwtKey = []byte("test_secret_key_for_testing") +} + +func TestGenerateJWT(t *testing.T) { + username := "testuser" + tokenString, err := GenerateJWT(username) + + assert.NoError(t, err) + assert.NotEmpty(t, tokenString) + + // Проверяем, что токен может быть расшифрован + claims := &Claims{} + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return jwtKey, nil + }) + + assert.NoError(t, err) + assert.True(t, token.Valid) + assert.Equal(t, username, claims.Username) + + // Проверяем, что токен истекает в течение 24 часов + assert.WithinDuration(t, time.Now().Add(24*time.Hour), time.Unix(claims.ExpiresAt.Unix(), 0), 10*time.Second) +} + +func TestAuthMiddleware_ValidToken(t *testing.T) { + gin.SetMode(gin.TestMode) + + // Создаем валидный токен + username := "testuser" + tokenString, _ := GenerateJWT(username) + + // Создаем запрос с валидным токеном + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("Authorization", "Bearer "+tokenString) + + // Создаем gin контекст + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + // Применяем middleware + authMiddleware := AuthMiddleware() + authMiddleware(c) + + // Проверяем, что запрос не был прерван + assert.Equal(t, http.StatusOK, w.Code) + + // Проверяем, что username был установлен в контексте + usernameFromContext, exists := c.Get("username") + assert.True(t, exists) + assert.Equal(t, username, usernameFromContext) +} + +func TestAuthMiddleware_NoAuthHeader(t *testing.T) { + gin.SetMode(gin.TestMode) + + // Создаем запрос без заголовка Authorization + req, _ := http.NewRequest("GET", "/test", nil) + + // Создаем gin контекст + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + // Применяем middleware + authMiddleware := AuthMiddleware() + authMiddleware(c) + + // Проверяем, что запрос был прерван с ошибкой 401 + assert.Equal(t, http.StatusUnauthorized, w.Code) + + // Проверяем, что в теле ответа содержится ошибка + assert.Contains(t, w.Body.String(), "Authorization header is required") +} + +func TestAuthMiddleware_InvalidPrefix(t *testing.T) { + gin.SetMode(gin.TestMode) + + // Создаем запрос с неверным префиксом в заголовке Authorization + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("Authorization", "InvalidPrefix token123") + + // Создаем gin контекст + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + // Применяем middleware + authMiddleware := AuthMiddleware() + authMiddleware(c) + + // Проверяем, что запрос был прерван с ошибкой 401 + assert.Equal(t, http.StatusUnauthorized, w.Code) + + // Проверяем, что в теле ответа содержится ошибка + assert.Contains(t, w.Body.String(), "Bearer token is required") +} + +func TestAuthMiddleware_InvalidToken(t *testing.T) { + gin.SetMode(gin.TestMode) + + // Создаем запрос с невалидным токеном + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("Authorization", "Bearer invalid_token_string") + + // Создаем gin контекст + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + // Применяем middleware + authMiddleware := AuthMiddleware() + authMiddleware(c) + + // Проверяем, что запрос был прерван с ошибкой 401 + assert.Equal(t, http.StatusUnauthorized, w.Code) + + // Проверяем, что в теле ответа содержится ошибка + assert.Contains(t, w.Body.String(), "Invalid token") +} + +func TestAuthMiddleware_ExpiredToken(t *testing.T) { + gin.SetMode(gin.TestMode) + + // Создаем истекший токен + expiredClaims := &Claims{ + Username: "testuser", + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)), // Токен истек час назад + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, expiredClaims) + expiredToken, _ := token.SignedString(jwtKey) + + // Создаем запрос с истекшим токеном + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("Authorization", "Bearer "+expiredToken) + + // Создаем gin контекст + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + // Применяем middleware + authMiddleware := AuthMiddleware() + authMiddleware(c) + + // Проверяем, что запрос был прерван с ошибкой 401 + assert.Equal(t, http.StatusUnauthorized, w.Code) + + // Проверяем, что в теле ответа содержится ошибка + assert.Contains(t, w.Body.String(), "Invalid token") +} + +func TestAuthMiddleware_MalformedToken(t *testing.T) { + gin.SetMode(gin.TestMode) + + // Создаем запрос с неправильно сформированным токеном + req, _ := http.NewRequest("GET", "/test", nil) + req.Header.Set("Authorization", "Bearer malformed_token") + + // Создаем gin контекст + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + // Применяем middleware + authMiddleware := AuthMiddleware() + authMiddleware(c) + + // Проверяем, что запрос был прерван с ошибкой 401 + assert.Equal(t, http.StatusUnauthorized, w.Code) + + // Проверяем, что в теле ответа содержится ошибка + assert.Contains(t, w.Body.String(), "Invalid token") +} diff --git a/internal/model/common_test.go b/internal/model/common_test.go new file mode 100644 index 0000000..6b05e5c --- /dev/null +++ b/internal/model/common_test.go @@ -0,0 +1,180 @@ +package model + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTimeFieldsInitialization(t *testing.T) { + tf := &TimeFields{} + + // Проверяем, что временные метки изначально равны нулю + assert.Zero(t, tf.GetCreatedAt()) + assert.Zero(t, tf.GetUpdatedAt()) + + // Инициализируем временные метки + tf.initializeTimestamps() + + // Проверяем, что временные метки теперь не равны нулю + assert.NotZero(t, tf.GetCreatedAt()) + assert.NotZero(t, tf.GetUpdatedAt()) + + // Проверяем, что время создания и обновления примерно равны + assert.WithinDuration(t, tf.GetCreatedAt(), tf.GetUpdatedAt(), time.Second) +} + +func TestTimeFieldsUpdateTimestamp(t *testing.T) { + tf := &TimeFields{} + tf.initializeTimestamps() + + originalUpdatedAt := tf.GetUpdatedAt() + + // Ждем немного времени + time.Sleep(10 * time.Millisecond) + + // Обновляем временную метку + tf.updateTimestamp() + + // Проверяем, что время обновления изменилось + assert.True(t, tf.GetUpdatedAt().After(originalUpdatedAt)) + + // Проверяем, что время создания осталось прежним + assert.WithinDuration(t, tf.GetCreatedAt(), originalUpdatedAt, time.Second) +} + +func TestTimeFieldsGetters(t *testing.T) { + tf := &TimeFields{} + tf.initializeTimestamps() + + createdAt := tf.GetCreatedAt() + updatedAt := tf.GetUpdatedAt() + + assert.NotZero(t, createdAt) + assert.NotZero(t, updatedAt) + assert.WithinDuration(t, createdAt, updatedAt, time.Second) +} + +func TestTimeFieldsMultipleUpdates(t *testing.T) { + tf := &TimeFields{} + tf.initializeTimestamps() + + // Сохраняем начальные времена + initialCreatedAt := tf.GetCreatedAt() + initialUpdatedAt := tf.GetUpdatedAt() + + // Обновляем несколько раз + for i := 0; i < 5; i++ { + time.Sleep(10 * time.Millisecond) + tf.updateTimestamp() + + // Проверяем, что время создания не изменилось + assert.Equal(t, initialCreatedAt, tf.GetCreatedAt()) + + // Проверяем, что время обновления изменилось + assert.True(t, tf.GetUpdatedAt().After(initialUpdatedAt)) + + initialUpdatedAt = tf.GetUpdatedAt() + } +} + +func TestTimeFieldsMarshalJSON(t *testing.T) { + tf := &TimeFields{} + tf.initializeTimestamps() + + jsonData, err := json.Marshal(tf) + assert.NoError(t, err) + + // Проверяем, что JSON содержит ожидаемые поля + var timeMap map[string]interface{} + err = json.Unmarshal(jsonData, &timeMap) + assert.NoError(t, err) + + assert.Contains(t, timeMap, "created_at") + assert.Contains(t, timeMap, "updated_at") + + // Проверяем, что значения соответствуют ожидаемым + createdAtStr, ok := timeMap["created_at"].(string) + assert.True(t, ok) + + updatedAtStr, ok := timeMap["updated_at"].(string) + assert.True(t, ok) + + createdAt, err := time.Parse(time.RFC3339, createdAtStr) + assert.NoError(t, err) + + updatedAt, err := time.Parse(time.RFC3339, updatedAtStr) + assert.NoError(t, err) + + assert.WithinDuration(t, tf.GetCreatedAt(), createdAt, time.Second) + assert.WithinDuration(t, tf.GetUpdatedAt(), updatedAt, time.Second) +} + +func TestTimeFieldsUnmarshalJSON(t *testing.T) { + // Создаем JSON с временными метками + createdAt := time.Now().Add(-1 * time.Hour) + updatedAt := time.Now() + + jsonStr := `{"created_at":"` + createdAt.Format(time.RFC3339) + `","updated_at":"` + updatedAt.Format(time.RFC3339) + `"}` + + tf := &TimeFields{} + err := tf.UnmarshalJSON([]byte(jsonStr)) + assert.NoError(t, err) + + assert.WithinDuration(t, createdAt, tf.GetCreatedAt(), time.Second) + assert.WithinDuration(t, updatedAt, tf.GetUpdatedAt(), time.Second) +} + +func TestTimeFieldsMarshalUnmarshalRoundTrip(t *testing.T) { + // Создаем TimeFields инициализируем + tf1 := &TimeFields{} + tf1.initializeTimestamps() + + // Сохраняем времена до сериализации + createdAtBefore := tf1.GetCreatedAt() + updatedAtBefore := tf1.GetUpdatedAt() + + // Маршалим в JSON + jsonData, err := json.Marshal(tf1) + assert.NoError(t, err) + + // Создаем новый объект и десериализуем + tf2 := &TimeFields{} + err = tf2.UnmarshalJSON(jsonData) + assert.NoError(t, err) + + // Проверяем, что значения сохранились + assert.WithinDuration(t, createdAtBefore, tf2.GetCreatedAt(), time.Second) + assert.WithinDuration(t, updatedAtBefore, tf2.GetUpdatedAt(), time.Second) + + // Проверяем, что можно снова обновить временную метку + time.Sleep(10 * time.Millisecond) + tf2.updateTimestamp() + + // Время обновления должно быть больше, чем до десериализации + assert.True(t, tf2.GetUpdatedAt().After(updatedAtBefore)) + + // Время создания должно остаться тем же + assert.WithinDuration(t, createdAtBefore, tf2.GetCreatedAt(), time.Second) +} + +func TestTimeFieldsUnmarshalInvalidJSON(t *testing.T) { + // Проверяем десериализацию с неправильным JSON + invalidJSON := `{"created_at":"invalid_time","updated_at":"invalid_time"}` + + tf := &TimeFields{} + err := tf.UnmarshalJSON([]byte(invalidJSON)) + assert.Error(t, err) + + // Проверяем десериализацию с неполным JSON + incompleteJSON := `{"created_at":"2023-01-01T00:00:00Z"}` + + tf2 := &TimeFields{} + err = tf2.UnmarshalJSON([]byte(incompleteJSON)) + assert.NoError(t, err) + assert.Equal(t, time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC), tf2.GetCreatedAt()) + // updatedAt будет равен нулю, так как не был задан в JSON + assert.Zero(t, tf2.GetUpdatedAt()) +} diff --git a/internal/model/note.go b/internal/model/note.go index 591082b..181c49a 100644 --- a/internal/model/note.go +++ b/internal/model/note.go @@ -4,7 +4,7 @@ import ( "encoding/json" "time" - "github.com/google/uuid" + "github.com/rd2w/go-notes/internal/util" ) // Note представляет сущность заметки @@ -18,7 +18,7 @@ type Note struct { // NewNote создает новую заметку с инициализацией временных меток func NewNote(title, content string) *Note { note := &Note{ - id: generateID(), + id: util.GenerateID(), title: title, content: content, } @@ -56,11 +56,6 @@ func (n *Note) SetContent(newContent string) { n.updateTimestamp() } -// generateID генерирует уникальный идентификатор -func generateID() string { - return uuid.New().String() -} - // JSONNote вспомогательная структура для JSON сериализации type JSONNote struct { ID string `json:"id"` diff --git a/internal/model/user.go b/internal/model/user.go new file mode 100644 index 0000000..5bab79a --- /dev/null +++ b/internal/model/user.go @@ -0,0 +1,192 @@ +package model + +import ( + "encoding/json" + "time" + + "github.com/rd2w/go-notes/internal/util" + "golang.org/x/crypto/bcrypt" +) + +// User представляет сущность пользователя +type User struct { + TimeFields + id string + username string + email string + password string // хешированный пароль +} + +// NewUser создает нового пользователя с инициализацией временных меток +func NewUser(username, email, password string) (*User, error) { + // Хешируем пароль + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + user := &User{ + id: util.GenerateID(), + username: username, + email: email, + password: string(hashedPassword), + } + user.initializeTimestamps() + return user, nil +} + +// NewUserWithPasswordHash создает нового пользователя с уже хешированным паролем (для загрузки из хранилища) +func NewUserWithPasswordHash(username, email, passwordHash string) *User { + user := &User{ + id: util.GenerateID(), + username: username, + email: email, + password: passwordHash, + } + user.initializeTimestamps() + return user +} + +// GetID возвращает идентификатор пользователя (реализация интерфейса Entity) +func (u *User) GetID() string { + return u.id +} + +// GetType возвращает тип сущности (реализация интерфейса Entity) +func (u *User) GetType() string { + return "user" +} + +// GetUsername возвращает имя пользователя +func (u *User) GetUsername() string { + return u.username +} + +// GetEmail возвращает email пользователя +func (u *User) GetEmail() string { + return u.email +} + +// GetPassword возвращает хеш пароля пользователя +func (u *User) GetPassword() string { + return u.password +} + +// CheckPassword проверяет, соответствует ли переданный пароль хешу +func (u *User) CheckPassword(password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(u.password), []byte(password)) + return err == nil +} + +// SetUsername устанавливает новое имя пользователя и обновляет временную метку +func (u *User) SetUsername(newUsername string) { + u.username = newUsername + u.updateTimestamp() +} + +// SetEmail устанавливает новый email и обновляет временную метку +func (u *User) SetEmail(newEmail string) { + u.email = newEmail + u.updateTimestamp() +} + +// SetPassword устанавливает новый пароль (хешируется автоматически) и обновляет временную метку +func (u *User) SetPassword(newPassword string) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + u.password = string(hashedPassword) + u.updateTimestamp() + return nil +} + +// JSONUser вспомогательная структура для JSON сериализации +type JSONUser struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// JSONUserWithPassword вспомогательная структура для JSON сериализации с паролем (для внутреннего хранения) +type JSONUserWithPassword struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// MarshalJSON реализует интерфейс json.Marshaler +// При сериализации в JSON пароль не включается в целях безопасности +func (u *User) MarshalJSON() ([]byte, error) { + return json.Marshal(JSONUser{ + ID: u.id, + Username: u.username, + Email: u.email, + CreatedAt: u.createdAt, + UpdatedAt: u.updatedAt, + }) +} + +// MarshalJSONWithPassword реализует сериализацию с включением пароля (для внутреннего хранения) +func (u *User) MarshalJSONWithPassword() ([]byte, error) { + return json.Marshal(JSONUserWithPassword{ + ID: u.id, + Username: u.username, + Email: u.email, + Password: u.password, + CreatedAt: u.createdAt, + UpdatedAt: u.updatedAt, + }) +} + +// UnmarshalJSONWithPassword реализует десериализацию с извлечением пароля (для внутреннего хранения) +func (u *User) UnmarshalJSONWithPassword(data []byte) error { + var jsonUser JSONUserWithPassword + if err := json.Unmarshal(data, &jsonUser); err != nil { + return err + } + + u.id = jsonUser.ID + u.username = jsonUser.Username + u.email = jsonUser.Email + u.password = jsonUser.Password + u.createdAt = jsonUser.CreatedAt + u.updatedAt = jsonUser.UpdatedAt + + return nil +} + +// UnmarshalJSON реализует интерфейс json.Unmarshaler +func (u *User) UnmarshalJSON(data []byte) error { + // Сначала десериализуем в промежуточную структуру без пароля + var jsonUser JSONUser + if err := json.Unmarshal(data, &jsonUser); err != nil { + return err + } + + // Теперь попробуем десериализовать в структуру с паролем + var fullData map[string]interface{} + if err := json.Unmarshal(data, &fullData); err != nil { + return err + } + + u.id = jsonUser.ID + u.username = jsonUser.Username + u.email = jsonUser.Email + u.createdAt = jsonUser.CreatedAt + u.updatedAt = jsonUser.UpdatedAt + + // Восстанавливаем пароль, если он присутствует в данных + if password, exists := fullData["password"]; exists { + if passwordStr, ok := password.(string); ok { + u.password = passwordStr + } + } + + return nil +} diff --git a/internal/model/user_test.go b/internal/model/user_test.go new file mode 100644 index 0000000..220e7cc --- /dev/null +++ b/internal/model/user_test.go @@ -0,0 +1,273 @@ +package model + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/bcrypt" +) + +func TestNewUser(t *testing.T) { + username := "testuser" + email := "test@example.com" + password := "password123" + + user, err := NewUser(username, email, password) + + assert.NoError(t, err) + assert.NotNil(t, user) + assert.NotEmpty(t, user.GetID()) + assert.Equal(t, username, user.GetUsername()) + assert.Equal(t, email, user.GetEmail()) + assert.NotEmpty(t, user.GetPassword()) + assert.True(t, user.CheckPassword(password)) + assert.False(t, user.CheckPassword("wrongpassword")) + + // Проверяем, что временные метки установлены + assert.NotZero(t, user.GetCreatedAt()) + assert.NotZero(t, user.GetUpdatedAt()) +} + +func TestNewUserWithPasswordHash(t *testing.T) { + username := "testuser" + email := "test@example.com" + password := "password123" + + // Создаем хеш пароля + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + assert.NoError(t, err) + + user := NewUserWithPasswordHash(username, email, string(hashedPassword)) + + assert.NotNil(t, user) + assert.NotEmpty(t, user.GetID()) + assert.Equal(t, username, user.GetUsername()) + assert.Equal(t, email, user.GetEmail()) + assert.Equal(t, string(hashedPassword), user.GetPassword()) + assert.True(t, user.CheckPassword(password)) + assert.False(t, user.CheckPassword("wrongpassword")) + + // Проверяем, что временные метки установлены + assert.NotZero(t, user.GetCreatedAt()) + assert.NotZero(t, user.GetUpdatedAt()) +} + +func TestNewUserError(t *testing.T) { + // Тестируем ошибку при генерации хеша пароля (искусственно вызываем ошибку) + // В реальности bcrypt.GenerateFromPassword редко возвращает ошибки, + // но тестируем этот сценарий на всякий случай + user, err := NewUser("testuser", "test@example.com", "password123") + + // Должно пройти успешно, так как валидный пароль + assert.NoError(t, err) + assert.NotNil(t, user) +} + +func TestGetters(t *testing.T) { + username := "testuser" + email := "test@example.com" + password := "password123" + + user, err := NewUser(username, email, password) + assert.NoError(t, err) + + assert.Equal(t, user.GetID(), user.GetID()) + assert.Equal(t, username, user.GetUsername()) + assert.Equal(t, email, user.GetEmail()) + assert.Equal(t, "user", user.GetType()) + assert.NotEmpty(t, user.GetPassword()) +} + +func TestSetUsername(t *testing.T) { + user, err := NewUser("testuser", "test@example.com", "password123") + assert.NoError(t, err) + + originalUpdatedAt := user.GetUpdatedAt() + + newUsername := "newusername" + user.SetUsername(newUsername) + + assert.Equal(t, newUsername, user.GetUsername()) + assert.True(t, user.GetUpdatedAt().After(originalUpdatedAt)) +} + +func TestSetEmail(t *testing.T) { + user, err := NewUser("testuser", "test@example.com", "password123") + assert.NoError(t, err) + + originalUpdatedAt := user.GetUpdatedAt() + + newEmail := "newemail@example.com" + user.SetEmail(newEmail) + + assert.Equal(t, newEmail, user.GetEmail()) + assert.True(t, user.GetUpdatedAt().After(originalUpdatedAt)) +} + +func TestSetPassword(t *testing.T) { + user, err := NewUser("testuser", "test@example.com", "password123") + assert.NoError(t, err) + + originalUpdatedAt := user.GetUpdatedAt() + + newPassword := "newpassword123" + err = user.SetPassword(newPassword) + assert.NoError(t, err) + + assert.True(t, user.CheckPassword(newPassword)) + assert.False(t, user.CheckPassword("password123")) + assert.True(t, user.GetUpdatedAt().After(originalUpdatedAt)) +} + +func TestSetPasswordError(t *testing.T) { + // Тестируем ошибку при установке пароля + // В реальности bcrypt.GenerateFromPassword редко возвращает ошибки, + // но тестируем этот сценарий на всякий случай + user, err := NewUser("testuser", "test@example.com", "password123") + assert.NoError(t, err) + + err = user.SetPassword("newpassword123") + + // Должно пройти успешно, так как валидный пароль + assert.NoError(t, err) + assert.True(t, user.CheckPassword("newpassword123")) +} + +func TestCheckPassword(t *testing.T) { + password := "password123" + user, err := NewUser("testuser", "test@example.com", password) + assert.NoError(t, err) + + assert.True(t, user.CheckPassword(password)) + assert.False(t, user.CheckPassword("wrongpassword")) + assert.False(t, user.CheckPassword("")) +} + +func TestMarshalJSON(t *testing.T) { + username := "testuser" + email := "test@example.com" + password := "password123" + + user, err := NewUser(username, email, password) + assert.NoError(t, err) + + jsonData, err := json.Marshal(user) + assert.NoError(t, err) + + // Проверяем, что JSON содержит ожидаемые поля, но не содержит пароля + var userMap map[string]interface{} + err = json.Unmarshal(jsonData, &userMap) + assert.NoError(t, err) + + assert.Contains(t, userMap, "id") + assert.Contains(t, userMap, "username") + assert.Contains(t, userMap, "email") + assert.Contains(t, userMap, "created_at") + assert.Contains(t, userMap, "updated_at") + assert.NotContains(t, userMap, "password") + + assert.Equal(t, user.GetID(), userMap["id"]) + assert.Equal(t, username, userMap["username"]) + assert.Equal(t, email, userMap["email"]) +} + +func TestMarshalJSONWithPassword(t *testing.T) { + username := "testuser" + email := "test@example.com" + password := "password123" + + user, err := NewUser(username, email, password) + assert.NoError(t, err) + + jsonData, err := user.MarshalJSONWithPassword() + assert.NoError(t, err) + + // Проверяем, что JSON содержит все поля, включая пароль + var userMap map[string]interface{} + err = json.Unmarshal(jsonData, &userMap) + assert.NoError(t, err) + + assert.Contains(t, userMap, "id") + assert.Contains(t, userMap, "username") + assert.Contains(t, userMap, "email") + assert.Contains(t, userMap, "password") + assert.Contains(t, userMap, "created_at") + assert.Contains(t, userMap, "updated_at") + + assert.Equal(t, user.GetID(), userMap["id"]) + assert.Equal(t, username, userMap["username"]) + assert.Equal(t, email, userMap["email"]) + assert.Equal(t, user.GetPassword(), userMap["password"]) +} + +func TestUnmarshalJSONWithPassword(t *testing.T) { + username := "testuser" + email := "test@example.com" + password := "password123" + + user, err := NewUser(username, email, password) + assert.NoError(t, err) + + // Создаем JSON с паролем + jsonData, err := user.MarshalJSONWithPassword() + assert.NoError(t, err) + + // Создаем нового пользователя и десериализуем в него данные + newUser := &User{} + err = newUser.UnmarshalJSONWithPassword(jsonData) + assert.NoError(t, err) + + assert.Equal(t, user.GetID(), newUser.GetID()) + assert.Equal(t, user.GetUsername(), newUser.GetUsername()) + assert.Equal(t, user.GetEmail(), newUser.GetEmail()) + assert.Equal(t, user.GetPassword(), newUser.GetPassword()) + assert.WithinDuration(t, user.GetCreatedAt(), newUser.GetCreatedAt(), time.Second) + assert.WithinDuration(t, user.GetUpdatedAt(), newUser.GetUpdatedAt(), time.Second) +} + +func TestUnmarshalJSON(t *testing.T) { + username := "testuser" + email := "test@example.com" + password := "password123" + + user, err := NewUser(username, email, password) + assert.NoError(t, err) + + // Маршалим в JSON (без пароля) + jsonData, err := json.Marshal(user) + assert.NoError(t, err) + + // Создаем нового пользователя и десериализуем в него данные + newUser := &User{} + err = newUser.UnmarshalJSON(jsonData) + assert.NoError(t, err) + + assert.Equal(t, user.GetID(), newUser.GetID()) + assert.Equal(t, user.GetUsername(), newUser.GetUsername()) + assert.Equal(t, user.GetEmail(), newUser.GetEmail()) + // Пароль не должен быть установлен при десериализации без пароля + assert.Empty(t, newUser.GetPassword()) + assert.WithinDuration(t, user.GetCreatedAt(), newUser.GetCreatedAt(), time.Second) + assert.WithinDuration(t, user.GetUpdatedAt(), newUser.GetUpdatedAt(), time.Second) +} + +func TestUnmarshalJSONWithPasswordIncluded(t *testing.T) { + // Создаем JSON с паролем вручную + jsonStr := `{"id":"test-id","username":"testuser","email":"test@example.com","password":"hashed_password","created_at":"2023-01-01T00:00:00Z","updated_at":"2023-01-01T00:00:00Z"}` + + user := &User{} + err := user.UnmarshalJSON([]byte(jsonStr)) + assert.NoError(t, err) + + assert.Equal(t, "test-id", user.id) + assert.Equal(t, "testuser", user.username) + assert.Equal(t, "test@example.com", user.email) + assert.Equal(t, "hashed_password", user.password) + + createdAt, _ := time.Parse(time.RFC3339, "2023-01-01T00:00:00Z") + updatedAt, _ := time.Parse(time.RFC3339, "2023-01-01T00:00:00Z") + assert.Equal(t, createdAt, user.createdAt) + assert.Equal(t, updatedAt, user.updatedAt) +} diff --git a/internal/repository/factory_test.go b/internal/repository/factory_test.go new file mode 100644 index 0000000..3b1696a --- /dev/null +++ b/internal/repository/factory_test.go @@ -0,0 +1,286 @@ +package repository + +import ( + "sync" + "testing" + + "github.com/rd2w/go-notes/internal/model" + "github.com/stretchr/testify/assert" +) + +// MockRepository - тестовая реализация репозитория для проверки фабрики +type MockRepository struct { + _ int // unused field, added to satisfy linter +} + +func (m *MockRepository) Save(entity Entity) { + // пустая реализация для удовлетворения интерфейса +} + +func (m *MockRepository) GetAllNotes() []*model.Note { + // пустая реализация для удовлетворения интерфейса + return nil +} + +func (m *MockRepository) GetNotesCount() int { + // пустая реализация для удовлетворения интерфейса + return 0 +} + +func (m *MockRepository) GetNewNotes(lastIndex int) []*model.Note { + // пустая реализация для удовлетворения интерфейса + return nil +} + +func (m *MockRepository) GetAllByType(entityType string) []Entity { + // пустая реализация для удовлетворения интерфейса + return nil +} + +func (m *MockRepository) GetByID(entityType, id string) Entity { + // пустая реализация для удовлетворения интерфейса + return nil +} + +func (m *MockRepository) DeleteByID(entityType, id string) bool { + // пустая реализация для удовлетворения интерфейса + return false +} + +func TestRegister(t *testing.T) { + // Сохраняем оригинальные значения для восстановления + originalCreators := make(map[StorageType]Creator) + for k, v := range creators { + originalCreators[k] = v + } + originalDefaultType := defaultType + + // Восстанавливаем оригинальные значения после теста + defer func() { + creatorsLock.Lock() + defer creatorsLock.Unlock() + creators = make(map[StorageType]Creator) + for k, v := range originalCreators { + creators[k] = v + } + defaultType = originalDefaultType + }() + + // Регистрируем новый тип репозитория + mockType := StorageType("mock") + mockCreator := func() Repository { + return &MockRepository{} + } + + Register(mockType, mockCreator) + + // Проверяем, что создатель был зарегистрирован + creatorsLock.RLock() + creator, exists := creators[mockType] + creatorsLock.RUnlock() + + assert.True(t, exists) + assert.NotNil(t, creator) + + // Проверяем, что создатель создает экземпляр правильно + repo := creator() + assert.IsType(t, &MockRepository{}, repo) +} + +func TestNewRepository(t *testing.T) { + // Сохраняем оригинальные значения для восстановления + originalCreators := make(map[StorageType]Creator) + for k, v := range creators { + originalCreators[k] = v + } + originalDefaultType := defaultType + + // Восстанавливаем оригинальные значения после теста + defer func() { + creatorsLock.Lock() + defer creatorsLock.Unlock() + creators = make(map[StorageType]Creator) + for k, v := range originalCreators { + creators[k] = v + } + defaultType = originalDefaultType + }() + + // Регистрируем создателя для типа по умолчанию (JSON) + jsonCreator := func() Repository { + return &MockRepository{} + } + Register(JSON, jsonCreator) + + // Создаем репозиторий с помощью NewRepository (должен использовать тип по умолчанию) + repo := NewRepository() + + assert.IsType(t, &MockRepository{}, repo) +} + +func TestNewRepositoryByType(t *testing.T) { + // Сохраняем оригинальные значения для восстановления + originalCreators := make(map[StorageType]Creator) + for k, v := range creators { + originalCreators[k] = v + } + originalDefaultType := defaultType + + // Восстанавливаем оригинальные значения после теста + defer func() { + creatorsLock.Lock() + defer creatorsLock.Unlock() + creators = make(map[StorageType]Creator) + for k, v := range originalCreators { + creators[k] = v + } + defaultType = originalDefaultType + }() + + // Регистрируем создателей для разных типов + jsonCreator := func() Repository { + return &MockRepository{} + } + Register(JSON, jsonCreator) + + ramCreator := func() Repository { + return &MockRepository{} + } + Register(RAM, ramCreator) + + // Тестируем создание репозитория по типу JSON + repoJSON := NewRepositoryByType(JSON) + assert.IsType(t, &MockRepository{}, repoJSON) + + // Тестируем создание репозитория по типу RAM + repoRAM := NewRepositoryByType(RAM) + assert.IsType(t, &MockRepository{}, repoRAM) +} + +func TestNewRepositoryByTypeWithNonExistentType(t *testing.T) { + // Сохраняем оригинальные значения для восстановления + originalCreators := make(map[StorageType]Creator) + for k, v := range creators { + originalCreators[k] = v + } + originalDefaultType := defaultType + + // Восстанавливаем оригинальные значения после теста + defer func() { + creatorsLock.Lock() + defer creatorsLock.Unlock() + creators = make(map[StorageType]Creator) + for k, v := range originalCreators { + creators[k] = v + } + defaultType = originalDefaultType + }() + + // Регистрируем только RAM создатель + ramCreator := func() Repository { + return &MockRepository{} + } + Register(RAM, ramCreator) + + // Создаем репозиторий с несуществующим типом, ожидаем, что будет использован RAM как fallback + nonExistentType := StorageType("nonexistent") + repo := NewRepositoryByType(nonExistentType) + + assert.IsType(t, &MockRepository{}, repo) +} + +func TestFactoryWithMultipleGoroutines(t *testing.T) { + // Сохраняем оригинальные значения для восстановления + originalCreators := make(map[StorageType]Creator) + for k, v := range creators { + originalCreators[k] = v + } + originalDefaultType := defaultType + + // Восстанавливаем оригинальные значения после теста + defer func() { + creatorsLock.Lock() + defer creatorsLock.Unlock() + creators = make(map[StorageType]Creator) + for k, v := range originalCreators { + creators[k] = v + } + defaultType = originalDefaultType + }() + + // Регистрируем создателей + jsonCreator := func() Repository { + return &MockRepository{} + } + Register(JSON, jsonCreator) + + ramCreator := func() Repository { + return &MockRepository{} + } + Register(RAM, ramCreator) + + // Тестируем параллельный доступ к фабрике + var wg sync.WaitGroup + const numGoroutines = 10 + + // Создаем репозитории в разных горутинах + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + var repo Repository + if i%2 == 0 { + repo = NewRepositoryByType(JSON) + } else { + repo = NewRepositoryByType(RAM) + } + assert.IsType(t, &MockRepository{}, repo) + }(i) + } + + wg.Wait() +} + +func TestDefaultTypeFallback(t *testing.T) { + // Сохраняем оригинальные значения для восстановления + originalCreators := make(map[StorageType]Creator) + for k, v := range creators { + originalCreators[k] = v + } + originalDefaultType := defaultType + + // Восстанавливаем оригинальные значения после теста + defer func() { + creatorsLock.Lock() + defer creatorsLock.Unlock() + creators = make(map[StorageType]Creator) + for k, v := range originalCreators { + creators[k] = v + } + defaultType = originalDefaultType + }() + + // Устанавливаем RAM как тип по умолчанию + defaultType = RAM + + // Регистрируем создателей + ramCreator := func() Repository { + return &MockRepository{} + } + Register(RAM, ramCreator) + + // Регистрируем JSON, но не будем использовать его напрямую + jsonCreator := func() Repository { + return &MockRepository{} + } + Register(JSON, jsonCreator) + + // Создаем репозиторий с помощью NewRepository (должен использовать тип по умолчанию - RAM) + repo := NewRepository() + assert.IsType(t, &MockRepository{}, repo) + + // Создаем репозиторий с несуществующим типом (должен использовать RAM как fallback) + nonExistentType := StorageType("nonexistent") + repo = NewRepositoryByType(nonExistentType) + assert.IsType(t, &MockRepository{}, repo) +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 1eb758a..5c4d666 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -16,4 +16,7 @@ type Repository interface { GetAllNotes() []*model.Note GetNotesCount() int GetNewNotes(lastIndex int) []*model.Note + GetAllByType(entityType string) []Entity + GetByID(entityType, id string) Entity + DeleteByID(entityType, id string) bool } diff --git a/internal/repository/repository_test.go b/internal/repository/repository_test.go index 9d6d055..f650496 100644 --- a/internal/repository/repository_test.go +++ b/internal/repository/repository_test.go @@ -64,7 +64,7 @@ func TestRepository_Save(t *testing.T) { name: "Save unsupported entity", entity: &mockEntity{id: "test", entityType: "unsupported"}, expectedCount: 0, - logContains: "Репозиторий: неподдерживаемый тип сущности", + logContains: "Репозиторий: сохранена сущность unsupported ID=test", }, } diff --git a/internal/repository/storage/fs/json_repository.go b/internal/repository/storage/fs/json_repository.go index 84829d2..a7980ec 100644 --- a/internal/repository/storage/fs/json_repository.go +++ b/internal/repository/storage/fs/json_repository.go @@ -8,48 +8,50 @@ import ( "os" "path/filepath" "sync" - "time" "github.com/rd2w/go-notes/internal/model" "github.com/rd2w/go-notes/internal/repository" ) const ( - StorageDir = "data" - NoteFilePrefix = "notes" - TimeFormat = "2006-01-02_15-04-05" + StorageDir = "data" + NotesFileName = "notes.json" + UsersFileName = "users.json" + TimeFormat = "2006-01-02_15-04-05" ) // Тестовые переменные, которые можно изменить в тестах var ( - TestStorageDir = StorageDir - TestNoteFilePrefix = NoteFilePrefix - TestTimeFormat = TimeFormat + TestStorageDir = StorageDir + TestNotesFileName = NotesFileName + TestUsersFileName = UsersFileName + TestTimeFormat = TimeFormat ) // JSONRepository реализация репозитория с хранением данных в JSON файлах type JSONRepository struct { notes []*model.Note notesIndex map[string]*model.Note + users []*model.User + usersIndex map[string]*model.User + entities map[string][]repository.Entity // Хранит все сущности по типу + entityIndex map[string]repository.Entity // Для быстрого поиска по ID mu sync.RWMutex - startTime time.Time // Время запуска репозитория для формирования имени файла - initialNotesCount int // Количество заметок, загруженных из файла при инициализации + initialNotesCount int // Количество заметок, загруженных из файла при инициализации + initialUsersCount int // Количество пользователей, загруженных из файла при инициализации } // NewJSONRepository создает новый экземпляр JSON репозитория (возвращает интерфейс) func NewJSONRepository() repository.Repository { - // Проверяем, есть ли уже существующие файлы с заметками - latestTime := findLatestNoteFileTime() - if latestTime.IsZero() { - // Если файлов нет, используем текущее время - latestTime = time.Now() - } - repo := &JSONRepository{ notes: make([]*model.Note, 0), notesIndex: make(map[string]*model.Note), - startTime: latestTime, + users: make([]*model.User, 0), + usersIndex: make(map[string]*model.User), + entities: make(map[string][]repository.Entity), + entityIndex: make(map[string]repository.Entity), initialNotesCount: 0, + initialUsersCount: 0, } // Загружаем данные из хранилища при создании репозитория @@ -58,61 +60,47 @@ func NewJSONRepository() repository.Repository { return repo } -// findLatestNoteFileTime находит время самого последнего файла с заметками -func findLatestNoteFileTime() time.Time { - // Проверяем, существует ли директория - if _, err := os.Stat(TestStorageDir); os.IsNotExist(err) { - return time.Time{} - } - - // Читаем содержимое директории - files, err := os.ReadDir(TestStorageDir) - if err != nil { - log.Printf("Ошибка при чтении директории %s: %v", TestStorageDir, err) - return time.Time{} - } - - var latestTime time.Time - - for _, file := range files { - if file.IsDir() { - continue - } - - filename := file.Name() - if filepath.Ext(filename) == ".json" && len(filename) > len(TestNoteFilePrefix) && filename[:len(TestNoteFilePrefix)] == TestNoteFilePrefix { - // Извлекаем временную метку из имени файла - timeStr := filename[len(TestNoteFilePrefix)+1 : len(filename)-5] // убираем префикс_ и .json - if fileTime, err := time.Parse(TestTimeFormat, timeStr); err == nil { - if fileTime.After(latestTime) { - latestTime = fileTime - } - } - } - } - - return latestTime -} - // Save сохраняет сущность в соответствующий слайс и в JSON файл func (r *JSONRepository) Save(entity repository.Entity) { + log.Printf("Репозиторий: вызван метод Save для сущности типа %s с ID=%s", entity.GetType(), entity.GetID()) r.mu.Lock() defer r.mu.Unlock() - switch entity := entity.(type) { + switch e := entity.(type) { case *model.Note: // Добавляем заметку в слайс и индекс - r.notes = append(r.notes, entity) - r.notesIndex[entity.GetID()] = entity + r.notes = append(r.notes, e) + r.notesIndex[e.GetID()] = e - log.Printf("Репозиторий: сохранена заметка ID=%s", entity.GetID()) + log.Printf("Репозиторий: сохранена заметка ID=%s", e.GetID()) // Сохраняем в JSON файл - if err := r.saveToJSON(); err != nil { - log.Printf("Ошибка при сохранении в JSON: %v", err) + if err := r.saveNotesToJSON(); err != nil { + log.Printf("Ошибка при сохранении заметки в JSON: %v", err) + } + case *model.User: + // Добавляем пользователя в слайс и индекс + r.users = append(r.users, e) + r.usersIndex[e.GetID()] = e + + log.Printf("Репозиторий: сохранен пользователь ID=%s", e.GetID()) + + // Сохраняем в JSON файл + if err := r.saveUsersToJSON(); err != nil { + log.Printf("Ошибка при сохранении пользователя в JSON: %v", err) } default: - log.Printf("Репозиторий: неподдерживаемый тип сущности: %T", entity) + // Для других типов сущностей + entityType := entity.GetType() + r.entities[entityType] = append(r.entities[entityType], entity) + r.entityIndex[entity.GetID()] = entity + + log.Printf("Репозиторий: сохранена сущность %s ID=%s", entityType, entity.GetID()) + + // Сохраняем в JSON файл + if err := r.saveEntitiesToJSON(entityType); err != nil { + log.Printf("Ошибка при сохранении сущности %s в JSON: %v", entityType, err) + } } } @@ -144,12 +132,6 @@ func (r *JSONRepository) GetNewNotes(lastIndex int) []*model.Note { lastIndex = 0 } - // Если индекс меньше начального количества заметок (загруженных из файла), - // начинаем с начального количества, чтобы не возвращать загруженные заметки как "новые" - if lastIndex < r.initialNotesCount { - lastIndex = r.initialNotesCount - } - if lastIndex >= len(r.notes) { return []*model.Note{} } @@ -160,48 +142,75 @@ func (r *JSONRepository) GetNewNotes(lastIndex int) []*model.Note { return result } -// LoadFromStorage загружает данные из JSON файла при старте приложения +// LoadFromStorage загружает данные из JSON файлов при старте приложения func (r *JSONRepository) LoadFromStorage() { r.mu.Lock() defer r.mu.Unlock() + // Загружаем заметки + r.loadNotesFromStorage() + + // Загружаем пользователей + r.loadUsersFromStorage() +} + +// loadNotesFromStorage загружает заметки из JSON файла +func (r *JSONRepository) loadNotesFromStorage() { // Находим файл с данными для заметок - noteFile, err := r.findLatestFile(NoteFilePrefix) - if err != nil { + noteFile := filepath.Join(TestStorageDir, TestNotesFileName) + + // Проверяем, существует ли файл + if _, err := os.Stat(noteFile); os.IsNotExist(err) { // Если файл не найден, начинаем с пустого репозитория - log.Printf("Файл с данными не найден, начнем с пустого репозитория: %v", err) + log.Printf("Файл с заметками %s не найден, начнем с пустого репозитория", noteFile) return } // Загружаем данные из файла - if err := r.loadFromJSONFile(noteFile); err != nil { - fmt.Printf("Ошибка при загрузке данных из файла %s: %v\n", noteFile, err) + if err := r.loadNotesFromJSONFile(noteFile); err != nil { + fmt.Printf("Ошибка при загрузке заметок из файла %s: %v\n", noteFile, err) return } - // Обновляем startTime на основе времени файла - filename := filepath.Base(noteFile) - timeStr := filename[len(NoteFilePrefix)+1 : len(filename)-5] // убираем префикс_ и .json - if fileTime, err := time.Parse(TimeFormat, timeStr); err == nil { - r.startTime = fileTime - } - fmt.Printf("Загружено %d заметок из файла %s\n", len(r.notes), noteFile) // Сохраняем количество загруженных заметок как начальное r.initialNotesCount = len(r.notes) } -// saveToJSON сохраняет данные в JSON файл с временной меткой запуска приложения -func (r *JSONRepository) saveToJSON() error { +// loadUsersFromStorage загружает пользователей из JSON файла +func (r *JSONRepository) loadUsersFromStorage() { + // Находим файл с данными для пользователей + userFile := filepath.Join(TestStorageDir, TestUsersFileName) + + // Проверяем, существует ли файл + if _, err := os.Stat(userFile); os.IsNotExist(err) { + // Если файл не найден, начинаем с пустого репозитория + log.Printf("Файл с пользователями %s не найден, начнем с пустого репозитория", userFile) + return + } + + // Загружаем данные из файла + if err := r.loadUsersFromJSONFile(userFile); err != nil { + fmt.Printf("Ошибка при загрузке пользователей из файла %s: %v\n", userFile, err) + return + } + + fmt.Printf("Загружено %d пользователей из файла %s\n", len(r.users), userFile) + + // Сохраняем количество загруженных пользователей как начальное + r.initialUsersCount = len(r.users) +} + +// saveNotesToJSON сохраняет заметки в JSON файл +func (r *JSONRepository) saveNotesToJSON() error { // Создаем директорию, если она не существует if err := os.MkdirAll(TestStorageDir, 0755); err != nil { return fmt.Errorf("не удалось создать директорию %s: %w", TestStorageDir, err) } - // Формируем имя файла с временной меткой запуска приложения - timestamp := r.startTime.Format(TestTimeFormat) - filename := fmt.Sprintf("%s_%s.json", TestNoteFilePrefix, timestamp) + // Формируем имя файла + filename := TestNotesFileName filePath := filepath.Join(TestStorageDir, filename) // Создаем/перезаписываем файл @@ -219,14 +228,88 @@ func (r *JSONRepository) saveToJSON() error { encoder := json.NewEncoder(file) encoder.SetIndent("", " ") if err := encoder.Encode(r.notes); err != nil { - return fmt.Errorf("ошибка при кодировании в JSON: %w", err) + return fmt.Errorf("ошибка при кодировании заметок в JSON: %w", err) + } + + return nil +} + +// saveUsersToJSON сохраняет пользователей в JSON файл +func (r *JSONRepository) saveUsersToJSON() error { + // Создаем директорию, если она не существует + if err := os.MkdirAll(TestStorageDir, 0755); err != nil { + return fmt.Errorf("не удалось создать директорию %s: %w", TestStorageDir, err) + } + + // Формируем имя файла + filename := TestUsersFileName + filePath := filepath.Join(TestStorageDir, filename) + + // Создаем/перезаписываем файл + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("не удалось создать файл %s: %w", filePath, err) + } + defer func() { + if closeErr := file.Close(); closeErr != nil { + log.Printf("Ошибка при закрытии файла %s: %v", filePath, closeErr) + } + }() + + // Кодируем данные в JSON, включая пароли + usersWithPasswords := make([]json.RawMessage, len(r.users)) + for i, user := range r.users { + userData, err := user.MarshalJSONWithPassword() + if err != nil { + return fmt.Errorf("ошибка при кодировании пользователя %s: %w", user.GetID(), err) + } + usersWithPasswords[i] = userData + } + + // Кодируем данные в JSON + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(usersWithPasswords); err != nil { + return fmt.Errorf("ошибка при кодировании пользователей в JSON: %w", err) + } + + return nil +} + +// saveEntitiesToJSON сохраняет сущности указанного типа в JSON файл +func (r *JSONRepository) saveEntitiesToJSON(entityType string) error { + // Создаем директорию, если она не существует + if err := os.MkdirAll(TestStorageDir, 0755); err != nil { + return fmt.Errorf("не удалось создать директорию %s: %w", TestStorageDir, err) + } + + // Формируем имя файла + filename := fmt.Sprintf("%ss.json", entityType) // например, "items.json" + filePath := filepath.Join(TestStorageDir, filename) + + // Создаем/перезаписываем файл + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("не удалось создать файл %s: %w", filePath, err) + } + defer func() { + if closeErr := file.Close(); closeErr != nil { + log.Printf("Ошибка при закрытии файла %s: %v", filePath, closeErr) + } + }() + + // Кодируем данные в JSON + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + if err := encoder.Encode(r.entities[entityType]); err != nil { + return fmt.Errorf("ошибка при кодировании сущностей %s в JSON: %w", entityType, err) } return nil } -// loadFromJSONFile загружает данные из указанного JSON файла -func (r *JSONRepository) loadFromJSONFile(filepath string) error { +// loadNotesFromJSONFile загружает заметки из указанного JSON файла +func (r *JSONRepository) loadNotesFromJSONFile(filepath string) error { file, err := os.Open(filepath) if err != nil { return fmt.Errorf("не удалось открыть файл %s: %w", filepath, err) @@ -269,22 +352,171 @@ func (r *JSONRepository) loadFromJSONFile(filepath string) error { return nil } -// findLatestFile находит файл с указанным префиксом, используя время запуска приложения -func (r *JSONRepository) findLatestFile(prefix string) (string, error) { - // Проверяем, существует ли директория - if _, err := os.Stat(TestStorageDir); os.IsNotExist(err) { - return "", fmt.Errorf("директория %s не существует", TestStorageDir) +// loadUsersFromJSONFile загружает пользователей из указанного JSON файла +func (r *JSONRepository) loadUsersFromJSONFile(filepath string) error { + file, err := os.Open(filepath) + if err != nil { + return fmt.Errorf("не удалось открыть файл %s: %w", filepath, err) + } + defer func() { + if closeErr := file.Close(); closeErr != nil { + log.Printf("Ошибка при закрытии файла %s: %v", filepath, closeErr) + } + }() + + // Читаем содержимое файла + byteValue, err := io.ReadAll(file) + if err != nil { + return fmt.Errorf("ошибка при чтении файла: %w", err) } - // Формируем имя файла на основе времени запуска - timestamp := r.startTime.Format(TestTimeFormat) - filename := fmt.Sprintf("%s_%s.json", prefix, timestamp) - filePath := filepath.Join(TestStorageDir, filename) + // Декодируем данные из JSON + var jsonUsers []json.RawMessage + if err := json.Unmarshal(byteValue, &jsonUsers); err != nil { + return fmt.Errorf("ошибка при декодировании JSON: %w", err) + } - // Проверяем, существует ли файл - if _, err := os.Stat(filePath); os.IsNotExist(err) { - return "", fmt.Errorf("файл %s не существует", filePath) + // Преобразуем каждый JSON объект в User + users := make([]*model.User, len(jsonUsers)) + for i, rawUser := range jsonUsers { + user := &model.User{} + if err := json.Unmarshal(rawUser, user); err != nil { + return fmt.Errorf("ошибка при десериализации пользователя: %w", err) + } + users[i] = user + } + + // Обновляем внутренние структуры + r.users = users + r.usersIndex = make(map[string]*model.User) + for _, user := range users { + r.usersIndex[user.GetID()] = user + } + + return nil +} + +// GetAllByType возвращает все сущности указанного типа +func (r *JSONRepository) GetAllByType(entityType string) []repository.Entity { + r.mu.RLock() + defer r.mu.RUnlock() + + switch entityType { + case "note": + entities := make([]repository.Entity, len(r.notes)) + for i, note := range r.notes { + entities[i] = note + } + return entities + case "user": + entities := make([]repository.Entity, len(r.users)) + for i, user := range r.users { + entities[i] = user + } + return entities + default: + entities, exists := r.entities[entityType] + if !exists { + return []repository.Entity{} + } + + result := make([]repository.Entity, len(entities)) + copy(result, entities) + return result + } +} + +// GetByID возвращает сущность по типу и ID +func (r *JSONRepository) GetByID(entityType, id string) repository.Entity { + r.mu.RLock() + defer r.mu.RUnlock() + + switch entityType { + case "note": + if note, exists := r.notesIndex[id]; exists { + return note + } + case "user": + if user, exists := r.usersIndex[id]; exists { + return user + } + default: + if entity, exists := r.entityIndex[id]; exists { + // Проверяем, что тип сущности совпадает + if entity.GetType() == entityType { + return entity + } + } + } + + return nil +} + +// DeleteByID удаляет сущность по типу и ID +func (r *JSONRepository) DeleteByID(entityType, id string) bool { + r.mu.Lock() + defer r.mu.Unlock() + + switch entityType { + case "note": + // Удаляем заметку + if note, exists := r.notesIndex[id]; exists { + // Удаляем из слайса + for i, n := range r.notes { + if n.GetID() == note.GetID() { + r.notes = append(r.notes[:i], r.notes[i+1:]...) + break + } + } + // Удаляем из индекса + delete(r.notesIndex, id) + + // Сохраняем изменения в файл + if err := r.saveNotesToJSON(); err != nil { + log.Printf("Ошибка при сохранении заметок после удаления: %v", err) + } + return true + } + case "user": + // Удаляем пользователя + if user, exists := r.usersIndex[id]; exists { + // Удаляем из слайса + for i, u := range r.users { + if u.GetID() == user.GetID() { + r.users = append(r.users[:i], r.users[i+1:]...) + break + } + } + // Удаляем из индекса + delete(r.usersIndex, id) + + // Сохраняем изменения в файл + if err := r.saveUsersToJSON(); err != nil { + log.Printf("Ошибка при сохранении пользователей после удаления: %v", err) + } + return true + } + default: + // Удаляем другую сущность + if entity, exists := r.entityIndex[id]; exists && entity.GetType() == entityType { + // Удаляем из слайса соответствующего типа + entities := r.entities[entityType] + for i, e := range entities { + if e.GetID() == id { + r.entities[entityType] = append(entities[:i], entities[i+1:]...) + break + } + } + // Удаляем из общего индекса + delete(r.entityIndex, id) + + // Сохраняем изменения в файл + if err := r.saveEntitiesToJSON(entityType); err != nil { + log.Printf("Ошибка при сохранении сущностей %s после удаления: %v", entityType, err) + } + return true + } } - return filePath, nil + return false } diff --git a/internal/repository/storage/fs/json_repository_test.go b/internal/repository/storage/fs/json_repository_test.go index 88200f3..5e057bc 100644 --- a/internal/repository/storage/fs/json_repository_test.go +++ b/internal/repository/storage/fs/json_repository_test.go @@ -91,9 +91,13 @@ func TestJSONRepository_SaveUnsupportedEntity(t *testing.T) { // Проверяем, что заметки не были сохранены для неподдерживаемых типов assert.Equal(t, 0, repo.GetNotesCount(), "Не должно быть сохраненных заметок для неподдерживаемых сущностей") + // Проверяем, что сущность была сохранена в общий слайс + entities := repo.GetAllByType("unsupported") + assert.Len(t, entities, 1, "Должна быть одна неподдерживаемая сущность") + // Проверяем вывод в лог logOutput := buf.String() - assert.Contains(t, logOutput, "Репозиторий: неподдерживаемый тип сущности") + assert.Contains(t, logOutput, "Репозиторий: сохранена сущность") } // TestJSONRepository_SaveMultipleNotes тестирует сохранение нескольких заметок @@ -386,8 +390,7 @@ func TestJSONRepository_SaveToFile(t *testing.T) { // Проверяем, что файл имеет правильное имя var noteFileFound bool for _, file := range files { - if filepath.Ext(file.Name()) == ".json" && len(file.Name()) > len(fs.TestNoteFilePrefix) && - file.Name()[:len(fs.TestNoteFilePrefix)] == fs.TestNoteFilePrefix { + if file.Name() == fs.TestNotesFileName { noteFileFound = true break } @@ -407,9 +410,8 @@ func TestJSONRepository_LoadFromStorage(t *testing.T) { note := model.NewNote("Loaded Note", "Loaded Content") notes := []*model.Note{note} - // Формируем имя файла с временной меткой - timestamp := time.Now().Format(fs.TestTimeFormat) - filename := filepath.Join(tempDir, fs.TestNoteFilePrefix+"_"+timestamp+".json") + // Формируем имя файла + filename := filepath.Join(tempDir, fs.TestNotesFileName) file, err := os.Create(filename) require.NoError(t, err) diff --git a/internal/repository/storage/ram/ram_repository.go b/internal/repository/storage/ram/ram_repository.go index 2c7130d..3cb95c2 100644 --- a/internal/repository/storage/ram/ram_repository.go +++ b/internal/repository/storage/ram/ram_repository.go @@ -10,16 +10,20 @@ import ( // RamRepository управляет хранением различных сущностей в памяти type RamRepository struct { - notes []*model.Note - notesIndex map[string]*model.Note // Для быстрого поиска по ID - mu sync.RWMutex + notes []*model.Note + notesIndex map[string]*model.Note // Для быстрого поиска по ID + entities map[string][]repository.Entity // Хранит все сущности по типу + entityIndex map[string]repository.Entity // Для быстрого поиска по ID + mu sync.RWMutex } // NewRamRepository создает новый экземпляр RAM репозитория (возвращает интерфейс) func NewRamRepository() repository.Repository { return &RamRepository{ - notes: make([]*model.Note, 0), - notesIndex: make(map[string]*model.Note), + notes: make([]*model.Note, 0), + notesIndex: make(map[string]*model.Note), + entities: make(map[string][]repository.Entity), + entityIndex: make(map[string]repository.Entity), } } @@ -28,13 +32,23 @@ func (r *RamRepository) Save(entity repository.Entity) { r.mu.Lock() defer r.mu.Unlock() - switch entity := entity.(type) { + switch e := entity.(type) { case *model.Note: - r.notes = append(r.notes, entity) - r.notesIndex[entity.GetID()] = entity - log.Printf("Репозиторий: сохранена заметка ID=%s", entity.GetID()) + r.notes = append(r.notes, e) + r.notesIndex[e.GetID()] = e + log.Printf("Репозиторий: сохранена заметка ID=%s", e.GetID()) + case *model.User: + // Добавляем пользователя в общий массив сущностей + entityType := e.GetType() + r.entities[entityType] = append(r.entities[entityType], e) + r.entityIndex[e.GetID()] = e + log.Printf("Репозиторий: сохранен пользователь ID=%s", e.GetID()) default: - log.Printf("Репозиторий: неподдерживаемый тип сущности: %T", entity) + // Для других типов сущностей + entityType := entity.GetType() + r.entities[entityType] = append(r.entities[entityType], entity) + r.entityIndex[entity.GetID()] = entity + log.Printf("Репозиторий: сохранена сущность %s ID=%s", entityType, entity.GetID()) } } @@ -73,3 +87,80 @@ func (r *RamRepository) GetNewNotes(lastIndex int) []*model.Note { copy(result, newNotes) return result } + +// GetAllByType возвращает все сущности указанного типа +func (r *RamRepository) GetAllByType(entityType string) []repository.Entity { + r.mu.RLock() + defer r.mu.RUnlock() + + entities, exists := r.entities[entityType] + if !exists { + return []repository.Entity{} + } + + result := make([]repository.Entity, len(entities)) + copy(result, entities) + return result +} + +// GetByID возвращает сущность по типу и ID +func (r *RamRepository) GetByID(entityType, id string) repository.Entity { + r.mu.RLock() + defer r.mu.RUnlock() + + if entityType == "note" { + // Для заметок проверяем в notesIndex + if note, exists := r.notesIndex[id]; exists { + return note + } + } else { + // Для других типов проверяем в entityIndex + if entity, exists := r.entityIndex[id]; exists { + // Проверяем, что тип сущности совпадает + if entity.GetType() == entityType { + return entity + } + } + } + + return nil +} + +// DeleteByID удаляет сущность по типу и ID +func (r *RamRepository) DeleteByID(entityType, id string) bool { + r.mu.Lock() + defer r.mu.Unlock() + + if entityType == "note" { + // Удаляем заметку + if note, exists := r.notesIndex[id]; exists { + // Удаляем из слайса + for i, n := range r.notes { + if n.GetID() == note.GetID() { + r.notes = append(r.notes[:i], r.notes[i+1:]...) + break + } + } + // Удаляем из индекса + delete(r.notesIndex, id) + return true + } + } else { + // Удаляем другую сущность + if entity, exists := r.entityIndex[id]; exists && entity.GetType() == entityType { + // Удаляем из слайса соответствующего типа + entities := r.entities[entityType] + for i, e := range entities { + if e.GetID() == id { + r.entities[entityType] = append(entities[:i], entities[i+1:]...) + break + } + } + // Удаляем из общего индекса + delete(r.entityIndex, id) + return true + } + } + + return false +} diff --git a/internal/service/service.go b/internal/service/service.go deleted file mode 100644 index f6b75bb..0000000 --- a/internal/service/service.go +++ /dev/null @@ -1,95 +0,0 @@ -package service - -import ( - "context" - "fmt" - "log" - "time" - - "github.com/rd2w/go-notes/internal/model" - "github.com/rd2w/go-notes/internal/repository" -) - -const ( - EntityChanBuffer = 10 - MaxNotesCount = 10 -) - -// Service содержит бизнес-логику приложения -type Service struct { - repo repository.Repository - ctx context.Context - interval time.Duration -} - -// NewService создает новый экземпляр сервиса -func NewService(repo repository.Repository, ctx context.Context, interval time.Duration) *Service { - return &Service{ - repo: repo, - ctx: ctx, - interval: interval, - } -} - -// Start запускает все горутины сервиса -func (s *Service) Start() { - // Создаем канал для передачи сущностей между горутинами - entityChan := make(chan repository.Entity, EntityChanBuffer) - - // Запускаем горутину для генерации данных - go s.startDataGeneration(entityChan) - - // Запускаем горутину для сохранения данных - go s.startDataSaving(entityChan) -} - -// startDataGeneration запускает периодическое создание тестовых данных -func (s *Service) startDataGeneration(entityChan chan<- repository.Entity) { - go func() { - noteCounter := 1 - ticker := time.NewTicker(s.interval) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - title := fmt.Sprintf("Тестовая заметка %d", noteCounter) - content := fmt.Sprintf("Это содержимое тестовой заметки номер %d", noteCounter) - - note := model.NewNote(title, content) - - select { - case entityChan <- note: - log.Printf("Сервис: создана заметка %d", noteCounter) - case <-s.ctx.Done(): - log.Println("Сервис: завершение генерации данных") - return - } - - noteCounter++ - if noteCounter > MaxNotesCount { - log.Println("Сервис: генерация тестовых данных завершена") - return - } - case <-s.ctx.Done(): - log.Println("Сервис: завершение работы генерации по сигналу") - return - } - } - }() -} - -// startDataSaving запускает сохранение данных в репозиторий -func (s *Service) startDataSaving(entityChan <-chan repository.Entity) { - for { - select { - case entity := <-entityChan: - // Вызываем синхронный метод сохранения в репозитории - s.repo.Save(entity) - log.Printf("Сервис: сохранена сущность %s", entity.GetID()) - case <-s.ctx.Done(): - log.Println("Сервис: завершение сохранения данных") - return - } - } -} diff --git a/internal/service/service_test.go b/internal/service/service_test.go deleted file mode 100644 index 128c6eb..0000000 --- a/internal/service/service_test.go +++ /dev/null @@ -1,569 +0,0 @@ -package service - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sync" - "testing" - "time" - - "github.com/rd2w/go-notes/internal/repository/storage/fs" - "github.com/rd2w/go-notes/internal/repository/storage/ram" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestService_StopWithContext тестирует остановку генерации через контекст -func TestService_StopWithContext(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - service := NewService(repo, ctx, 10*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем создание хотя бы одной заметки - time.Sleep(15 * time.Millisecond) - - // Останавливаем сервис - cancel() - - // Ждем завершения - time.Sleep(20 * time.Millisecond) - - // Проверяем, что сервис остановился и создал заметки до остановки - notes := repo.GetAllNotes() - assert.Greater(t, len(notes), 0, "Должны быть созданы заметки до остановки") -} - -// TestService_LimitTenNotes тестирует ограничение в 10 заметок -func TestService_LimitTenNotes(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - service := NewService(repo, ctx, 5*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем, пока будут созданы все заметки (ограничение в 10) - time.Sleep(100 * time.Millisecond) - - // Проверяем что было создано ровно 10 заметок - assert.Len(t, repo.GetAllNotes(), 10, "Должно быть создано ровно 10 заметок") -} - -// TestService_NoteTitles тестирует корректность заголовков заметок -func TestService_NoteTitles(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - service := NewService(repo, ctx, 5*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем создание нескольких заметок - time.Sleep(30 * time.Millisecond) - - // Проверяем заголовки - notes := repo.GetAllNotes() - require.GreaterOrEqual(t, len(notes), 3, "Должно быть создано как минимум 3 заметки") - - for i := 0; i < len(notes) && i < 3; i++ { - expectedTitle := fmt.Sprintf("Тестовая заметка %d", i+1) - assert.Equal(t, expectedTitle, notes[i].GetTitle(), - "Заголовок заметки %d должен быть '%s'", i+1, expectedTitle) - } -} - -// TestService_ConcurrentSafety тестирует безопасность конкурентного доступа -func TestService_ConcurrentSafety(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - - // Создаем сервис - service := NewService(repo, ctx, 10*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем немного времени - time.Sleep(100 * time.Millisecond) - - // Проверяем, что сервис работает без паники и создает заметки - notesCount := repo.GetNotesCount() - assert.Greater(t, notesCount, 0, "Должны быть созданы заметки") - t.Logf("Создано заметок: %d", notesCount) -} - -// TestService_ImmediateStop тестирует немедленную остановку сервиса -func TestService_ImmediateStop(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - - // Останавливаем сервис сразу же - cancel() - - repo := ram.NewRamRepository() - service := NewService(repo, ctx, 10*time.Millisecond) - service.Start() - - // Даем время на обработку - time.Sleep(20 * time.Millisecond) - - // Проверяем, что не было создано заметок - notes := repo.GetAllNotes() - assert.Len(t, notes, 0, "Не должно быть созданных заметок при немедленной остановке") -} - -// TestService_NoteContent тестирует содержимое заметок -func TestService_NoteContent(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - service := NewService(repo, ctx, 5*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем создание заметок - time.Sleep(40 * time.Millisecond) - - // Проверяем содержимое заметок - notes := repo.GetAllNotes() - require.GreaterOrEqual(t, len(notes), 3, "Должно быть создано как минимум 3 заметки") - - for i := 0; i < len(notes) && i < 3; i++ { - expectedContent := fmt.Sprintf("Это содержимое тестовой заметки номер %d", i+1) - assert.Equal(t, expectedContent, notes[i].GetContent(), - "Содержимое заметки %d должно быть '%s'", i+1, expectedContent) - } -} - -// TestService_SimpleCase тестирует простой сценарий работы сервиса -func TestService_SimpleCase(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - service := NewService(repo, ctx, 20*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем создание хотя бы одной заметки - time.Sleep(30 * time.Millisecond) - - // Проверяем, что создана хотя бы одна заметка - assert.GreaterOrEqual(t, repo.GetNotesCount(), 1, "Должна быть создана хотя бы одна заметка") -} - -// TestService_MultipleInstances тестирует работу нескольких экземпляров сервиса -func TestService_MultipleInstances(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - - // Создаем два сервиса - service1 := NewService(repo, ctx, 15*time.Millisecond) - service2 := NewService(repo, ctx, 20*time.Millisecond) - - // Запускаем оба сервиса - service1.Start() - service2.Start() - - // Ждем некоторое время - time.Sleep(100 * time.Millisecond) - - // Проверяем, что оба сервиса создают заметки - notesCount := repo.GetNotesCount() - assert.Greater(t, notesCount, 0, "Должны быть созданы заметки от обоих сервисов") - t.Logf("Создано заметок от обоих сервисов: %d", notesCount) -} - -// TestService_GoroutineSafety тестирует безопасность при одновременном доступе к репозиторию -func TestService_GoroutineSafety(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - service := NewService(repo, ctx, 5*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Одновременно читаем заметки из репозитория в нескольких горутинах - var wg sync.WaitGroup - for i := 0; i < 5; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 10; j++ { - _ = repo.GetAllNotes() - time.Sleep(2 * time.Millisecond) - } - }() - } - - // Ждем выполнения чтения - time.Sleep(50 * time.Millisecond) - - // Отменяем контекст для остановки сервиса - cancel() - - // Даем время на завершение - time.Sleep(20 * time.Millisecond) - - // Завершаем горутины чтения - wg.Wait() - - // Проверяем, что заметки были созданы - notesCount := repo.GetNotesCount() - assert.Greater(t, notesCount, 0, "Должны быть созданы заметки") -} - -// TestService_DataGenerationAndSaving тестирует полный цикл генерации и сохранения данных -func TestService_DataGenerationAndSaving(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - repo := ram.NewRamRepository() - service := NewService(repo, ctx, 10*time.Millisecond) - - // Сохраняем начальное количество заметок - initialCount := repo.GetNotesCount() - - // Запускаем сервис - service.Start() - - // Ждем создание нескольких заметок - time.Sleep(60 * time.Millisecond) - - // Проверяем, что количество заметок увеличилось - finalCount := repo.GetNotesCount() - assert.Greater(t, finalCount, initialCount, "Количество заметок должно увеличиться после запуска сервиса") - - // Проверяем, что все заметки имеют корректные ID - notes := repo.GetAllNotes() - for _, note := range notes { - assert.NotEmpty(t, note.GetID(), "У заметки должен быть ID") - assert.Equal(t, "note", note.GetType(), "Тип заметки должен быть 'note'") - } -} - -// TestService_StopWithContext_WithJSON тестирует остановку генерации через контекст с JSON репозиторием -func TestService_StopWithContext_WithJSON(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Создаем временный JSON репозиторий - repo := fs.NewJSONRepository() - service := NewService(repo, ctx, 10*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем создание хотя бы одной заметки - time.Sleep(15 * time.Millisecond) - - // Останавливаем сервис - cancel() - - // Ждем завершения - time.Sleep(20 * time.Millisecond) - - // Проверяем, что сервис остановился и создал заметки до остановки - notes := repo.GetAllNotes() - assert.Greater(t, len(notes), 0, "Должны быть созданы заметки до остановки") - - // Очищаем файлы после теста - _ = cleanupJSONFiles() -} - -// TestService_LimitTenNotes_WithJSON тестирует ограничение в 10 заметок с JSON репозиторием -func TestService_LimitTenNotes_WithJSON(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Создаем временный JSON репозиторий - repo := fs.NewJSONRepository() - service := NewService(repo, ctx, 5*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем, пока будут созданы все заметки (ограничение в 10) - time.Sleep(60 * time.Millisecond) - - // Проверяем что было создано ровно 10 заметок - assert.Len(t, repo.GetAllNotes(), 10, "Должно быть создано ровно 10 заметок") - - // Очищаем файлы после теста - _ = cleanupJSONFiles() -} - -// TestService_NoteTitles_WithJSON тестирует корректность заголовков заметок с JSON репозиторием -func TestService_NoteTitles_WithJSON(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Создаем временный JSON репозиторий - repo := fs.NewJSONRepository() - service := NewService(repo, ctx, 5*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем создание нескольких заметок - time.Sleep(30 * time.Millisecond) - - // Проверяем заголовки - notes := repo.GetAllNotes() - require.GreaterOrEqual(t, len(notes), 3, "Должно быть создано как минимум 3 заметки") - - for i := 0; i < len(notes) && i < 3; i++ { - expectedTitle := fmt.Sprintf("Тестовая заметка %d", i+1) - assert.Equal(t, expectedTitle, notes[i].GetTitle(), - "Заголовок заметки %d должен быть '%s'", i+1, expectedTitle) - } - - // Очищаем файлы после теста - _ = cleanupJSONFiles() -} - -// TestService_ConcurrentSafety_WithJSON тестирует безопасность конкурентного доступа с JSON репозиторием -func TestService_ConcurrentSafety_WithJSON(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Создаем временный JSON репозиторий - repo := fs.NewJSONRepository() - - // Создаем сервис - service := NewService(repo, ctx, 10*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем немного времени - time.Sleep(100 * time.Millisecond) - - // Проверяем, что сервис работает без паники и создает заметки - notesCount := repo.GetNotesCount() - assert.Greater(t, notesCount, 0, "Должны быть созданы заметки") - t.Logf("Создано заметок: %d", notesCount) - - // Очищаем файлы после теста - _ = cleanupJSONFiles() -} - -// TestService_ImmediateStop_WithJSON тестирует немедленную остановку сервиса с JSON репозиторием -func TestService_ImmediateStop_WithJSON(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - - // Останавливаем сервис сразу же - cancel() - - // Создаем временный JSON репозиторий - repo := fs.NewJSONRepository() - service := NewService(repo, ctx, 10*time.Millisecond) - service.Start() - - // Даем время на обработку - time.Sleep(20 * time.Millisecond) - - // Проверяем, что не было создано заметок - notes := repo.GetAllNotes() - assert.Len(t, notes, 0, "Не должно быть созданных заметок при немедленной остановке") - - // Очищаем файлы после теста - _ = cleanupJSONFiles() -} - -// TestService_NoteContent_WithJSON тестирует содержимое заметок с JSON репозиторием -func TestService_NoteContent_WithJSON(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Создаем временный JSON репозиторий - repo := fs.NewJSONRepository() - service := NewService(repo, ctx, 5*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем создание заметок - time.Sleep(40 * time.Millisecond) - - // Проверяем содержимое заметок - notes := repo.GetAllNotes() - require.GreaterOrEqual(t, len(notes), 3, "Должно быть создано как минимум 3 заметки") - - for i := 0; i < len(notes) && i < 3; i++ { - expectedContent := fmt.Sprintf("Это содержимое тестовой заметки номер %d", i+1) - assert.Equal(t, expectedContent, notes[i].GetContent(), - "Содержимое заметки %d должно быть '%s'", i+1, expectedContent) - } - - // Очищаем файлы после теста - _ = cleanupJSONFiles() -} - -// TestService_SimpleCase_WithJSON тестирует простой сценарий работы сервиса с JSON репозиторием -func TestService_SimpleCase_WithJSON(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Создаем временный JSON репозиторий - repo := fs.NewJSONRepository() - service := NewService(repo, ctx, 20*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Ждем создание хотя бы одной заметки - time.Sleep(30 * time.Millisecond) - - // Проверяем, что создана хотя бы одна заметка - assert.GreaterOrEqual(t, repo.GetNotesCount(), 1, "Должна быть создана хотя бы одна заметка") - - // Очищаем файлы после теста - _ = cleanupJSONFiles() -} - -// TestService_MultipleInstances_WithJSON тестирует работу нескольких экземпляров сервиса с JSON репозиторием -func TestService_MultipleInstances_WithJSON(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Создаем временный JSON репозиторий - repo := fs.NewJSONRepository() - - // Создаем два сервиса - service1 := NewService(repo, ctx, 15*time.Millisecond) - service2 := NewService(repo, ctx, 20*time.Millisecond) - - // Запускаем оба сервиса - service1.Start() - service2.Start() - - // Ждем некоторое время - time.Sleep(100 * time.Millisecond) - - // Проверяем, что оба сервиса создают заметки - notesCount := repo.GetNotesCount() - assert.Greater(t, notesCount, 0, "Должны быть созданы заметки от обоих сервисов") - t.Logf("Создано заметок от обоих сервисов: %d", notesCount) - - // Очищаем файлы после теста - _ = cleanupJSONFiles() -} - -// TestService_GoroutineSafety_WithJSON тестирует безопасность при одновременном доступе к JSON репозиторию -func TestService_GoroutineSafety_WithJSON(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Создаем временный JSON репозиторий - repo := fs.NewJSONRepository() - service := NewService(repo, ctx, 5*time.Millisecond) - - // Запускаем сервис - service.Start() - - // Одновременно читаем заметки из репозитория в нескольких горутинах - var wg sync.WaitGroup - for i := 0; i < 5; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 10; j++ { - _ = repo.GetAllNotes() - time.Sleep(2 * time.Millisecond) - } - }() - } - - // Ждем выполнения чтения - time.Sleep(50 * time.Millisecond) - - // Отменяем контекст для остановки сервиса - cancel() - - // Даем время на завершение - time.Sleep(20 * time.Millisecond) - - // Завершаем горутины чтения - wg.Wait() - - // Проверяем, что заметки были созданы - notesCount := repo.GetNotesCount() - assert.Greater(t, notesCount, 0, "Должны быть созданы заметки") - - // Очищаем файлы после теста - _ = cleanupJSONFiles() -} - -// TestService_DataGenerationAndSaving_WithJSON тестирует полный цикл генерации и сохранения данных с JSON репозиторием -func TestService_DataGenerationAndSaving_WithJSON(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - // Создаем временный JSON репозиторий - repo := fs.NewJSONRepository() - service := NewService(repo, ctx, 10*time.Millisecond) - - // Сохраняем начальное количество заметок - initialCount := repo.GetNotesCount() - - // Запускаем сервис - service.Start() - - // Ждем создание нескольких заметок - time.Sleep(60 * time.Millisecond) - - // Проверяем, что количество заметок увеличилось - finalCount := repo.GetNotesCount() - assert.Greater(t, finalCount, initialCount, "Количество заметок должно увеличиться после запуска сервиса") - - // Проверяем, что все заметки имеют корректные ID - notes := repo.GetAllNotes() - for _, note := range notes { - assert.NotEmpty(t, note.GetID(), "У заметки должен быть ID") - assert.Equal(t, "note", note.GetType(), "Тип заметки должен быть 'note'") - } - - // Очищаем файлы после теста - _ = cleanupJSONFiles() -} - -// cleanupJSONFiles удаляет все JSON файлы из директории data -func cleanupJSONFiles() error { - files, err := os.ReadDir(fs.StorageDir) - if err != nil { - return err - } - - for _, file := range files { - if filepath.Ext(file.Name()) == ".json" && - len(file.Name()) > len(fs.NoteFilePrefix) && - file.Name()[:len(fs.NoteFilePrefix)] == fs.NoteFilePrefix { - filePath := filepath.Join(fs.StorageDir, file.Name()) - if err := os.Remove(filePath); err != nil { - return err - } - } - } - - return nil -} diff --git a/internal/util/id_generator.go b/internal/util/id_generator.go new file mode 100644 index 0000000..0f23b50 --- /dev/null +++ b/internal/util/id_generator.go @@ -0,0 +1,8 @@ +package util + +import "github.com/google/uuid" + +// GenerateID генерирует уникальный идентификатор +func GenerateID() string { + return uuid.New().String() +} diff --git a/internal/util/id_generator_test.go b/internal/util/id_generator_test.go new file mode 100644 index 0000000..34a1904 --- /dev/null +++ b/internal/util/id_generator_test.go @@ -0,0 +1,40 @@ +package util + +import ( + "regexp" + "testing" +) + +// TestGenerateID тестирует функцию GenerateID +func TestGenerateID(t *testing.T) { + // Тестируем, что генерируемый ID не пустой + id := GenerateID() + if id == "" { + t.Error("Generated ID should not be empty") + } + + // Проверяем, что ID имеет правильный формат UUID + uuidRegex := regexp.MustCompile(`^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$`) + if !uuidRegex.MatchString(id) { + t.Errorf("Generated ID '%s' does not match UUID format", id) + } +} + +// TestGenerateIDUniqueness тестирует уникальность генерируемых ID +func TestGenerateIDUniqueness(t *testing.T) { + ids := make(map[string]bool) + count := 1000 // Количество генераций для тестирования уникальности + + for i := 0; i < count; i++ { + id := GenerateID() + if ids[id] { + t.Errorf("Duplicate ID generated: %s", id) + } + ids[id] = true + } + + // Проверяем, что мы получили ожидаемое количество уникальных ID + if len(ids) != count { + t.Errorf("Expected %d unique IDs, but got %d", count, len(ids)) + } +} diff --git a/pkg/proto/note/notes.pb.go b/pkg/proto/note/notes.pb.go new file mode 100644 index 0000000..3c4f6f7 --- /dev/null +++ b/pkg/proto/note/notes.pb.go @@ -0,0 +1,546 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v6.33.0 +// source: note/notes.proto + +package note + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Message для заметки +type Note struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` + CreatedAt int64 `protobuf:"varint,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt int64 `protobuf:"varint,5,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Note) Reset() { + *x = Note{} + mi := &file_note_notes_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Note) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Note) ProtoMessage() {} + +func (x *Note) ProtoReflect() protoreflect.Message { + mi := &file_note_notes_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Note.ProtoReflect.Descriptor instead. +func (*Note) Descriptor() ([]byte, []int) { + return file_note_notes_proto_rawDescGZIP(), []int{0} +} + +func (x *Note) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Note) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *Note) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *Note) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *Note) GetUpdatedAt() int64 { + if x != nil { + return x.UpdatedAt + } + return 0 +} + +// Message для запроса создания заметки +type CreateNoteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateNoteRequest) Reset() { + *x = CreateNoteRequest{} + mi := &file_note_notes_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateNoteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateNoteRequest) ProtoMessage() {} + +func (x *CreateNoteRequest) ProtoReflect() protoreflect.Message { + mi := &file_note_notes_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateNoteRequest.ProtoReflect.Descriptor instead. +func (*CreateNoteRequest) Descriptor() ([]byte, []int) { + return file_note_notes_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateNoteRequest) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *CreateNoteRequest) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +// Message для запроса получения сущности по ID +type GetRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRequest) Reset() { + *x = GetRequest{} + mi := &file_note_notes_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRequest) ProtoMessage() {} + +func (x *GetRequest) ProtoReflect() protoreflect.Message { + mi := &file_note_notes_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRequest.ProtoReflect.Descriptor instead. +func (*GetRequest) Descriptor() ([]byte, []int) { + return file_note_notes_proto_rawDescGZIP(), []int{2} +} + +func (x *GetRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// Message для запроса обновления заметки +type UpdateNoteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateNoteRequest) Reset() { + *x = UpdateNoteRequest{} + mi := &file_note_notes_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateNoteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateNoteRequest) ProtoMessage() {} + +func (x *UpdateNoteRequest) ProtoReflect() protoreflect.Message { + mi := &file_note_notes_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateNoteRequest.ProtoReflect.Descriptor instead. +func (*UpdateNoteRequest) Descriptor() ([]byte, []int) { + return file_note_notes_proto_rawDescGZIP(), []int{3} +} + +func (x *UpdateNoteRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateNoteRequest) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *UpdateNoteRequest) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +// Message для ответа с заметкой +type NoteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Note *Note `protobuf:"bytes,1,opt,name=note,proto3" json:"note,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NoteResponse) Reset() { + *x = NoteResponse{} + mi := &file_note_notes_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NoteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NoteResponse) ProtoMessage() {} + +func (x *NoteResponse) ProtoReflect() protoreflect.Message { + mi := &file_note_notes_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NoteResponse.ProtoReflect.Descriptor instead. +func (*NoteResponse) Descriptor() ([]byte, []int) { + return file_note_notes_proto_rawDescGZIP(), []int{4} +} + +func (x *NoteResponse) GetNote() *Note { + if x != nil { + return x.Note + } + return nil +} + +// Message для ответа с несколькими заметками +type NotesListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Notes []*Note `protobuf:"bytes,1,rep,name=notes,proto3" json:"notes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NotesListResponse) Reset() { + *x = NotesListResponse{} + mi := &file_note_notes_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NotesListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NotesListResponse) ProtoMessage() {} + +func (x *NotesListResponse) ProtoReflect() protoreflect.Message { + mi := &file_note_notes_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NotesListResponse.ProtoReflect.Descriptor instead. +func (*NotesListResponse) Descriptor() ([]byte, []int) { + return file_note_notes_proto_rawDescGZIP(), []int{5} +} + +func (x *NotesListResponse) GetNotes() []*Note { + if x != nil { + return x.Notes + } + return nil +} + +// Message для ответа об успешности операции +type SuccessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SuccessResponse) Reset() { + *x = SuccessResponse{} + mi := &file_note_notes_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SuccessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SuccessResponse) ProtoMessage() {} + +func (x *SuccessResponse) ProtoReflect() protoreflect.Message { + mi := &file_note_notes_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SuccessResponse.ProtoReflect.Descriptor instead. +func (*SuccessResponse) Descriptor() ([]byte, []int) { + return file_note_notes_proto_rawDescGZIP(), []int{6} +} + +func (x *SuccessResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *SuccessResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// Пустое сообщение для запросов без параметров +type Empty struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Empty) Reset() { + *x = Empty{} + mi := &file_note_notes_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Empty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + mi := &file_note_notes_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { + return file_note_notes_proto_rawDescGZIP(), []int{7} +} + +var File_note_notes_proto protoreflect.FileDescriptor + +const file_note_notes_proto_rawDesc = "" + + "\n" + + "\x10note/notes.proto\x12\x05notes\"\x84\x01\n" + + "\x04Note\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x18\n" + + "\acontent\x18\x03 \x01(\tR\acontent\x12\x1d\n" + + "\n" + + "created_at\x18\x04 \x01(\x03R\tcreatedAt\x12\x1d\n" + + "\n" + + "updated_at\x18\x05 \x01(\x03R\tupdatedAt\"C\n" + + "\x11CreateNoteRequest\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x12\x18\n" + + "\acontent\x18\x02 \x01(\tR\acontent\"\x1c\n" + + "\n" + + "GetRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"S\n" + + "\x11UpdateNoteRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x18\n" + + "\acontent\x18\x03 \x01(\tR\acontent\"/\n" + + "\fNoteResponse\x12\x1f\n" + + "\x04note\x18\x01 \x01(\v2\v.notes.NoteR\x04note\"6\n" + + "\x11NotesListResponse\x12!\n" + + "\x05notes\x18\x01 \x03(\v2\v.notes.NoteR\x05notes\"E\n" + + "\x0fSuccessResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"\a\n" + + "\x05Empty2\xa9\x02\n" + + "\fNotesService\x12;\n" + + "\n" + + "CreateNote\x12\x18.notes.CreateNoteRequest\x1a\x13.notes.NoteResponse\x121\n" + + "\aGetNote\x12\x11.notes.GetRequest\x1a\x13.notes.NoteResponse\x12;\n" + + "\n" + + "UpdateNote\x12\x18.notes.UpdateNoteRequest\x1a\x13.notes.NoteResponse\x127\n" + + "\n" + + "DeleteNote\x12\x11.notes.GetRequest\x1a\x16.notes.SuccessResponse\x123\n" + + "\tListNotes\x12\f.notes.Empty\x1a\x18.notes.NotesListResponseB\x12Z\x10./pkg/proto/noteb\x06proto3" + +var ( + file_note_notes_proto_rawDescOnce sync.Once + file_note_notes_proto_rawDescData []byte +) + +func file_note_notes_proto_rawDescGZIP() []byte { + file_note_notes_proto_rawDescOnce.Do(func() { + file_note_notes_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_note_notes_proto_rawDesc), len(file_note_notes_proto_rawDesc))) + }) + return file_note_notes_proto_rawDescData +} + +var file_note_notes_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_note_notes_proto_goTypes = []any{ + (*Note)(nil), // 0: notes.Note + (*CreateNoteRequest)(nil), // 1: notes.CreateNoteRequest + (*GetRequest)(nil), // 2: notes.GetRequest + (*UpdateNoteRequest)(nil), // 3: notes.UpdateNoteRequest + (*NoteResponse)(nil), // 4: notes.NoteResponse + (*NotesListResponse)(nil), // 5: notes.NotesListResponse + (*SuccessResponse)(nil), // 6: notes.SuccessResponse + (*Empty)(nil), // 7: notes.Empty +} +var file_note_notes_proto_depIdxs = []int32{ + 0, // 0: notes.NoteResponse.note:type_name -> notes.Note + 0, // 1: notes.NotesListResponse.notes:type_name -> notes.Note + 1, // 2: notes.NotesService.CreateNote:input_type -> notes.CreateNoteRequest + 2, // 3: notes.NotesService.GetNote:input_type -> notes.GetRequest + 3, // 4: notes.NotesService.UpdateNote:input_type -> notes.UpdateNoteRequest + 2, // 5: notes.NotesService.DeleteNote:input_type -> notes.GetRequest + 7, // 6: notes.NotesService.ListNotes:input_type -> notes.Empty + 4, // 7: notes.NotesService.CreateNote:output_type -> notes.NoteResponse + 4, // 8: notes.NotesService.GetNote:output_type -> notes.NoteResponse + 4, // 9: notes.NotesService.UpdateNote:output_type -> notes.NoteResponse + 6, // 10: notes.NotesService.DeleteNote:output_type -> notes.SuccessResponse + 5, // 11: notes.NotesService.ListNotes:output_type -> notes.NotesListResponse + 7, // [7:12] is the sub-list for method output_type + 2, // [2:7] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_note_notes_proto_init() } +func file_note_notes_proto_init() { + if File_note_notes_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_note_notes_proto_rawDesc), len(file_note_notes_proto_rawDesc)), + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_note_notes_proto_goTypes, + DependencyIndexes: file_note_notes_proto_depIdxs, + MessageInfos: file_note_notes_proto_msgTypes, + }.Build() + File_note_notes_proto = out.File + file_note_notes_proto_goTypes = nil + file_note_notes_proto_depIdxs = nil +} diff --git a/pkg/proto/note/notes_grpc.pb.go b/pkg/proto/note/notes_grpc.pb.go new file mode 100644 index 0000000..bd32632 --- /dev/null +++ b/pkg/proto/note/notes_grpc.pb.go @@ -0,0 +1,277 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.33.0 +// source: note/notes.proto + +package note + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + NotesService_CreateNote_FullMethodName = "/notes.NotesService/CreateNote" + NotesService_GetNote_FullMethodName = "/notes.NotesService/GetNote" + NotesService_UpdateNote_FullMethodName = "/notes.NotesService/UpdateNote" + NotesService_DeleteNote_FullMethodName = "/notes.NotesService/DeleteNote" + NotesService_ListNotes_FullMethodName = "/notes.NotesService/ListNotes" +) + +// NotesServiceClient is the client API for NotesService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Сервис для работы с заметками +type NotesServiceClient interface { + CreateNote(ctx context.Context, in *CreateNoteRequest, opts ...grpc.CallOption) (*NoteResponse, error) + GetNote(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*NoteResponse, error) + UpdateNote(ctx context.Context, in *UpdateNoteRequest, opts ...grpc.CallOption) (*NoteResponse, error) + DeleteNote(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*SuccessResponse, error) + ListNotes(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*NotesListResponse, error) +} + +type notesServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewNotesServiceClient(cc grpc.ClientConnInterface) NotesServiceClient { + return ¬esServiceClient{cc} +} + +func (c *notesServiceClient) CreateNote(ctx context.Context, in *CreateNoteRequest, opts ...grpc.CallOption) (*NoteResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NoteResponse) + err := c.cc.Invoke(ctx, NotesService_CreateNote_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *notesServiceClient) GetNote(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*NoteResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NoteResponse) + err := c.cc.Invoke(ctx, NotesService_GetNote_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *notesServiceClient) UpdateNote(ctx context.Context, in *UpdateNoteRequest, opts ...grpc.CallOption) (*NoteResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NoteResponse) + err := c.cc.Invoke(ctx, NotesService_UpdateNote_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *notesServiceClient) DeleteNote(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*SuccessResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SuccessResponse) + err := c.cc.Invoke(ctx, NotesService_DeleteNote_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *notesServiceClient) ListNotes(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*NotesListResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NotesListResponse) + err := c.cc.Invoke(ctx, NotesService_ListNotes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// NotesServiceServer is the server API for NotesService service. +// All implementations must embed UnimplementedNotesServiceServer +// for forward compatibility. +// +// Сервис для работы с заметками +type NotesServiceServer interface { + CreateNote(context.Context, *CreateNoteRequest) (*NoteResponse, error) + GetNote(context.Context, *GetRequest) (*NoteResponse, error) + UpdateNote(context.Context, *UpdateNoteRequest) (*NoteResponse, error) + DeleteNote(context.Context, *GetRequest) (*SuccessResponse, error) + ListNotes(context.Context, *Empty) (*NotesListResponse, error) + mustEmbedUnimplementedNotesServiceServer() +} + +// UnimplementedNotesServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedNotesServiceServer struct{} + +func (UnimplementedNotesServiceServer) CreateNote(context.Context, *CreateNoteRequest) (*NoteResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateNote not implemented") +} +func (UnimplementedNotesServiceServer) GetNote(context.Context, *GetRequest) (*NoteResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetNote not implemented") +} +func (UnimplementedNotesServiceServer) UpdateNote(context.Context, *UpdateNoteRequest) (*NoteResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateNote not implemented") +} +func (UnimplementedNotesServiceServer) DeleteNote(context.Context, *GetRequest) (*SuccessResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteNote not implemented") +} +func (UnimplementedNotesServiceServer) ListNotes(context.Context, *Empty) (*NotesListResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListNotes not implemented") +} +func (UnimplementedNotesServiceServer) mustEmbedUnimplementedNotesServiceServer() {} +func (UnimplementedNotesServiceServer) testEmbeddedByValue() {} + +// UnsafeNotesServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to NotesServiceServer will +// result in compilation errors. +type UnsafeNotesServiceServer interface { + mustEmbedUnimplementedNotesServiceServer() +} + +func RegisterNotesServiceServer(s grpc.ServiceRegistrar, srv NotesServiceServer) { + // If the following call pancis, it indicates UnimplementedNotesServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&NotesService_ServiceDesc, srv) +} + +func _NotesService_CreateNote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateNoteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NotesServiceServer).CreateNote(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NotesService_CreateNote_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NotesServiceServer).CreateNote(ctx, req.(*CreateNoteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NotesService_GetNote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NotesServiceServer).GetNote(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NotesService_GetNote_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NotesServiceServer).GetNote(ctx, req.(*GetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NotesService_UpdateNote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateNoteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NotesServiceServer).UpdateNote(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NotesService_UpdateNote_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NotesServiceServer).UpdateNote(ctx, req.(*UpdateNoteRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NotesService_DeleteNote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NotesServiceServer).DeleteNote(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NotesService_DeleteNote_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NotesServiceServer).DeleteNote(ctx, req.(*GetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NotesService_ListNotes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NotesServiceServer).ListNotes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NotesService_ListNotes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NotesServiceServer).ListNotes(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// NotesService_ServiceDesc is the grpc.ServiceDesc for NotesService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var NotesService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "notes.NotesService", + HandlerType: (*NotesServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateNote", + Handler: _NotesService_CreateNote_Handler, + }, + { + MethodName: "GetNote", + Handler: _NotesService_GetNote_Handler, + }, + { + MethodName: "UpdateNote", + Handler: _NotesService_UpdateNote_Handler, + }, + { + MethodName: "DeleteNote", + Handler: _NotesService_DeleteNote_Handler, + }, + { + MethodName: "ListNotes", + Handler: _NotesService_ListNotes_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "note/notes.proto", +} diff --git a/pkg/proto/user/users.pb.go b/pkg/proto/user/users.pb.go new file mode 100644 index 0000000..b18f760 --- /dev/null +++ b/pkg/proto/user/users.pb.go @@ -0,0 +1,555 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v6.33.0 +// source: user/users.proto + +package user + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Message для пользователя +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + CreatedAt int64 `protobuf:"varint,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt int64 `protobuf:"varint,5,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_user_users_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_user_users_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_user_users_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *User) GetUpdatedAt() int64 { + if x != nil { + return x.UpdatedAt + } + return 0 +} + +// Message для запроса создания пользователя +type CreateUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` + Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateUserRequest) Reset() { + *x = CreateUserRequest{} + mi := &file_user_users_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserRequest) ProtoMessage() {} + +func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_users_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead. +func (*CreateUserRequest) Descriptor() ([]byte, []int) { + return file_user_users_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateUserRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *CreateUserRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *CreateUserRequest) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +// Message для запроса получения сущности по ID +type GetRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRequest) Reset() { + *x = GetRequest{} + mi := &file_user_users_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRequest) ProtoMessage() {} + +func (x *GetRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_users_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRequest.ProtoReflect.Descriptor instead. +func (*GetRequest) Descriptor() ([]byte, []int) { + return file_user_users_proto_rawDescGZIP(), []int{2} +} + +func (x *GetRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// Message для запроса обновления пользователя +type UpdateUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateUserRequest) Reset() { + *x = UpdateUserRequest{} + mi := &file_user_users_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserRequest) ProtoMessage() {} + +func (x *UpdateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_user_users_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateUserRequest.ProtoReflect.Descriptor instead. +func (*UpdateUserRequest) Descriptor() ([]byte, []int) { + return file_user_users_proto_rawDescGZIP(), []int{3} +} + +func (x *UpdateUserRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateUserRequest) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *UpdateUserRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +// Message для ответа с пользователем +type UserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserResponse) Reset() { + *x = UserResponse{} + mi := &file_user_users_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserResponse) ProtoMessage() {} + +func (x *UserResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_users_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserResponse.ProtoReflect.Descriptor instead. +func (*UserResponse) Descriptor() ([]byte, []int) { + return file_user_users_proto_rawDescGZIP(), []int{4} +} + +func (x *UserResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +// Message для ответа с несколькими пользователями +type UsersListResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UsersListResponse) Reset() { + *x = UsersListResponse{} + mi := &file_user_users_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UsersListResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UsersListResponse) ProtoMessage() {} + +func (x *UsersListResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_users_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UsersListResponse.ProtoReflect.Descriptor instead. +func (*UsersListResponse) Descriptor() ([]byte, []int) { + return file_user_users_proto_rawDescGZIP(), []int{5} +} + +func (x *UsersListResponse) GetUsers() []*User { + if x != nil { + return x.Users + } + return nil +} + +// Message для ответа об успешности операции +type SuccessResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SuccessResponse) Reset() { + *x = SuccessResponse{} + mi := &file_user_users_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SuccessResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SuccessResponse) ProtoMessage() {} + +func (x *SuccessResponse) ProtoReflect() protoreflect.Message { + mi := &file_user_users_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SuccessResponse.ProtoReflect.Descriptor instead. +func (*SuccessResponse) Descriptor() ([]byte, []int) { + return file_user_users_proto_rawDescGZIP(), []int{6} +} + +func (x *SuccessResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *SuccessResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +// Пустое сообщение для запросов без параметров +type Empty struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Empty) Reset() { + *x = Empty{} + mi := &file_user_users_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Empty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + mi := &file_user_users_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { + return file_user_users_proto_rawDescGZIP(), []int{7} +} + +var File_user_users_proto protoreflect.FileDescriptor + +const file_user_users_proto_rawDesc = "" + + "\n" + + "\x10user/users.proto\x12\x05users\"\x86\x01\n" + + "\x04User\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\x12\x14\n" + + "\x05email\x18\x03 \x01(\tR\x05email\x12\x1d\n" + + "\n" + + "created_at\x18\x04 \x01(\x03R\tcreatedAt\x12\x1d\n" + + "\n" + + "updated_at\x18\x05 \x01(\x03R\tupdatedAt\"a\n" + + "\x11CreateUserRequest\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x14\n" + + "\x05email\x18\x02 \x01(\tR\x05email\x12\x1a\n" + + "\bpassword\x18\x03 \x01(\tR\bpassword\"\x1c\n" + + "\n" + + "GetRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"U\n" + + "\x11UpdateUserRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1a\n" + + "\busername\x18\x02 \x01(\tR\busername\x12\x14\n" + + "\x05email\x18\x03 \x01(\tR\x05email\"/\n" + + "\fUserResponse\x12\x1f\n" + + "\x04user\x18\x01 \x01(\v2\v.users.UserR\x04user\"6\n" + + "\x11UsersListResponse\x12!\n" + + "\x05users\x18\x01 \x03(\v2\v.users.UserR\x05users\"E\n" + + "\x0fSuccessResponse\x12\x18\n" + + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\"\a\n" + + "\x05Empty2\xa8\x02\n" + + "\vUserService\x12;\n" + + "\n" + + "CreateUser\x12\x18.users.CreateUserRequest\x1a\x13.users.UserResponse\x121\n" + + "\aGetUser\x12\x11.users.GetRequest\x1a\x13.users.UserResponse\x12;\n" + + "\n" + + "UpdateUser\x12\x18.users.UpdateUserRequest\x1a\x13.users.UserResponse\x127\n" + + "\n" + + "DeleteUser\x12\x11.users.GetRequest\x1a\x16.users.SuccessResponse\x123\n" + + "\tListUsers\x12\f.users.Empty\x1a\x18.users.UsersListResponseB\x12Z\x10./pkg/proto/userb\x06proto3" + +var ( + file_user_users_proto_rawDescOnce sync.Once + file_user_users_proto_rawDescData []byte +) + +func file_user_users_proto_rawDescGZIP() []byte { + file_user_users_proto_rawDescOnce.Do(func() { + file_user_users_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_user_users_proto_rawDesc), len(file_user_users_proto_rawDesc))) + }) + return file_user_users_proto_rawDescData +} + +var file_user_users_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_user_users_proto_goTypes = []any{ + (*User)(nil), // 0: users.User + (*CreateUserRequest)(nil), // 1: users.CreateUserRequest + (*GetRequest)(nil), // 2: users.GetRequest + (*UpdateUserRequest)(nil), // 3: users.UpdateUserRequest + (*UserResponse)(nil), // 4: users.UserResponse + (*UsersListResponse)(nil), // 5: users.UsersListResponse + (*SuccessResponse)(nil), // 6: users.SuccessResponse + (*Empty)(nil), // 7: users.Empty +} +var file_user_users_proto_depIdxs = []int32{ + 0, // 0: users.UserResponse.user:type_name -> users.User + 0, // 1: users.UsersListResponse.users:type_name -> users.User + 1, // 2: users.UserService.CreateUser:input_type -> users.CreateUserRequest + 2, // 3: users.UserService.GetUser:input_type -> users.GetRequest + 3, // 4: users.UserService.UpdateUser:input_type -> users.UpdateUserRequest + 2, // 5: users.UserService.DeleteUser:input_type -> users.GetRequest + 7, // 6: users.UserService.ListUsers:input_type -> users.Empty + 4, // 7: users.UserService.CreateUser:output_type -> users.UserResponse + 4, // 8: users.UserService.GetUser:output_type -> users.UserResponse + 4, // 9: users.UserService.UpdateUser:output_type -> users.UserResponse + 6, // 10: users.UserService.DeleteUser:output_type -> users.SuccessResponse + 5, // 11: users.UserService.ListUsers:output_type -> users.UsersListResponse + 7, // [7:12] is the sub-list for method output_type + 2, // [2:7] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_user_users_proto_init() } +func file_user_users_proto_init() { + if File_user_users_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_user_users_proto_rawDesc), len(file_user_users_proto_rawDesc)), + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_user_users_proto_goTypes, + DependencyIndexes: file_user_users_proto_depIdxs, + MessageInfos: file_user_users_proto_msgTypes, + }.Build() + File_user_users_proto = out.File + file_user_users_proto_goTypes = nil + file_user_users_proto_depIdxs = nil +} diff --git a/pkg/proto/user/users_grpc.pb.go b/pkg/proto/user/users_grpc.pb.go new file mode 100644 index 0000000..7955dd3 --- /dev/null +++ b/pkg/proto/user/users_grpc.pb.go @@ -0,0 +1,277 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v6.33.0 +// source: user/users.proto + +package user + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + UserService_CreateUser_FullMethodName = "/users.UserService/CreateUser" + UserService_GetUser_FullMethodName = "/users.UserService/GetUser" + UserService_UpdateUser_FullMethodName = "/users.UserService/UpdateUser" + UserService_DeleteUser_FullMethodName = "/users.UserService/DeleteUser" + UserService_ListUsers_FullMethodName = "/users.UserService/ListUsers" +) + +// UserServiceClient is the client API for UserService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Сервис для работы с пользователями +type UserServiceClient interface { + CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*UserResponse, error) + GetUser(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*UserResponse, error) + UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UserResponse, error) + DeleteUser(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*SuccessResponse, error) + ListUsers(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*UsersListResponse, error) +} + +type userServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { + return &userServiceClient{cc} +} + +func (c *userServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*UserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UserResponse) + err := c.cc.Invoke(ctx, UserService_CreateUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) GetUser(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*UserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UserResponse) + err := c.cc.Invoke(ctx, UserService_GetUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UserResponse) + err := c.cc.Invoke(ctx, UserService_UpdateUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) DeleteUser(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*SuccessResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SuccessResponse) + err := c.cc.Invoke(ctx, UserService_DeleteUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) ListUsers(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*UsersListResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UsersListResponse) + err := c.cc.Invoke(ctx, UserService_ListUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// UserServiceServer is the server API for UserService service. +// All implementations must embed UnimplementedUserServiceServer +// for forward compatibility. +// +// Сервис для работы с пользователями +type UserServiceServer interface { + CreateUser(context.Context, *CreateUserRequest) (*UserResponse, error) + GetUser(context.Context, *GetRequest) (*UserResponse, error) + UpdateUser(context.Context, *UpdateUserRequest) (*UserResponse, error) + DeleteUser(context.Context, *GetRequest) (*SuccessResponse, error) + ListUsers(context.Context, *Empty) (*UsersListResponse, error) + mustEmbedUnimplementedUserServiceServer() +} + +// UnimplementedUserServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedUserServiceServer struct{} + +func (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*UserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") +} +func (UnimplementedUserServiceServer) GetUser(context.Context, *GetRequest) (*UserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented") +} +func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserRequest) (*UserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateUser not implemented") +} +func (UnimplementedUserServiceServer) DeleteUser(context.Context, *GetRequest) (*SuccessResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented") +} +func (UnimplementedUserServiceServer) ListUsers(context.Context, *Empty) (*UsersListResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented") +} +func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} +func (UnimplementedUserServiceServer) testEmbeddedByValue() {} + +// UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to UserServiceServer will +// result in compilation errors. +type UnsafeUserServiceServer interface { + mustEmbedUnimplementedUserServiceServer() +} + +func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { + // If the following call pancis, it indicates UnimplementedUserServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&UserService_ServiceDesc, srv) +} + +func _UserService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).CreateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_CreateUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).GetUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_GetUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).GetUser(ctx, req.(*GetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_UpdateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).UpdateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_UpdateUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).UpdateUser(ctx, req.(*UpdateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).DeleteUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_DeleteUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).DeleteUser(ctx, req.(*GetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).ListUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_ListUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).ListUsers(ctx, req.(*Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var UserService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "users.UserService", + HandlerType: (*UserServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateUser", + Handler: _UserService_CreateUser_Handler, + }, + { + MethodName: "GetUser", + Handler: _UserService_GetUser_Handler, + }, + { + MethodName: "UpdateUser", + Handler: _UserService_UpdateUser_Handler, + }, + { + MethodName: "DeleteUser", + Handler: _UserService_DeleteUser_Handler, + }, + { + MethodName: "ListUsers", + Handler: _UserService_ListUsers_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "user/users.proto", +} diff --git a/scripts/generate-proto.sh b/scripts/generate-proto.sh new file mode 100755 index 0000000..31697f9 --- /dev/null +++ b/scripts/generate-proto.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -e + +PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +PROTO_DIR="$PROJECT_ROOT/api/proto" +GO_OUT_DIR="$PROJECT_ROOT/pkg/proto" +PROTOC_GEN_GO="$(go env GOPATH)/bin/protoc-gen-go" +PROTOC_GEN_GO_GRPC="$(go env GOPATH)/bin/protoc-gen-go-grpc" + +echo "🔧 Generating Go code from protobuf definitions..." + +# Проверяем наличие плагинов +if [ ! -f "$PROTOC_GEN_GO" ]; then + echo "❌ Error: protoc-gen-go not found at $PROTOC_GEN_GO" + echo "Please run: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest" + exit 1 +fi + +if [ ! -f "$PROTOC_GEN_GO_GRPC" ]; then + echo "❌ Error: protoc-gen-go-grpc not found at $PROTOC_GEN_GO_GRPC" + echo "Please run: go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest" + exit 1 +fi + +echo "⚙️ Using protoc-gen-go: $PROTOC_GEN_GO" +echo "⚙️ Using protoc-gen-go-grpc: $PROTOC_GEN_GO_GRPC" + +# Создаем директорию для сгенерированного кода +mkdir -p "$GO_OUT_DIR" + +# Генерируем код для каждого protobuf файла +find "$PROTO_DIR" -name "*.proto" -print0 | while IFS= read -r -d '' proto_file; do + echo "📦 Generating: $proto_file" + + protoc --go_out="$GO_OUT_DIR" \ + --go-grpc_out="$GO_OUT_DIR" \ + --go_opt=paths=source_relative \ + --go-grpc_opt=paths=source_relative \ + -I="$PROTO_DIR" \ + "$proto_file" +done + +echo "✅ Protobuf code generation completed!" diff --git a/scripts/generate-swag.sh b/scripts/generate-swag.sh new file mode 100755 index 0000000..157d8ab --- /dev/null +++ b/scripts/generate-swag.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +set -e + +PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +MAIN_GO_FILE="cmd/web-server/main.go" +SWAGGER_DOCS_DIR="$PROJECT_ROOT/docs" + +echo "🔧 Generating Swagger documentation..." + +# Проверяем наличие swag +if ! command -v swag &> /dev/null; then + echo "❌ Error: swag not found" + echo "Please install swag by running: go install github.com/swaggo/swag/cmd/swag@latest" + exit 1 +fi + +echo "⚙️ Using swag: $(which swag)" + +# Проверяем наличие главного Go файла +if [ ! -f "$MAIN_GO_FILE" ]; then + echo "❌ Error: Main Go file not found at $MAIN_GO_FILE" + exit 1 +fi + +# Создаем директорию для сгенерированного кода +mkdir -p "$SWAGGER_DOCS_DIR" + +echo "📦 Generating Swagger docs from: $MAIN_GO_FILE" + +# Генерируем Swagger документацию +cd "$PROJECT_ROOT" && swag init -g "$MAIN_GO_FILE" --output "docs" + +echo "✅ Swagger documentation generation completed!"