Init commit

This commit is contained in:
NikitolProject 2025-03-23 20:05:51 +03:00
commit 6c37504c06
42 changed files with 8983 additions and 0 deletions

31
.dockerignore Normal file
View File

@ -0,0 +1,31 @@
# Git
.git
.gitignore
# Docker
Dockerfile
docker-compose.yaml
.dockerignore
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# Dependency directories (remove the comment below if you want to include vendored dependencies)
# vendor/
# Environment variables
.env
# IDE specific files
.idea
.vscode

6
.env.example Normal file
View File

@ -0,0 +1,6 @@
TELEGRAM_BOT_TOKEN=123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ
TELEGRAM_APP_URL=https://wish-list-bot.telegram.bot
MONGO_USER=mongo_user
MONGO_PASSWORD=mongo_password
MONGO_DATABASE=books

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env

46
Dockerfile Normal file
View File

@ -0,0 +1,46 @@
FROM golang:1.23-alpine AS builder
WORKDIR /app
# Copy go.mod and go.sum first to leverage Docker cache
COPY go.mod go.sum ./
RUN go mod download
# Copy the rest of the code
COPY . .
# Build the application
RUN go build -o app ./cmd/main.go
# Use a minimal alpine image for the final container
FROM alpine:latest
# Install necessary packages
RUN apk --no-cache add ca-certificates netcat-openbsd
WORKDIR /app
# Copy the pre-built binary from the builder stage
COPY --from=builder /app/app .
# Copy source code and tests for testing
COPY --from=builder /app/pkg ./pkg
COPY --from=builder /app/api ./api
COPY --from=builder /app/tests ./tests
COPY --from=builder /app/scripts ./scripts
COPY --from=builder /app/go.mod ./go.mod
COPY --from=builder /app/go.sum ./go.sum
# Copy the Go binary and environment for tests
COPY --from=builder /usr/local/go /usr/local/go
ENV PATH="/usr/local/go/bin:${PATH}"
ENV GOPATH="/go"
# Set environment variables
ENV GO_ENV=production
# Expose the application port
EXPOSE 8080
# Command to run the application
CMD ["./app"]

168
README.md Normal file
View File

@ -0,0 +1,168 @@
# Wish List API
![Go](https://img.shields.io/badge/go-%2300ADD8.svg?style=for-the-badge&logo=go&logoColor=white)
![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white)
![Swagger](https://img.shields.io/badge/-Swagger-%23Clojure?style=for-the-badge&logo=swagger&logoColor=white)
![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)
API-сервер для приложения списка желаний с использованием принципов чистой архитектуры.
## Описание
Wish List API - это RESTful API-сервис, который позволяет пользователям создавать, управлять и делиться своими списками желаемых подарков. Приложение разработано с использованием чистой архитектуры для обеспечения масштабируемости, тестируемости и поддержки в долгосрочной перспективе.
## Основные возможности
- Регистрация и аутентификация пользователей
- Авторизация через Telegram
- Создание, просмотр, редактирование и удаление списков желаний
- Добавление, просмотр, редактирование и удаление элементов в списке желаний
- Настройка приватности списков желаний (публичный/приватный доступ)
- Подробная документация API через Swagger
## Технологический стек
- **Go** - язык программирования
- **Fiber** - веб-фреймворк
- **MongoDB** - база данных
- **JWT** - токены для аутентификации
- **Swagger** - документация API
- **Docker** - контейнеризация приложения
## Архитектура проекта
Проект следует принципам чистой архитектуры, разделяя код на следующие слои:
- `api/` - HTTP обработчики, маршрутизация и представления
- `handlers/` - HTTP обработчики запросов
- `middleware/` - промежуточные обработчики
- `presenter/` - форматирование ответов
- `routes/` - маршрутизация API
- `cmd/` - точка входа в приложение
- `docs/` - автоматически сгенерированная Swagger документация
- `pkg/` - основная бизнес-логика и сущности
- `auth/` - аутентификация и авторизация
- `entities/` - основные сущности (модели данных)
- `user/` - сервисы для работы с пользователями
- `wish-list/` - сервисы для работы со списками желаний
- `tests/` - тесты
- `unit/` - модульные тесты
- `integration/` - интеграционные тесты
## Установка и запуск
### Требования
- [Go](https://golang.org/dl/) 1.18 или выше
- [Docker](https://www.docker.com/get-started) и Docker Compose
- [MongoDB](https://www.mongodb.com/try/download/community) (при локальном запуске без Docker)
### Переменные окружения
Создайте файл `.env` на основе `.env.example`:
```env
TELEGRAM_BOT_TOKEN=123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ
TELEGRAM_APP_URL=https://wish-list-bot.telegram.bot
MONGO_USER=mongo_user
MONGO_PASSWORD=mongo_password
MONGO_DATABASE=books
```
### Запуск с использованием Docker
1. Клонируйте репозиторий:
```bash
git clone https://github.com/yourusername/wish-list-api.git
cd wish-list-api
```
2. Запустите приложение с помощью Docker Compose:
```bash
docker-compose up -d
```
3. API будет доступен по адресу `http://localhost:8080`
4. Документация Swagger доступна по адресу `http://localhost:8080/docs/index.html`
### Локальный запуск для разработки
1. Клонируйте репозиторий:
```bash
git clone https://github.com/yourusername/wish-list-api.git
cd wish-list-api
```
2. Установите зависимости:
```bash
go mod download
```
3. Запустите приложение:
```bash
go run cmd/main.go
```
## API Endpoints
Основные эндпоинты API:
### Аутентификация
- **POST /api/auth/register** - Регистрация нового пользователя
- **POST /api/auth/login** - Авторизация пользователя
- **GET /api/auth/telegram-login** - Авторизация через Telegram
### Пользователи
- **GET /api/users/:id** - Получение информации о пользователе
- **PUT /api/users/:id** - Обновление информации пользователя
### Списки желаний
- **POST /api/wishlist** - Создание нового списка желаний
- **GET /api/wishlist/:id** - Получение списка желаний по ID
- **GET /api/wishlist/user/:userId** - Получение всех списков желаний пользователя
- **PUT /api/wishlist/:id** - Обновление списка желаний
- **DELETE /api/wishlist/:id** - Удаление списка желаний
### Элементы списка желаний
- **POST /api/wishlist/item** - Добавление элемента в список желаний
- **GET /api/wishlist/:wishlistId/items** - Получение всех элементов списка желаний
- **GET /api/wishlist/item/:id** - Получение элемента списка по ID
- **PUT /api/wishlist/item/:id** - Обновление элемента списка
- **DELETE /api/wishlist/item/:id** - Удаление элемента из списка
## Тестирование
### Запуск модульных тестов
```bash
go test ./tests/unit/...
```
### Запуск интеграционных тестов
```bash
go test ./tests/integration/...
```
### Запуск всех тестов с покрытием
```bash
./run-docker-tests.sh
```
## Contributing
1. Форкните репозиторий
2. Создайте новую ветку для вашей функциональности (`git checkout -b feature/amazing-feature`)
3. Зафиксируйте изменения (`git commit -m 'Add some amazing feature'`)
4. Отправьте изменения в ветку (`git push origin feature/amazing-feature`)
5. Создайте Pull Request
## Лицензия
Распространяется под лицензией MIT. Смотрите `LICENSE` для получения дополнительной информации.

View File

@ -0,0 +1,161 @@
package handlers
import (
"net/http"
"wish-list-api/api/presenter"
"wish-list-api/pkg/auth"
"wish-list-api/pkg/entities"
"github.com/gofiber/fiber/v2"
)
// @Summary Вход пользователя
// @Description Аутентифицирует пользователя и выдает JWT токены
// @Tags auth
// @Accept json
// @Produce json
// @Param credentials body entities.LoginRequest true "Учетные данные пользователя"
// @Success 200 {object} presenter.AuthResponse
// @Failure 400 {object} presenter.AuthResponse
// @Failure 401 {object} presenter.AuthResponse
// @Failure 500 {object} presenter.AuthResponse
// @Router /auth/login [post]
func Login(service auth.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
var requestBody entities.LoginRequest
err := c.BodyParser(&requestBody)
if err != nil {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.AuthErrorResponse(err))
}
if requestBody.Email == "" || requestBody.Password == "" {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.AuthErrorResponse(fiber.ErrBadRequest))
}
tokens, err := service.Login(&requestBody)
if err != nil {
c.Status(http.StatusUnauthorized)
return c.JSON(presenter.AuthErrorResponse(err))
}
return c.JSON(presenter.AuthSuccessResponse(tokens))
}
}
// @Summary Регистрация пользователя
// @Description Регистрирует нового пользователя и выдает JWT токены
// @Tags auth
// @Accept json
// @Produce json
// @Param user body entities.RegisterRequest true "Данные нового пользователя"
// @Success 200 {object} presenter.AuthResponse
// @Failure 400 {object} presenter.AuthResponse
// @Failure 409 {object} presenter.AuthResponse
// @Failure 500 {object} presenter.AuthResponse
// @Router /auth/register [post]
func Register(service auth.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
var requestBody entities.RegisterRequest
err := c.BodyParser(&requestBody)
if err != nil {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.AuthErrorResponse(err))
}
if requestBody.Email == "" || requestBody.Password == "" {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.AuthErrorResponse(fiber.ErrBadRequest))
}
user, err := service.Register(&requestBody)
if err != nil {
c.Status(http.StatusConflict)
return c.JSON(presenter.AuthErrorResponse(err))
}
tokens, err := service.Login(&entities.LoginRequest{
Email: requestBody.Email,
Password: requestBody.Password,
})
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.AuthErrorResponse(err))
}
return c.JSON(presenter.AuthSuccessResponseWithUser(tokens, user))
}
}
// @Summary Обновление токенов
// @Description Обновляет JWT токены с помощью refresh токена
// @Tags auth
// @Accept json
// @Produce json
// @Param refreshToken body entities.TokenRequest true "Refresh токен"
// @Success 200 {object} presenter.AuthResponse
// @Failure 400 {object} presenter.AuthResponse
// @Failure 401 {object} presenter.AuthResponse
// @Failure 500 {object} presenter.AuthResponse
// @Router /auth/refresh [post]
func RefreshToken(service auth.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
var requestBody entities.TokenRequest
err := c.BodyParser(&requestBody)
if err != nil {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.AuthErrorResponse(err))
}
if requestBody.RefreshToken == "" {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.AuthErrorResponse(fiber.ErrBadRequest))
}
tokens, err := service.RefreshToken(requestBody.RefreshToken)
if err != nil {
c.Status(http.StatusUnauthorized)
return c.JSON(presenter.AuthErrorResponse(err))
}
return c.JSON(presenter.AuthSuccessResponse(tokens))
}
}
// @Summary Вход пользователя через Telegram
// @Description Аутентифицирует пользователя через Telegram и выдает JWT токены
// @Tags auth
// @Accept json
// @Produce json
// @Param credentials body entities.TelegramAuthRequest true "Данные аутентификации Telegram"
// @Success 200 {object} presenter.AuthResponse
// @Failure 400 {object} presenter.AuthResponse
// @Failure 401 {object} presenter.AuthResponse
// @Failure 500 {object} presenter.AuthResponse
// @Router /auth/telegram [post]
func LoginWithTelegram(telegramService auth.TelegramAuthService) fiber.Handler {
return func(c *fiber.Ctx) error {
var requestBody entities.TelegramAuthRequest
err := c.BodyParser(&requestBody)
if err != nil {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.AuthErrorResponse(err))
}
if requestBody.TelegramID == 0 || requestBody.AuthDate == 0 || requestBody.Hash == "" {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.AuthErrorResponse(fiber.ErrBadRequest))
}
tokens, err := telegramService.AuthenticateWithTelegram(&requestBody)
if err != nil {
c.Status(http.StatusUnauthorized)
return c.JSON(presenter.AuthErrorResponse(err))
}
c.Status(http.StatusOK)
return c.JSON(presenter.AuthSuccessResponse(tokens))
}
}

View File

@ -0,0 +1,177 @@
package handlers
import (
"net/http"
"wish-list-api/api/presenter"
"wish-list-api/pkg/entities"
"wish-list-api/pkg/user"
"github.com/gofiber/fiber/v2"
"github.com/pkg/errors"
)
// @Summary Добавить нового пользователя
// @Description Создает нового пользователя в системе
// @Tags users
// @Accept json
// @Produce json
// @Param user body entities.User true "Информация о пользователе"
// @Success 200 {object} presenter.UserResponse
// @Failure 400 {object} presenter.UserResponse
// @Failure 500 {object} presenter.UserResponse
// @Router /users [post]
func CreateUser(service user.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
var requestBody entities.User
err := c.BodyParser(&requestBody)
if err != nil {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.UserErrorResponse(err))
}
if requestBody.Email == "" || requestBody.Password == "" {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.UserErrorResponse(errors.New(
"Please specify email and password")))
}
result, err := service.CreateUser(&requestBody)
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.UserErrorResponse(err))
}
return c.JSON(presenter.UserSuccessResponse(result))
}
}
// @Summary Обновить пользователя
// @Description Обновляет информацию о существующем пользователе
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param user body entities.User true "Информация о пользователе для обновления"
// @Success 200 {object} presenter.UserResponse
// @Failure 400 {object} presenter.UserResponse
// @Failure 500 {object} presenter.UserResponse
// @Router /users [put]
func UpdateUser(service user.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
var requestBody entities.User
err := c.BodyParser(&requestBody)
if err != nil {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.UserErrorResponse(err))
}
result, err := service.UpdateUser(&requestBody)
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.UserErrorResponse(err))
}
return c.JSON(presenter.UserSuccessResponse(result))
}
}
// @Summary Удалить пользователя
// @Description Удаляет пользователя из системы по ID
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body entities.DeleteUserRequest true "ID пользователя для удаления"
// @Success 200 {object} presenter.UserResponse
// @Failure 400 {object} presenter.UserResponse
// @Failure 500 {object} presenter.UserResponse
// @Router /users [delete]
func DeleteUser(service user.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
var requestBody entities.DeleteUserRequest
err := c.BodyParser(&requestBody)
if err != nil {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.UserErrorResponse(err))
}
userID := requestBody.ID
err = service.DeleteUser(userID)
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.UserErrorResponse(err))
}
return c.JSON(presenter.UserSuccessResponse(nil))
}
}
// @Summary Получить всех пользователей
// @Description Возвращает список всех пользователей в системе
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} presenter.UsersResponse
// @Failure 500 {object} presenter.UserResponse
// @Router /users [get]
func GetAllUsers(service user.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
fetched, err := service.GetAllUsers()
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.UserErrorResponse(err))
}
return c.JSON(presenter.UsersSuccessResponse(fetched))
}
}
// @Summary Получить пользователя по ID
// @Description Возвращает информацию о пользователе по его ID
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "ID пользователя"
// @Success 200 {object} presenter.UserResponse
// @Failure 400 {object} presenter.UserResponse
// @Failure 500 {object} presenter.UserResponse
// @Router /users/{id} [get]
func GetUserByID(service user.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.UserErrorResponse(errors.New("id is required")))
}
result, err := service.GetUser(id)
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.UserErrorResponse(err))
}
return c.JSON(presenter.UserSuccessResponse(result))
}
}
// @Summary Получить пользователя по Email
// @Description Возвращает информацию о пользователе по его Email
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param email path string true "Email пользователя"
// @Success 200 {object} presenter.UserResponse
// @Failure 400 {object} presenter.UserResponse
// @Failure 500 {object} presenter.UserResponse
// @Router /users/email/{email} [get]
func GetUserByEmail(service user.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
email := c.Params("email")
if email == "" {
c.Status(http.StatusBadRequest)
return c.JSON(presenter.UserErrorResponse(errors.New("email is required")))
}
result, err := service.GetUserByEmail(email)
if err != nil {
c.Status(http.StatusInternalServerError)
return c.JSON(presenter.UserErrorResponse(err))
}
return c.JSON(presenter.UserSuccessResponse(result))
}
}

View File

@ -0,0 +1,456 @@
package handlers
import (
"wish-list-api/api/presenter"
"wish-list-api/pkg/auth"
"wish-list-api/pkg/entities"
wishlist "wish-list-api/pkg/wish-list"
"github.com/gofiber/fiber/v2"
)
type WishListHandler struct {
wishListService wishlist.Service
authService auth.Service
}
func NewWishListHandler(wishListService wishlist.Service, authService auth.Service) *WishListHandler {
return &WishListHandler{
wishListService: wishListService,
authService: authService,
}
}
// @Summary Create a new wishlist
// @Description Create a new wishlist for the authenticated user
// @Tags wishlist
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param wishlist body entities.WishList true "Wishlist data"
// @Success 201 {object} presenter.WishListResponse
// @Failure 400 {object} presenter.WishListResponse
// @Failure 401 {object} presenter.WishListResponse
// @Security BearerAuth
// @Router /wishlist [post]
func (h *WishListHandler) CreateWishList(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
userID, err := h.authService.GetUserIDFromToken(token)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err))
}
wishList := new(entities.WishList)
if err := c.BodyParser(wishList); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err))
}
wishList.UserID = userID
result, err := h.wishListService.CreateWishList(wishList)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err))
}
return c.Status(fiber.StatusCreated).JSON(presenter.WishListSuccessResponse(result))
}
// @Summary Get a wishlist
// @Description Get a wishlist by its ID
// @Tags wishlist
// @Accept json
// @Produce json
// @Param id path string true "Wishlist ID"
// @Success 200 {object} presenter.WishListResponse
// @Failure 404 {object} presenter.WishListResponse
// @Security BearerAuth
// @Router /wishlist/{id} [get]
func (h *WishListHandler) GetWishList(c *fiber.Ctx) error {
id := c.Params("id")
result, err := h.wishListService.GetWishList(id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
if !result.IsPublic {
token := c.Get("Authorization")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
userID, err := h.authService.GetUserIDFromToken(token)
if err != nil || userID != result.UserID {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
}
return c.Status(fiber.StatusOK).JSON(presenter.WishListSuccessResponse(result))
}
// @Summary Get user wishlists
// @Description Get all wishlists for a specific user
// @Tags wishlist
// @Accept json
// @Produce json
// @Param userId path string true "User ID"
// @Success 200 {object} presenter.WishListsResponse
// @Failure 404 {object} presenter.WishListResponse
// @Security BearerAuth
// @Router /wishlist/user/{userId} [get]
func (h *WishListHandler) GetUserWishLists(c *fiber.Ctx) error {
userID := c.Params("userId")
isOwner := false
token := c.Get("Authorization")
if token != "" {
requestorID, err := h.authService.GetUserIDFromToken(token)
if err == nil && requestorID == userID {
isOwner = true
}
}
var result *[]presenter.WishList
var err error
result, err = h.wishListService.GetAllWishLists(userID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
if !isOwner {
publicLists := []presenter.WishList{}
for _, list := range *result {
if list.IsPublic {
publicLists = append(publicLists, list)
}
}
filteredResult := publicLists
result = &filteredResult
}
return c.Status(fiber.StatusOK).JSON(presenter.WishListsSuccessResponse(result))
}
// @Summary Update a wishlist
// @Description Update an existing wishlist
// @Tags wishlist
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param id path string true "Wishlist ID"
// @Param wishlist body entities.WishList true "Updated wishlist data"
// @Success 200 {object} presenter.WishListResponse
// @Failure 400 {object} presenter.WishListResponse
// @Failure 401 {object} presenter.WishListResponse
// @Failure 404 {object} presenter.WishListResponse
// @Security BearerAuth
// @Router /wishlist/{id} [put]
func (h *WishListHandler) UpdateWishList(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
userID, err := h.authService.GetUserIDFromToken(token)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err))
}
id := c.Params("id")
currentWishList, err := h.wishListService.GetWishList(id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
if currentWishList.UserID != userID {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
wishList := new(entities.WishList)
if err := c.BodyParser(wishList); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err))
}
wishList.ID = id
wishList.UserID = userID
result, err := h.wishListService.UpdateWishList(wishList)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err))
}
return c.Status(fiber.StatusOK).JSON(presenter.WishListSuccessResponse(result))
}
// @Summary Delete a wishlist
// @Description Delete a wishlist and all its items
// @Tags wishlist
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param id path string true "Wishlist ID"
// @Success 200 {object} presenter.WishListResponse
// @Failure 401 {object} presenter.WishListResponse
// @Failure 404 {object} presenter.WishListResponse
// @Security BearerAuth
// @Router /wishlist/{id} [delete]
func (h *WishListHandler) DeleteWishList(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
userID, err := h.authService.GetUserIDFromToken(token)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err))
}
id := c.Params("id")
currentWishList, err := h.wishListService.GetWishList(id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
if currentWishList.UserID != userID {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
if err := h.wishListService.DeleteWishList(id); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(presenter.WishListErrorResponse(err))
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"status": true,
"data": "Wishlist deleted successfully",
"error": nil,
})
}
// @Summary Create a wishlist item
// @Description Create a new item for a wishlist
// @Tags wishlist-items
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param item body entities.WishListItem true "Wishlist item data"
// @Success 201 {object} presenter.WishListItemResponse
// @Failure 400 {object} presenter.WishListItemResponse
// @Failure 401 {object} presenter.WishListItemResponse
// @Security BearerAuth
// @Router /wishlist/item [post]
func (h *WishListHandler) CreateWishListItem(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
userID, err := h.authService.GetUserIDFromToken(token)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err))
}
item := new(entities.WishListItem)
if err := c.BodyParser(item); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err))
}
wishList, err := h.wishListService.GetWishList(item.WishListID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
if wishList.UserID != userID {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
result, err := h.wishListService.CreateWishListItem(item)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err))
}
return c.Status(fiber.StatusCreated).JSON(presenter.WishListItemSuccessResponse(result))
}
// @Summary Get a wishlist item
// @Description Get a wishlist item by its ID
// @Tags wishlist-items
// @Accept json
// @Produce json
// @Param id path string true "Item ID"
// @Success 200 {object} presenter.WishListItemResponse
// @Failure 404 {object} presenter.WishListItemResponse
// @Security BearerAuth
// @Router /wishlist/item/{id} [get]
func (h *WishListHandler) GetWishListItem(c *fiber.Ctx) error {
id := c.Params("id")
result, err := h.wishListService.GetWishListItem(id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
wishList, err := h.wishListService.GetWishList(result.WishListID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
if !wishList.IsPublic {
token := c.Get("Authorization")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
userID, err := h.authService.GetUserIDFromToken(token)
if err != nil || userID != wishList.UserID {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
}
return c.Status(fiber.StatusOK).JSON(presenter.WishListItemSuccessResponse(result))
}
// @Summary Get wishlist items
// @Description Get all items in a wishlist
// @Tags wishlist-items
// @Accept json
// @Produce json
// @Param wishlistId path string true "Wishlist ID"
// @Success 200 {object} presenter.WishListItemsResponse
// @Failure 404 {object} presenter.WishListItemsResponse
// @Security BearerAuth
// @Router /wishlist/{wishlistId}/items [get]
func (h *WishListHandler) GetWishListItems(c *fiber.Ctx) error {
wishListID := c.Params("wishlistId")
wishList, err := h.wishListService.GetWishList(wishListID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
if !wishList.IsPublic {
token := c.Get("Authorization")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
userID, err := h.authService.GetUserIDFromToken(token)
if err != nil || userID != wishList.UserID {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
}
result, err := h.wishListService.GetAllWishListItems(wishListID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
return c.Status(fiber.StatusOK).JSON(presenter.WishListItemsSuccessResponse(result))
}
// @Summary Update a wishlist item
// @Description Update an existing wishlist item
// @Tags wishlist-items
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param id path string true "Item ID"
// @Param item body entities.WishListItem true "Updated item data"
// @Success 200 {object} presenter.WishListItemResponse
// @Failure 400 {object} presenter.WishListItemResponse
// @Failure 401 {object} presenter.WishListItemResponse
// @Failure 404 {object} presenter.WishListItemResponse
// @Security BearerAuth
// @Router /wishlist/item/{id} [put]
func (h *WishListHandler) UpdateWishListItem(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
userID, err := h.authService.GetUserIDFromToken(token)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err))
}
id := c.Params("id")
currentItem, err := h.wishListService.GetWishListItem(id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
wishList, err := h.wishListService.GetWishList(currentItem.WishListID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
if wishList.UserID != userID {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
item := new(entities.WishListItem)
if err := c.BodyParser(item); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err))
}
item.ID = id
item.WishListID = currentItem.WishListID
result, err := h.wishListService.UpdateWishListItem(item)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err))
}
return c.Status(fiber.StatusOK).JSON(presenter.WishListItemSuccessResponse(result))
}
// @Summary Delete a wishlist item
// @Description Delete a wishlist item
// @Tags wishlist-items
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer token"
// @Param id path string true "Item ID"
// @Success 200 {object} presenter.WishListItemResponse
// @Failure 401 {object} presenter.WishListItemResponse
// @Failure 404 {object} presenter.WishListItemResponse
// @Security BearerAuth
// @Router /wishlist/item/{id} [delete]
func (h *WishListHandler) DeleteWishListItem(c *fiber.Ctx) error {
token := c.Get("Authorization")
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
userID, err := h.authService.GetUserIDFromToken(token)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err))
}
id := c.Params("id")
currentItem, err := h.wishListService.GetWishListItem(id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
wishList, err := h.wishListService.GetWishList(currentItem.WishListID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err))
}
if wishList.UserID != userID {
return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized))
}
if err := h.wishListService.DeleteWishListItem(id); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(presenter.WishListErrorResponse(err))
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"status": true,
"data": "Wishlist item deleted successfully",
"error": nil,
})
}

View File

@ -0,0 +1,46 @@
package middleware
import (
"strings"
"wish-list-api/pkg/auth"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
func Protected(authService auth.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"status": false,
"error": "требуется авторизация",
})
}
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
token, err := authService.ValidateToken(tokenString)
if err != nil || !token.Valid {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"status": false,
"error": "недействительный токен",
})
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"status": false,
"error": "недействительный токен",
})
}
c.Locals("userID", claims["user_id"])
c.Locals("email", claims["email"])
return c.Next()
}
}

46
api/presenter/auth.go Normal file
View File

@ -0,0 +1,46 @@
package presenter
import (
"wish-list-api/pkg/entities"
"github.com/gofiber/fiber/v2"
)
type AuthResponse struct {
Status bool `json:"status"`
Data *entities.TokenPair `json:"data,omitempty"`
User *User `json:"user,omitempty"`
Error *string `json:"error"`
}
func AuthSuccessResponse(tokens *entities.TokenPair) *fiber.Map {
return &fiber.Map{
"status": true,
"data": tokens,
"error": nil,
}
}
func AuthSuccessResponseWithUser(tokens *entities.TokenPair, user *entities.User) *fiber.Map {
userResponse := User{
ID: user.ID,
Email: user.Email,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
return &fiber.Map{
"status": true,
"data": tokens,
"user": userResponse,
"error": nil,
}
}
func AuthErrorResponse(err error) *fiber.Map {
return &fiber.Map{
"status": false,
"data": nil,
"error": err.Error(),
}
}

59
api/presenter/user.go Normal file
View File

@ -0,0 +1,59 @@
package presenter
import (
"wish-list-api/pkg/entities"
"time"
"github.com/gofiber/fiber/v2"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type User struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type UserResponse struct {
Status bool `json:"status"`
Data User `json:"data"`
Error *string `json:"error"`
}
type UsersResponse struct {
Status bool `json:"status"`
Data []User `json:"data"`
Error *string `json:"error"`
}
func UserSuccessResponse(data *entities.User) *fiber.Map {
user := User{
ID: data.ID,
Email: data.Email,
CreatedAt: data.CreatedAt,
UpdatedAt: data.UpdatedAt,
}
return &fiber.Map{
"status": true,
"data": user,
"error": nil,
}
}
func UsersSuccessResponse(data *[]User) *fiber.Map {
return &fiber.Map{
"status": true,
"data": data,
"error": nil,
}
}
func UserErrorResponse(err error) *fiber.Map {
return &fiber.Map{
"status": false,
"data": "",
"error": err.Error(),
}
}

120
api/presenter/wish-list.go Normal file
View File

@ -0,0 +1,120 @@
package presenter
import (
"wish-list-api/pkg/entities"
"time"
"github.com/gofiber/fiber/v2"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type WishList struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Title string `json:"title"`
UserID string `json:"user_id"`
Description string `json:"description"`
IsPublic bool `json:"is_public"`
PhotoURL string `json:"photo_url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type WishListItem struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Title string `json:"title"`
URL string `json:"url"`
Cost float64 `json:"cost"`
WishListID string `json:"wish_list_id"`
Description string `json:"description"`
PhotoURL string `json:"photo_url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type WishListResponse struct {
Status bool `json:"status"`
Data WishList `json:"data"`
Error *string `json:"error"`
}
type WishListsResponse struct {
Status bool `json:"status"`
Data []WishList `json:"data"`
Error *string `json:"error"`
}
type WishListItemResponse struct {
Status bool `json:"status"`
Data WishListItem `json:"data"`
Error *string `json:"error"`
}
type WishListItemsResponse struct {
Status bool `json:"status"`
Data []WishListItem `json:"data"`
Error *string `json:"error"`
}
func WishListSuccessResponse(data *entities.WishList) *fiber.Map {
id, _ := primitive.ObjectIDFromHex(data.ID)
wishList := WishList{
ID: id,
Title: data.Title,
UserID: data.UserID,
Description: data.Description,
IsPublic: data.IsPublic,
PhotoURL: data.PhotoURL,
CreatedAt: data.CreatedAt,
UpdatedAt: data.UpdatedAt,
}
return &fiber.Map{
"status": true,
"data": wishList,
"error": nil,
}
}
func WishListsSuccessResponse(data *[]WishList) *fiber.Map {
return &fiber.Map{
"status": true,
"data": data,
"error": nil,
}
}
func WishListItemSuccessResponse(data *entities.WishListItem) *fiber.Map {
id, _ := primitive.ObjectIDFromHex(data.ID)
wishListItem := WishListItem{
ID: id,
Title: data.Title,
URL: data.URL,
Cost: data.Cost,
WishListID: data.WishListID,
Description: data.Description,
PhotoURL: data.PhotoURL,
CreatedAt: data.CreatedAt,
UpdatedAt: data.UpdatedAt,
}
return &fiber.Map{
"status": true,
"data": wishListItem,
"error": nil,
}
}
func WishListItemsSuccessResponse(data *[]WishListItem) *fiber.Map {
return &fiber.Map{
"status": true,
"data": data,
"error": nil,
}
}
func WishListErrorResponse(err error) *fiber.Map {
return &fiber.Map{
"status": false,
"data": "",
"error": err.Error(),
}
}

15
api/routes/auth.go Normal file
View File

@ -0,0 +1,15 @@
package routes
import (
"wish-list-api/api/handlers"
"wish-list-api/pkg/auth"
"github.com/gofiber/fiber/v2"
)
func AuthRouter(app fiber.Router, service auth.Service, telegramService auth.TelegramAuthService) {
app.Post("/auth/login", handlers.Login(service))
app.Post("/auth/register", handlers.Register(service))
app.Post("/auth/refresh", handlers.RefreshToken(service))
app.Post("/auth/telegram", handlers.LoginWithTelegram(telegramService))
}

21
api/routes/user.go Normal file
View File

@ -0,0 +1,21 @@
package routes
import (
"wish-list-api/api/handlers"
"wish-list-api/api/middleware"
"wish-list-api/pkg/auth"
"wish-list-api/pkg/user"
"github.com/gofiber/fiber/v2"
)
func UserRouter(app fiber.Router, userService user.Service, authService auth.Service) {
app.Post("/users", handlers.CreateUser(userService))
app.Use("/users", middleware.Protected(authService))
app.Get("/users", handlers.GetAllUsers(userService))
app.Get("/users/:id", handlers.GetUserByID(userService))
app.Get("/users/email/:email", handlers.GetUserByEmail(userService))
app.Put("/users", handlers.UpdateUser(userService))
app.Delete("/users", handlers.DeleteUser(userService))
}

32
api/routes/wish_list.go Normal file
View File

@ -0,0 +1,32 @@
package routes
import (
"wish-list-api/api/handlers"
"wish-list-api/api/middleware"
"wish-list-api/pkg/auth"
wishlist "wish-list-api/pkg/wish-list"
"github.com/gofiber/fiber/v2"
)
func WishListRouter(api fiber.Router, wishListService wishlist.Service, authService auth.Service) {
wishListHandler := handlers.NewWishListHandler(wishListService, authService)
wishList := api.Group("/wishlist")
wishList.Get("/:id", wishListHandler.GetWishList)
wishList.Get("/user/:userId", wishListHandler.GetUserWishLists)
wishList.Get("/:wishlistId/items", wishListHandler.GetWishListItems)
wishList.Get("/item/:id", wishListHandler.GetWishListItem)
wishList.Post("/", middleware.Protected(authService), wishListHandler.CreateWishList)
wishList.Put("/:id", middleware.Protected(authService), wishListHandler.UpdateWishList)
wishList.Delete("/:id", middleware.Protected(authService), wishListHandler.DeleteWishList)
wishList.Post("/item", middleware.Protected(authService), wishListHandler.CreateWishListItem)
wishList.Put("/item/:id", middleware.Protected(authService), wishListHandler.UpdateWishListItem)
wishList.Delete("/item/:id", middleware.Protected(authService), wishListHandler.DeleteWishListItem)
}

109
cmd/main.go Normal file
View File

@ -0,0 +1,109 @@
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"wish-list-api/api/routes"
_ "wish-list-api/docs"
"wish-list-api/pkg/auth"
"wish-list-api/pkg/user"
wishlist "wish-list-api/pkg/wish-list"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/redirect"
swagger "github.com/swaggo/fiber-swagger"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
// @title Wish List API
// @version 1.0
// @description API-сервер для приложения списка желаний
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api
func main() {
db, cancel, err := databaseConnection()
defer cancel()
if err != nil {
log.Fatal("Database Connection Error $s", err)
}
fmt.Println("Database connection success!")
userCollection := db.Collection("users")
userRepo := user.NewRepo(userCollection)
userService := user.NewService(userRepo)
wishListCollection := db.Collection("wish_lists")
wishListItemCollection := db.Collection("wish_list_items")
wishListRepo := wishlist.NewRepo(wishListCollection, wishListItemCollection)
wishListService := wishlist.NewService(wishListRepo)
authService := auth.NewService(auth.ServiceConfig{
UserService: userService,
})
telegramAuthService := auth.NewTelegramAuthService(auth.TelegramAuthConfig{
UserService: userService,
AuthService: authService,
BotToken: os.Getenv("TELEGRAM_BOT_TOKEN"),
TelegramAppUrl: os.Getenv("TELEGRAM_APP_URL"),
})
app := fiber.New()
app.Use(cors.New())
app.Use(logger.New())
app.Use(redirect.New(redirect.Config{
Rules: map[string]string{
"/": "/docs/index.html",
},
StatusCode: 301,
}))
app.Get("/docs/*", swagger.FiberWrapHandler())
api := app.Group("/api")
routes.AuthRouter(api, authService, telegramAuthService)
routes.UserRouter(api, userService, authService)
routes.WishListRouter(api, wishListService, authService)
log.Fatal(app.Listen(":8080"))
}
func databaseConnection() (*mongo.Database, context.CancelFunc, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
mongoURI := os.Getenv("MONGODB_URI")
fmt.Println("MongoDB URI: ", mongoURI)
if mongoURI == "" {
mongoURI = "mongodb://mongo_user:mongo_password@localhost:27017/admin"
}
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI).
SetServerSelectionTimeout(5*time.Second))
if err != nil {
cancel()
return nil, nil, err
}
db := client.Database("books")
return db, cancel, nil
}

View File

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Go Coverage Report</title>
<style>
body {
background: black;
color: rgb(80, 80, 80);
}
body, pre, #legend span {
font-family: Menlo, monospace;
font-weight: bold;
}
#topbar {
background: black;
position: fixed;
top: 0; left: 0; right: 0;
height: 42px;
border-bottom: 1px solid rgb(80, 80, 80);
}
#content {
margin-top: 50px;
}
#nav, #legend {
float: left;
margin-left: 10px;
}
#legend {
margin-top: 12px;
}
#nav {
margin-top: 10px;
}
#legend span {
margin: 0 5px;
}
.cov0 { color: rgb(192, 0, 0) }
.cov1 { color: rgb(128, 128, 128) }
.cov2 { color: rgb(116, 140, 131) }
.cov3 { color: rgb(104, 152, 134) }
.cov4 { color: rgb(92, 164, 137) }
.cov5 { color: rgb(80, 176, 140) }
.cov6 { color: rgb(68, 188, 143) }
.cov7 { color: rgb(56, 200, 146) }
.cov8 { color: rgb(44, 212, 149) }
.cov9 { color: rgb(32, 224, 152) }
.cov10 { color: rgb(20, 236, 155) }
</style>
</head>
<body>
<div id="topbar">
<div id="nav">
<select id="files">
</select>
</div>
<div id="legend">
<span>not tracked</span>
<span class="cov0">no coverage</span>
<span class="cov1">low coverage</span>
<span class="cov2">*</span>
<span class="cov3">*</span>
<span class="cov4">*</span>
<span class="cov5">*</span>
<span class="cov6">*</span>
<span class="cov7">*</span>
<span class="cov8">*</span>
<span class="cov9">*</span>
<span class="cov10">high coverage</span>
</div>
</div>
<div id="content">
</div>
</body>
<script>
(function() {
var files = document.getElementById('files');
var visible;
files.addEventListener('change', onChange, false);
function select(part) {
if (visible)
visible.style.display = 'none';
visible = document.getElementById(part);
if (!visible)
return;
files.value = part;
visible.style.display = 'block';
location.hash = part;
}
function onChange() {
select(files.value);
window.scrollTo(0, 0);
}
if (location.hash != "") {
select(location.hash.substr(1));
}
if (!visible) {
select("file0");
}
})();
</script>
</html>

429
coverage/coverage_unit.html Normal file
View File

@ -0,0 +1,429 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>unit: Go Coverage Report</title>
<style>
body {
background: black;
color: rgb(80, 80, 80);
}
body, pre, #legend span {
font-family: Menlo, monospace;
font-weight: bold;
}
#topbar {
background: black;
position: fixed;
top: 0; left: 0; right: 0;
height: 42px;
border-bottom: 1px solid rgb(80, 80, 80);
}
#content {
margin-top: 50px;
}
#nav, #legend {
float: left;
margin-left: 10px;
}
#legend {
margin-top: 12px;
}
#nav {
margin-top: 10px;
}
#legend span {
margin: 0 5px;
}
.cov0 { color: rgb(192, 0, 0) }
.cov1 { color: rgb(128, 128, 128) }
.cov2 { color: rgb(116, 140, 131) }
.cov3 { color: rgb(104, 152, 134) }
.cov4 { color: rgb(92, 164, 137) }
.cov5 { color: rgb(80, 176, 140) }
.cov6 { color: rgb(68, 188, 143) }
.cov7 { color: rgb(56, 200, 146) }
.cov8 { color: rgb(44, 212, 149) }
.cov9 { color: rgb(32, 224, 152) }
.cov10 { color: rgb(20, 236, 155) }
</style>
</head>
<body>
<div id="topbar">
<div id="nav">
<select id="files">
<option value="file0">wish-list-api/tests/unit/mock_auth.go (22.5%)</option>
<option value="file1">wish-list-api/tests/unit/mock_repository.go (56.5%)</option>
</select>
</div>
<div id="legend">
<span>not tracked</span>
<span class="cov0">not covered</span>
<span class="cov8">covered</span>
</div>
</div>
<div id="content">
<pre class="file" id="file0" style="display: none">package unit
import (
"errors"
"time"
"wish-list-api/pkg/auth"
"wish-list-api/pkg/entities"
"github.com/golang-jwt/jwt/v5"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// MockAuthService реализует интерфейс auth.Service
type MockAuthService struct {
users map[primitive.ObjectID]*entities.User
// Токен -&gt; ID пользователя
tokens map[string]primitive.ObjectID
}
// NewMockAuthService создает новый mock сервис аутентификации
func NewMockAuthService() auth.Service <span class="cov8" title="1">{
return &amp;MockAuthService{
users: make(map[primitive.ObjectID]*entities.User),
tokens: make(map[string]primitive.ObjectID),
}
}</span>
// AddMockUser добавляет мок-пользователя в сервис
func (m *MockAuthService) AddMockUser(user *entities.User, token string) <span class="cov8" title="1">{
m.users[user.ID] = user
m.tokens[token] = user.ID
}</span>
// Login реализация для теста
func (m *MockAuthService) Login(credentials *entities.LoginRequest) (*entities.TokenPair, error) <span class="cov0" title="0">{
for _, user := range m.users </span><span class="cov0" title="0">{
if user.Email == credentials.Email &amp;&amp; m.ComparePasswords(user.Password, credentials.Password) </span><span class="cov0" title="0">{
token := "mock-token-for-" + user.ID.Hex()
m.tokens[token] = user.ID
return &amp;entities.TokenPair{
AccessToken: token,
RefreshToken: "refresh-" + token,
}, nil
}</span>
}
<span class="cov0" title="0">return nil, errors.New("invalid credentials")</span>
}
// Register реализация для теста
func (m *MockAuthService) Register(userData *entities.RegisterRequest) (*entities.User, error) <span class="cov0" title="0">{
// Проверяем, что email не занят
for _, existingUser := range m.users </span><span class="cov0" title="0">{
if existingUser.Email == userData.Email </span><span class="cov0" title="0">{
return nil, errors.New("email already exists")
}</span>
}
// Создаем нового пользователя
<span class="cov0" title="0">hashedPassword, _ := m.HashPassword(userData.Password)
user := &amp;entities.User{
ID: primitive.NewObjectID(),
Email: userData.Email,
Password: hashedPassword,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Сохраняем пользователя
m.users[user.ID] = user
return user, nil</span>
}
// RefreshToken реализация для теста
func (m *MockAuthService) RefreshToken(refreshToken string) (*entities.TokenPair, error) <span class="cov0" title="0">{
// Предполагаем, что refresh token имеет вид "refresh-&lt;access-token&gt;"
if len(refreshToken) &lt; 8 </span><span class="cov0" title="0">{
return nil, errors.New("invalid refresh token")
}</span>
<span class="cov0" title="0">accessToken := refreshToken[8:] // Получаем access token из refresh token
userID, ok := m.tokens[accessToken]
if !ok </span><span class="cov0" title="0">{
return nil, errors.New("invalid refresh token")
}</span>
<span class="cov0" title="0">newToken := "mock-token-for-" + userID.Hex() + "-refreshed"
m.tokens[newToken] = userID
return &amp;entities.TokenPair{
AccessToken: newToken,
RefreshToken: "refresh-" + newToken,
}, nil</span>
}
// ValidateToken реализация для теста
func (m *MockAuthService) ValidateToken(tokenString string) (*jwt.Token, error) <span class="cov0" title="0">{
// Проверяем, имеет ли токен префикс "Bearer "
if len(tokenString) &gt; 7 &amp;&amp; tokenString[:7] == "Bearer " </span><span class="cov0" title="0">{
tokenString = tokenString[7:]
}</span>
<span class="cov0" title="0">userID, ok := m.tokens[tokenString]
if !ok </span><span class="cov0" title="0">{
return nil, errors.New("invalid token")
}</span>
// Создаем mock JWT token
<span class="cov0" title="0">token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userID.Hex(),
"exp": time.Now().Add(time.Hour).Unix(),
})
return token, nil</span>
}
// GetUserIDFromToken реализация для теста
func (m *MockAuthService) GetUserIDFromToken(tokenString string) (string, error) <span class="cov8" title="1">{
// Проверяем, имеет ли токен префикс "Bearer "
if len(tokenString) &gt; 7 &amp;&amp; tokenString[:7] == "Bearer " </span><span class="cov8" title="1">{
tokenString = tokenString[7:]
}</span>
<span class="cov8" title="1">userID, ok := m.tokens[tokenString]
if !ok </span><span class="cov8" title="1">{
return "", errors.New("invalid token")
}</span>
<span class="cov8" title="1">return userID.Hex(), nil</span>
}
// HashPassword реализация для теста
func (m *MockAuthService) HashPassword(password string) (string, error) <span class="cov0" title="0">{
// Просто возвращаем пароль с префиксом для имитации хеширования
return "hashed_" + password, nil
}</span>
// ComparePasswords реализация для теста
func (m *MockAuthService) ComparePasswords(hashedPassword string, plainPassword string) bool <span class="cov0" title="0">{
// Проверяем, соответствует ли "хешированный" пароль нашей простой схеме
return hashedPassword == "hashed_"+plainPassword
}</span>
</pre>
<pre class="file" id="file1" style="display: none">package unit
import (
"errors"
"time"
"wish-list-api/api/presenter"
"wish-list-api/pkg/entities"
wishlist "wish-list-api/pkg/wish-list"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// MockWishListRepository реализует интерфейс Repository для тестирования
type MockWishListRepository struct {
wishLists map[string]*entities.WishList
wishListItems map[string]*entities.WishListItem
}
// NewMockWishListRepository создает новый экземпляр мок-репозитория
func NewMockWishListRepository() wishlist.Repository <span class="cov8" title="1">{
return &amp;MockWishListRepository{
wishLists: make(map[string]*entities.WishList),
wishListItems: make(map[string]*entities.WishListItem),
}
}</span>
// CreateWishList реализует создание списка желаний
func (r *MockWishListRepository) CreateWishList(wishList *entities.WishList) (*entities.WishList, error) <span class="cov8" title="1">{
if wishList.ID == "" </span><span class="cov8" title="1">{
wishList.ID = "mock-id-" + time.Now().String()
}</span>
<span class="cov8" title="1">wishList.CreatedAt = time.Now()
wishList.UpdatedAt = time.Now()
r.wishLists[wishList.ID] = wishList
return wishList, nil</span>
}
// ReadWishList реализует чтение списка желаний по ID
func (r *MockWishListRepository) ReadWishList(ID string) (*entities.WishList, error) <span class="cov8" title="1">{
if wishList, ok := r.wishLists[ID]; ok </span><span class="cov8" title="1">{
return wishList, nil
}</span>
<span class="cov8" title="1">return nil, errors.New("wishlist not found")</span>
}
// ReadAllWishLists реализует чтение всех списков желаний пользователя
func (r *MockWishListRepository) ReadAllWishLists(userID string) (*[]presenter.WishList, error) <span class="cov0" title="0">{
var result []presenter.WishList
for _, wl := range r.wishLists </span><span class="cov0" title="0">{
if wl.UserID == userID </span><span class="cov0" title="0">{
objID, _ := primitive.ObjectIDFromHex(wl.ID)
result = append(result, presenter.WishList{
ID: objID,
Title: wl.Title,
UserID: wl.UserID,
Description: wl.Description,
IsPublic: wl.IsPublic,
PhotoURL: wl.PhotoURL,
CreatedAt: wl.CreatedAt,
UpdatedAt: wl.UpdatedAt,
})
}</span>
}
<span class="cov0" title="0">return &amp;result, nil</span>
}
// ReadPublicWishLists реализует чтение всех публичных списков желаний
func (r *MockWishListRepository) ReadPublicWishLists() (*[]presenter.WishList, error) <span class="cov0" title="0">{
var result []presenter.WishList
for _, wl := range r.wishLists </span><span class="cov0" title="0">{
if wl.IsPublic </span><span class="cov0" title="0">{
objID, _ := primitive.ObjectIDFromHex(wl.ID)
result = append(result, presenter.WishList{
ID: objID,
Title: wl.Title,
UserID: wl.UserID,
Description: wl.Description,
IsPublic: wl.IsPublic,
PhotoURL: wl.PhotoURL,
CreatedAt: wl.CreatedAt,
UpdatedAt: wl.UpdatedAt,
})
}</span>
}
<span class="cov0" title="0">return &amp;result, nil</span>
}
// UpdateWishList реализует обновление списка желаний
func (r *MockWishListRepository) UpdateWishList(wishList *entities.WishList) (*entities.WishList, error) <span class="cov8" title="1">{
if _, ok := r.wishLists[wishList.ID]; !ok </span><span class="cov8" title="1">{
return nil, errors.New("wishlist not found")
}</span>
<span class="cov8" title="1">wishList.UpdatedAt = time.Now()
r.wishLists[wishList.ID] = wishList
return wishList, nil</span>
}
// DeleteWishList реализует удаление списка желаний
func (r *MockWishListRepository) DeleteWishList(ID string) error <span class="cov8" title="1">{
if _, ok := r.wishLists[ID]; !ok </span><span class="cov8" title="1">{
return errors.New("wishlist not found")
}</span>
<span class="cov8" title="1">delete(r.wishLists, ID)
// Удаляем все элементы, связанные с этим списком
for id, item := range r.wishListItems </span><span class="cov0" title="0">{
if item.WishListID == ID </span><span class="cov0" title="0">{
delete(r.wishListItems, id)
}</span>
}
<span class="cov8" title="1">return nil</span>
}
// CreateWishListItem реализует создание элемента списка желаний
func (r *MockWishListRepository) CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) <span class="cov8" title="1">{
if _, ok := r.wishLists[item.WishListID]; !ok </span><span class="cov8" title="1">{
return nil, errors.New("wishlist not found")
}</span>
<span class="cov8" title="1">if item.ID == "" </span><span class="cov8" title="1">{
item.ID = "mock-item-id-" + time.Now().String()
}</span>
<span class="cov8" title="1">item.CreatedAt = time.Now()
item.UpdatedAt = time.Now()
r.wishListItems[item.ID] = item
return item, nil</span>
}
// ReadWishListItem реализует чтение элемента списка желаний по ID
func (r *MockWishListRepository) ReadWishListItem(ID string) (*entities.WishListItem, error) <span class="cov0" title="0">{
if item, ok := r.wishListItems[ID]; ok </span><span class="cov0" title="0">{
return item, nil
}</span>
<span class="cov0" title="0">return nil, errors.New("wishlist item not found")</span>
}
// ReadAllWishListItems реализует чтение всех элементов списка желаний
func (r *MockWishListRepository) ReadAllWishListItems(wishListID string) (*[]presenter.WishListItem, error) <span class="cov8" title="1">{
if _, ok := r.wishLists[wishListID]; !ok </span><span class="cov0" title="0">{
return nil, errors.New("wishlist not found")
}</span>
<span class="cov8" title="1">var result []presenter.WishListItem
for _, item := range r.wishListItems </span><span class="cov8" title="1">{
if item.WishListID == wishListID </span><span class="cov8" title="1">{
objID, _ := primitive.ObjectIDFromHex(item.ID)
result = append(result, presenter.WishListItem{
ID: objID,
Title: item.Title,
URL: item.URL,
Cost: item.Cost,
WishListID: item.WishListID,
Description: item.Description,
PhotoURL: item.PhotoURL,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
})
}</span>
}
<span class="cov8" title="1">return &amp;result, nil</span>
}
// UpdateWishListItem реализует обновление элемента списка желаний
func (r *MockWishListRepository) UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) <span class="cov0" title="0">{
if _, ok := r.wishListItems[item.ID]; !ok </span><span class="cov0" title="0">{
return nil, errors.New("wishlist item not found")
}</span>
<span class="cov0" title="0">item.UpdatedAt = time.Now()
r.wishListItems[item.ID] = item
return item, nil</span>
}
// DeleteWishListItem реализует удаление элемента списка желаний
func (r *MockWishListRepository) DeleteWishListItem(ID string) error <span class="cov0" title="0">{
if _, ok := r.wishListItems[ID]; !ok </span><span class="cov0" title="0">{
return errors.New("wishlist item not found")
}</span>
<span class="cov0" title="0">delete(r.wishListItems, ID)
return nil</span>
}
</pre>
</div>
</body>
<script>
(function() {
var files = document.getElementById('files');
var visible;
files.addEventListener('change', onChange, false);
function select(part) {
if (visible)
visible.style.display = 'none';
visible = document.getElementById(part);
if (!visible)
return;
files.value = part;
visible.style.display = 'block';
location.hash = part;
}
function onChange() {
select(files.value);
window.scrollTo(0, 0);
}
if (location.hash != "") {
select(location.hash.substr(1));
}
if (!visible) {
select("file0");
}
})();
</script>
</html>

62
docker-compose.yaml Normal file
View File

@ -0,0 +1,62 @@
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: wish-list-api
ports:
- "8080:8080"
environment:
- MONGODB_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/${MONGO_DATABASE}
env_file:
- .env
depends_on:
- mongo
restart: unless-stopped
networks:
- app-network
tests:
build:
context: .
dockerfile: Dockerfile
container_name: wish-list-api-tests
environment:
- MONGODB_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/${MONGO_DATABASE}
- RUN_INTEGRATION_TESTS=true
env_file:
- .env
depends_on:
- mongo
volumes:
- ./coverage:/app/coverage
command: sh -c "chmod +x /app/scripts/run_tests.sh && /app/scripts/run_tests.sh"
networks:
- app-network
mongo:
image: mongo:latest
container_name: mongodb
restart: unless-stopped
environment:
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
- MONGO_INITDB_DATABASE=${MONGO_DATABASE}
env_file:
- .env
ports:
- "27017:27017"
volumes:
- mongo_data:/data/db
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
mongo_data:
driver: local

1478
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

1410
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

909
docs/swagger.yaml Normal file
View File

@ -0,0 +1,909 @@
basePath: /api
definitions:
entities.DeleteUserRequest:
properties:
id:
type: string
type: object
entities.LoginRequest:
properties:
email:
type: string
password:
type: string
type: object
entities.RegisterRequest:
properties:
email:
type: string
password:
type: string
type: object
entities.TelegramAuthRequest:
properties:
access_token:
type: string
auth_date:
type: integer
first_name:
type: string
hash:
type: string
last_name:
type: string
photo_url:
type: string
refresh_token:
type: string
telegram_id:
type: integer
username:
type: string
type: object
entities.TokenPair:
properties:
access_token:
type: string
refresh_token:
type: string
type: object
entities.TokenRequest:
properties:
refresh_token:
type: string
type: object
entities.User:
properties:
created_at:
type: string
email:
type: string
first_name:
type: string
id:
type: string
last_login_date:
type: string
last_name:
type: string
password:
type: string
photo_url:
type: string
telegram_id:
type: integer
telegram_username:
type: string
updated_at:
type: string
type: object
entities.WishList:
properties:
created_at:
type: string
description:
type: string
id:
type: string
is_public:
type: boolean
photo_url:
type: string
title:
type: string
updated_at:
type: string
user_id:
type: string
type: object
entities.WishListItem:
properties:
cost:
type: number
created_at:
type: string
description:
type: string
id:
type: string
photo_url:
type: string
title:
type: string
updated_at:
type: string
url:
type: string
wish_list_id:
type: string
type: object
presenter.AuthResponse:
properties:
data:
$ref: '#/definitions/entities.TokenPair'
error:
type: string
status:
type: boolean
user:
$ref: '#/definitions/presenter.User'
type: object
presenter.User:
properties:
created_at:
type: string
email:
type: string
id:
type: string
updated_at:
type: string
type: object
presenter.UserResponse:
properties:
data:
$ref: '#/definitions/presenter.User'
error:
type: string
status:
type: boolean
type: object
presenter.UsersResponse:
properties:
data:
items:
$ref: '#/definitions/presenter.User'
type: array
error:
type: string
status:
type: boolean
type: object
presenter.WishList:
properties:
created_at:
type: string
description:
type: string
id:
type: string
is_public:
type: boolean
photo_url:
type: string
title:
type: string
updated_at:
type: string
user_id:
type: string
type: object
presenter.WishListItem:
properties:
cost:
type: number
created_at:
type: string
description:
type: string
id:
type: string
photo_url:
type: string
title:
type: string
updated_at:
type: string
url:
type: string
wish_list_id:
type: string
type: object
presenter.WishListItemResponse:
properties:
data:
$ref: '#/definitions/presenter.WishListItem'
error:
type: string
status:
type: boolean
type: object
presenter.WishListItemsResponse:
properties:
data:
items:
$ref: '#/definitions/presenter.WishListItem'
type: array
error:
type: string
status:
type: boolean
type: object
presenter.WishListResponse:
properties:
data:
$ref: '#/definitions/presenter.WishList'
error:
type: string
status:
type: boolean
type: object
presenter.WishListsResponse:
properties:
data:
items:
$ref: '#/definitions/presenter.WishList'
type: array
error:
type: string
status:
type: boolean
type: object
host: localhost:8080
info:
contact: {}
description: API-сервер для приложения списка желаний
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
title: Wish List API
version: "1.0"
paths:
/auth/login:
post:
consumes:
- application/json
description: Аутентифицирует пользователя и выдает JWT токены
parameters:
- description: Учетные данные пользователя
in: body
name: credentials
required: true
schema:
$ref: '#/definitions/entities.LoginRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.AuthResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.AuthResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/presenter.AuthResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/presenter.AuthResponse'
summary: Вход пользователя
tags:
- auth
/auth/refresh:
post:
consumes:
- application/json
description: Обновляет JWT токены с помощью refresh токена
parameters:
- description: Refresh токен
in: body
name: refreshToken
required: true
schema:
$ref: '#/definitions/entities.TokenRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.AuthResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.AuthResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/presenter.AuthResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/presenter.AuthResponse'
summary: Обновление токенов
tags:
- auth
/auth/register:
post:
consumes:
- application/json
description: Регистрирует нового пользователя и выдает JWT токены
parameters:
- description: Данные нового пользователя
in: body
name: user
required: true
schema:
$ref: '#/definitions/entities.RegisterRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.AuthResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.AuthResponse'
"409":
description: Conflict
schema:
$ref: '#/definitions/presenter.AuthResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/presenter.AuthResponse'
summary: Регистрация пользователя
tags:
- auth
/auth/telegram:
post:
consumes:
- application/json
description: Аутентифицирует пользователя через Telegram и выдает JWT токены
parameters:
- description: Данные аутентификации Telegram
in: body
name: credentials
required: true
schema:
$ref: '#/definitions/entities.TelegramAuthRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.AuthResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.AuthResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/presenter.AuthResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/presenter.AuthResponse'
summary: Вход пользователя через Telegram
tags:
- auth
/users:
delete:
consumes:
- application/json
description: Удаляет пользователя из системы по ID
parameters:
- description: ID пользователя для удаления
in: body
name: request
required: true
schema:
$ref: '#/definitions/entities.DeleteUserRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.UserResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.UserResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/presenter.UserResponse'
security:
- BearerAuth: []
summary: Удалить пользователя
tags:
- users
get:
consumes:
- application/json
description: Возвращает список всех пользователей в системе
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.UsersResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/presenter.UserResponse'
security:
- BearerAuth: []
summary: Получить всех пользователей
tags:
- users
post:
consumes:
- application/json
description: Создает нового пользователя в системе
parameters:
- description: Информация о пользователе
in: body
name: user
required: true
schema:
$ref: '#/definitions/entities.User'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.UserResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.UserResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/presenter.UserResponse'
summary: Добавить нового пользователя
tags:
- users
put:
consumes:
- application/json
description: Обновляет информацию о существующем пользователе
parameters:
- description: Информация о пользователе для обновления
in: body
name: user
required: true
schema:
$ref: '#/definitions/entities.User'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.UserResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.UserResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/presenter.UserResponse'
security:
- BearerAuth: []
summary: Обновить пользователя
tags:
- users
/users/{id}:
get:
consumes:
- application/json
description: Возвращает информацию о пользователе по его ID
parameters:
- description: ID пользователя
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.UserResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.UserResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/presenter.UserResponse'
security:
- BearerAuth: []
summary: Получить пользователя по ID
tags:
- users
/users/email/{email}:
get:
consumes:
- application/json
description: Возвращает информацию о пользователе по его Email
parameters:
- description: Email пользователя
in: path
name: email
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.UserResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.UserResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/presenter.UserResponse'
security:
- BearerAuth: []
summary: Получить пользователя по Email
tags:
- users
/wishlist:
post:
consumes:
- application/json
description: Create a new wishlist for the authenticated user
parameters:
- description: Bearer token
in: header
name: Authorization
required: true
type: string
- description: Wishlist data
in: body
name: wishlist
required: true
schema:
$ref: '#/definitions/entities.WishList'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/presenter.WishListResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.WishListResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/presenter.WishListResponse'
security:
- BearerAuth: []
summary: Create a new wishlist
tags:
- wishlist
/wishlist/{id}:
delete:
consumes:
- application/json
description: Delete a wishlist and all its items
parameters:
- description: Bearer token
in: header
name: Authorization
required: true
type: string
- description: Wishlist ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.WishListResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/presenter.WishListResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/presenter.WishListResponse'
security:
- BearerAuth: []
summary: Delete a wishlist
tags:
- wishlist
get:
consumes:
- application/json
description: Get a wishlist by its ID
parameters:
- description: Wishlist ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.WishListResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/presenter.WishListResponse'
security:
- BearerAuth: []
summary: Get a wishlist
tags:
- wishlist
put:
consumes:
- application/json
description: Update an existing wishlist
parameters:
- description: Bearer token
in: header
name: Authorization
required: true
type: string
- description: Wishlist ID
in: path
name: id
required: true
type: string
- description: Updated wishlist data
in: body
name: wishlist
required: true
schema:
$ref: '#/definitions/entities.WishList'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.WishListResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.WishListResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/presenter.WishListResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/presenter.WishListResponse'
security:
- BearerAuth: []
summary: Update a wishlist
tags:
- wishlist
/wishlist/{wishlistId}/items:
get:
consumes:
- application/json
description: Get all items in a wishlist
parameters:
- description: Wishlist ID
in: path
name: wishlistId
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.WishListItemsResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/presenter.WishListItemsResponse'
security:
- BearerAuth: []
summary: Get wishlist items
tags:
- wishlist-items
/wishlist/item:
post:
consumes:
- application/json
description: Create a new item for a wishlist
parameters:
- description: Bearer token
in: header
name: Authorization
required: true
type: string
- description: Wishlist item data
in: body
name: item
required: true
schema:
$ref: '#/definitions/entities.WishListItem'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
security:
- BearerAuth: []
summary: Create a wishlist item
tags:
- wishlist-items
/wishlist/item/{id}:
delete:
consumes:
- application/json
description: Delete a wishlist item
parameters:
- description: Bearer token
in: header
name: Authorization
required: true
type: string
- description: Item ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
security:
- BearerAuth: []
summary: Delete a wishlist item
tags:
- wishlist-items
get:
consumes:
- application/json
description: Get a wishlist item by its ID
parameters:
- description: Item ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
security:
- BearerAuth: []
summary: Get a wishlist item
tags:
- wishlist-items
put:
consumes:
- application/json
description: Update an existing wishlist item
parameters:
- description: Bearer token
in: header
name: Authorization
required: true
type: string
- description: Item ID
in: path
name: id
required: true
type: string
- description: Updated item data
in: body
name: item
required: true
schema:
$ref: '#/definitions/entities.WishListItem'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/presenter.WishListItemResponse'
security:
- BearerAuth: []
summary: Update a wishlist item
tags:
- wishlist-items
/wishlist/user/{userId}:
get:
consumes:
- application/json
description: Get all wishlists for a specific user
parameters:
- description: User ID
in: path
name: userId
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/presenter.WishListsResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/presenter.WishListResponse'
security:
- BearerAuth: []
summary: Get user wishlists
tags:
- wishlist
securityDefinitions:
BearerAuth:
in: header
name: Authorization
type: apiKey
swagger: "2.0"

54
go.mod Normal file
View File

@ -0,0 +1,54 @@
module wish-list-api
go 1.23.0
toolchain go1.23.7
require (
github.com/gofiber/fiber/v2 v2.52.5
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.7.0
github.com/swaggo/fiber-swagger v1.1.0
github.com/swaggo/swag v1.7.8
go.mongodb.org/mongo-driver v1.16.1
golang.org/x/crypto v0.36.0
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
)

185
go.sum Normal file
View File

@ -0,0 +1,185 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs=
github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY=
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/gofiber/fiber/v2 v2.24.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE=
github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/swaggo/fiber-swagger v1.1.0 h1:3yeJeStLYtUvj9zZg0/COwEESIxPBKM+OohOxB0tJ68=
github.com/swaggo/fiber-swagger v1.1.0/go.mod h1:DaSh3M3So+l/lrdpnAHllOOmTsKZT4flCYjz+jbUIfo=
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM=
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/swag v1.7.8 h1:w249t0l/kc/DKMGlS0fppNJQxKyJ8heNaUWB6nsH3zc=
github.com/swaggo/swag v1.7.8/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8=
go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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/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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

237
pkg/auth/service.go Normal file
View File

@ -0,0 +1,237 @@
package auth
import (
"errors"
"os"
"strconv"
"time"
"wish-list-api/pkg/entities"
"wish-list-api/pkg/user"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
type Service interface {
Login(credentials *entities.LoginRequest) (*entities.TokenPair, error)
Register(userData *entities.RegisterRequest) (*entities.User, error)
RefreshToken(refreshToken string) (*entities.TokenPair, error)
ValidateToken(tokenString string) (*jwt.Token, error)
GetUserIDFromToken(tokenString string) (string, error)
HashPassword(password string) (string, error)
ComparePasswords(hashedPassword string, plainPassword string) bool
}
const (
DefaultAccessTokenExpiry = 30 * 60
DefaultRefreshTokenExpiry = 7 * 24 * 3600
DefaultSecretKey = "94cb4ff3-2396-4903-9266-9cdd9b885767"
)
type service struct {
userService user.Service
accessTokenExp int64
refreshTokenExp int64
accessTokenSecret string
refreshTokenSecret string
}
type ServiceConfig struct {
UserService user.Service
AccessTokenExp int64
RefreshTokenExp int64
AccessTokenSecret string
RefreshTokenSecret string
}
func NewService(config ServiceConfig) Service {
if config.AccessTokenExp == 0 {
if envExp := os.Getenv("ACCESS_TOKEN_EXPIRY"); envExp != "" {
if exp, err := strconv.ParseInt(envExp, 10, 64); err == nil {
config.AccessTokenExp = exp
} else {
config.AccessTokenExp = DefaultAccessTokenExpiry
}
} else {
config.AccessTokenExp = DefaultAccessTokenExpiry
}
}
if config.RefreshTokenExp == 0 {
if envExp := os.Getenv("REFRESH_TOKEN_EXPIRY"); envExp != "" {
if exp, err := strconv.ParseInt(envExp, 10, 64); err == nil {
config.RefreshTokenExp = exp
} else {
config.RefreshTokenExp = DefaultRefreshTokenExpiry
}
} else {
config.RefreshTokenExp = DefaultRefreshTokenExpiry
}
}
if config.AccessTokenSecret == "" {
if secret := os.Getenv("ACCESS_TOKEN_SECRET"); secret != "" {
config.AccessTokenSecret = secret
} else {
config.AccessTokenSecret = DefaultSecretKey
}
}
if config.RefreshTokenSecret == "" {
if secret := os.Getenv("REFRESH_TOKEN_SECRET"); secret != "" {
config.RefreshTokenSecret = secret
} else {
config.RefreshTokenSecret = DefaultSecretKey + "_refresh"
}
}
return &service{
userService: config.UserService,
accessTokenExp: config.AccessTokenExp,
refreshTokenExp: config.RefreshTokenExp,
accessTokenSecret: config.AccessTokenSecret,
refreshTokenSecret: config.RefreshTokenSecret,
}
}
func (s *service) Login(credentials *entities.LoginRequest) (*entities.TokenPair, error) {
user, err := s.userService.GetUserByEmail(credentials.Email)
if err != nil {
return nil, errors.New("неверный email или пароль")
}
if !s.ComparePasswords(user.Password, credentials.Password) {
return nil, errors.New("неверный email или пароль")
}
return s.generateTokenPair(user.ID.Hex(), user.Email)
}
func (s *service) Register(userData *entities.RegisterRequest) (*entities.User, error) {
existingUser, _ := s.userService.GetUserByEmail(userData.Email)
if existingUser != nil {
return nil, errors.New("пользователь с таким email уже существует")
}
hashedPassword, err := s.HashPassword(userData.Password)
if err != nil {
return nil, err
}
newUser := &entities.User{
Email: userData.Email,
Password: hashedPassword,
}
return s.userService.CreateUser(newUser)
}
func (s *service) RefreshToken(refreshToken string) (*entities.TokenPair, error) {
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("неожиданный метод подписи")
}
return []byte(s.refreshTokenSecret), nil
})
if err != nil {
return nil, errors.New("недействительный refresh token")
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
if exp, ok := claims["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return nil, errors.New("refresh token истек")
}
}
userID, ok := claims["user_id"].(string)
if !ok {
return nil, errors.New("недействительный refresh token")
}
user, err := s.userService.GetUser(userID)
if err != nil {
return nil, errors.New("пользователь не найден")
}
return s.generateTokenPair(user.ID.Hex(), user.Email)
}
return nil, errors.New("недействительный refresh token")
}
func (s *service) ValidateToken(tokenString string) (*jwt.Token, error) {
return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("неожиданный метод подписи")
}
return []byte(s.accessTokenSecret), nil
})
}
func (s *service) GetUserIDFromToken(tokenString string) (string, error) {
if len(tokenString) > 7 && tokenString[:7] == "Bearer " {
tokenString = tokenString[7:]
}
token, err := s.ValidateToken(tokenString)
if err != nil {
return "", err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return "", errors.New("invalid token claims")
}
userID, ok := claims["user_id"].(string)
if !ok {
return "", errors.New("user_id not found in token")
}
return userID, nil
}
func (s *service) generateTokenPair(userID, email string) (*entities.TokenPair, error) {
now := time.Now().Unix()
accessClaims := jwt.MapClaims{
"user_id": userID,
"email": email,
"exp": now + s.accessTokenExp,
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessTokenString, err := accessToken.SignedString([]byte(s.accessTokenSecret))
if err != nil {
return nil, err
}
refreshClaims := jwt.MapClaims{
"user_id": userID,
"exp": now + s.refreshTokenExp,
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refreshTokenString, err := refreshToken.SignedString([]byte(s.refreshTokenSecret))
if err != nil {
return nil, err
}
return &entities.TokenPair{
AccessToken: accessTokenString,
RefreshToken: refreshTokenString,
}, nil
}
func (s *service) HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func (s *service) ComparePasswords(hashedPassword string, plainPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword))
return err == nil
}

211
pkg/auth/telegram.go Normal file
View File

@ -0,0 +1,211 @@
package auth
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"sort"
"strconv"
"strings"
"time"
"wish-list-api/pkg/entities"
"wish-list-api/pkg/user"
)
const MaxAuthAge = 86400
type TelegramAuthService interface {
AuthenticateWithTelegram(data *entities.TelegramAuthRequest) (*entities.TokenPair, error)
ValidateTelegramAuthData(data *entities.TelegramAuthRequest) error
}
type telegramAuthService struct {
userService user.Service
authService Service
botToken string
telegramAppUrl string
}
type TelegramAuthConfig struct {
UserService user.Service
AuthService Service
BotToken string
TelegramAppUrl string
}
func NewTelegramAuthService(config TelegramAuthConfig) TelegramAuthService {
if config.BotToken == "" {
config.BotToken = os.Getenv("TELEGRAM_BOT_TOKEN")
}
if config.TelegramAppUrl == "" {
config.TelegramAppUrl = os.Getenv("TELEGRAM_APP_URL")
}
return &telegramAuthService{
userService: config.UserService,
authService: config.AuthService,
botToken: config.BotToken,
telegramAppUrl: config.TelegramAppUrl,
}
}
func (s *telegramAuthService) AuthenticateWithTelegram(data *entities.TelegramAuthRequest) (*entities.TokenPair, error) {
err := s.ValidateTelegramAuthData(data)
if err != nil {
return nil, err
}
user, err := s.userService.GetUserByTelegramID(data.TelegramID)
if err != nil {
return nil, err
}
if user == nil {
randomPassword, err := generateRandomPassword(16)
if err != nil {
return nil, err
}
email := fmt.Sprintf("telegram_%d@%s", data.TelegramID, s.telegramAppUrl)
user = &entities.User{
Email: email,
Password: randomPassword,
TelegramID: data.TelegramID,
TelegramUsername: data.Username,
FirstName: data.FirstName,
LastName: data.LastName,
PhotoURL: data.PhotoURL,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
LastLoginDate: time.Now(),
}
hashedPassword, err := s.authService.HashPassword(randomPassword)
if err != nil {
return nil, err
}
user.Password = hashedPassword
user, err = s.userService.CreateUser(user)
if err != nil {
return nil, err
}
} else {
updated := false
if user.TelegramUsername != data.Username {
user.TelegramUsername = data.Username
updated = true
}
if user.FirstName != data.FirstName {
user.FirstName = data.FirstName
updated = true
}
if user.LastName != data.LastName {
user.LastName = data.LastName
updated = true
}
if user.PhotoURL != data.PhotoURL && data.PhotoURL != "" {
user.PhotoURL = data.PhotoURL
updated = true
}
user.LastLoginDate = time.Now()
updated = true
if updated {
_, err = s.userService.UpdateUserTelegramData(user)
if err != nil {
return nil, err
}
}
}
loginRequest := &entities.LoginRequest{
Email: user.Email,
}
return s.authService.Login(loginRequest)
}
func (s *telegramAuthService) ValidateTelegramAuthData(data *entities.TelegramAuthRequest) error {
now := time.Now().Unix()
if now-data.AuthDate > MaxAuthAge {
return errors.New("данные аутентификации Telegram устарели")
}
secretKey := generateSecretKey(s.botToken)
dataCheckString := buildDataCheckString(data)
hash := generateHash(dataCheckString, secretKey)
if hash != data.Hash {
return errors.New("неверная подпись данных аутентификации Telegram")
}
return nil
}
func generateSecretKey(botToken string) []byte {
h := sha256.New()
h.Write([]byte(botToken))
return h.Sum(nil)
}
func buildDataCheckString(data *entities.TelegramAuthRequest) string {
params := map[string]string{
"auth_date": strconv.FormatInt(data.AuthDate, 10),
"first_name": data.FirstName,
"telegram_id": strconv.FormatInt(data.TelegramID, 10),
}
if data.LastName != "" {
params["last_name"] = data.LastName
}
if data.Username != "" {
params["username"] = data.Username
}
if data.PhotoURL != "" {
params["photo_url"] = data.PhotoURL
}
var keys []string
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var pairs []string
for _, k := range keys {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, params[k]))
}
return strings.Join(pairs, "\n")
}
func generateHash(data string, secretKey []byte) string {
h := hmac.New(sha256.New, secretKey)
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
func generateRandomPassword(length int) (string, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return hex.EncodeToString(bytes)[:length], nil
}

43
pkg/entities/auth.go Normal file
View File

@ -0,0 +1,43 @@
package entities
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
type RegisterRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type AccessTokenClaims struct {
UserID string `json:"user_id"`
Email string `json:"email"`
Exp int64 `json:"exp"`
}
type RefreshTokenClaims struct {
UserID string `json:"user_id"`
Exp int64 `json:"exp"`
}
type TokenRequest struct {
RefreshToken string `json:"refresh_token"`
}
type TelegramAuthRequest struct {
TelegramID int64 `json:"telegram_id"`
Username string `json:"username"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AuthDate int64 `json:"auth_date"`
Hash string `json:"hash"`
PhotoURL string `json:"photo_url,omitempty"`
AccessToken string `json:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
}

25
pkg/entities/user.go Normal file
View File

@ -0,0 +1,25 @@
package entities
import (
"time"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type User struct {
ID primitive.ObjectID `json:"id" bson:"_id,omitempty"`
Email string `json:"email" bson:"email"`
Password string `json:"password" bson:"password"`
TelegramID int64 `json:"telegram_id,omitempty" bson:"telegram_id,omitempty"`
TelegramUsername string `json:"telegram_username,omitempty" bson:"telegram_username,omitempty"`
FirstName string `json:"first_name,omitempty" bson:"first_name,omitempty"`
LastName string `json:"last_name,omitempty" bson:"last_name,omitempty"`
PhotoURL string `json:"photo_url,omitempty" bson:"photo_url,omitempty"`
LastLoginDate time.Time `json:"last_login_date,omitempty" bson:"last_login_date,omitempty"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
type DeleteUserRequest struct {
ID string `json:"id"`
}

36
pkg/entities/wish-list.go Normal file
View File

@ -0,0 +1,36 @@
package entities
import (
"time"
)
type WishList struct {
ID string `json:"id" bson:"_id,omitempty"`
Title string `json:"title" bson:"title"`
UserID string `json:"user_id" bson:"user_id"`
Description string `json:"description" bson:"description"`
IsPublic bool `json:"is_public" bson:"is_public"`
PhotoURL string `json:"photo_url" bson:"photo_url"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
type WishListItem struct {
ID string `json:"id" bson:"_id,omitempty"`
Title string `json:"title" bson:"title"`
URL string `json:"url" bson:"url"`
Cost float64 `json:"cost" bson:"cost"`
WishListID string `json:"wish_list_id" bson:"wish_list_id"`
Description string `json:"description" bson:"description"`
PhotoURL string `json:"photo_url" bson:"photo_url"`
CreatedAt time.Time `json:"created_at" bson:"created_at"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
}
type DeleteWishListRequest struct {
ID string `json:"id" bson:"_id"`
}
type DeleteWishListItemRequest struct {
ID string `json:"id" bson:"_id"`
}

168
pkg/user/repository.go Normal file
View File

@ -0,0 +1,168 @@
package user
import (
"context"
"time"
"wish-list-api/api/presenter"
"wish-list-api/pkg/entities"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type Repository interface {
CreateUser(user *entities.User) (*entities.User, error)
ReadUser(ID string) (*entities.User, error)
ReadUserByEmail(email string) (*entities.User, error)
ReadAllUsers() (*[]presenter.User, error)
UpdateUser(user *entities.User) (*entities.User, error)
DeleteUser(ID string) error
ReadUserByTelegramID(telegramID int64) (*entities.User, error)
UpdateUserTelegramData(user *entities.User) (*entities.User, error)
}
type repository struct {
Collection *mongo.Collection
}
func NewRepo(collection *mongo.Collection) Repository {
return &repository{
Collection: collection,
}
}
func NewMongoRepository(collection *mongo.Collection) Repository {
return NewRepo(collection)
}
func (r *repository) CreateUser(user *entities.User) (*entities.User, error) {
user.ID = primitive.NewObjectID()
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
_, err := r.Collection.InsertOne(context.Background(), user)
if err != nil {
return nil, err
}
return user, nil
}
func (r *repository) ReadUser(ID string) (*entities.User, error) {
var user entities.User
objectID, err := primitive.ObjectIDFromHex(ID)
if err != nil {
return nil, err
}
err = r.Collection.FindOne(context.Background(), bson.M{"_id": objectID}).Decode(&user)
if err != nil {
return nil, err
}
return &user, nil
}
func (r *repository) ReadUserByEmail(email string) (*entities.User, error) {
var user entities.User
err := r.Collection.FindOne(context.Background(), bson.M{"email": email}).Decode(&user)
if err != nil {
return nil, err
}
return &user, nil
}
func (r *repository) ReadAllUsers() (*[]presenter.User, error) {
var users []presenter.User
cursor, err := r.Collection.Find(context.Background(), bson.M{})
if err != nil {
return nil, err
}
for cursor.Next(context.TODO()) {
var user presenter.User
_ = cursor.Decode(&user)
users = append(users, user)
}
return &users, nil
}
func (r *repository) UpdateUser(user *entities.User) (*entities.User, error) {
user.UpdatedAt = time.Now()
_, err := r.Collection.UpdateOne(
context.Background(),
bson.M{"_id": user.ID},
bson.M{"$set": user},
)
if err != nil {
return nil, err
}
return user, nil
}
func (r *repository) DeleteUser(ID string) error {
objectID, err := primitive.ObjectIDFromHex(ID)
if err != nil {
return err
}
_, err = r.Collection.DeleteOne(context.Background(), bson.M{"_id": objectID})
if err != nil {
return err
}
return nil
}
func (r *repository) ReadUserByTelegramID(telegramID int64) (*entities.User, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var user entities.User
filter := bson.M{"telegram_id": telegramID}
err := r.Collection.FindOne(ctx, filter).Decode(&user)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *repository) UpdateUserTelegramData(user *entities.User) (*entities.User, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
user.UpdatedAt = time.Now()
user.LastLoginDate = time.Now()
filter := bson.M{"_id": user.ID}
update := bson.M{"$set": bson.M{
"telegram_id": user.TelegramID,
"telegram_username": user.TelegramUsername,
"first_name": user.FirstName,
"last_name": user.LastName,
"photo_url": user.PhotoURL,
"last_login_date": user.LastLoginDate,
"updated_at": user.UpdatedAt,
}}
_, err := r.Collection.UpdateOne(ctx, filter, update)
if err != nil {
return nil, err
}
return user, nil
}

60
pkg/user/service.go Normal file
View File

@ -0,0 +1,60 @@
package user
import (
"wish-list-api/api/presenter"
"wish-list-api/pkg/entities"
)
type Service interface {
CreateUser(user *entities.User) (*entities.User, error)
GetUser(ID string) (*entities.User, error)
GetUserByEmail(email string) (*entities.User, error)
GetAllUsers() (*[]presenter.User, error)
UpdateUser(user *entities.User) (*entities.User, error)
DeleteUser(ID string) error
GetUserByTelegramID(telegramID int64) (*entities.User, error)
UpdateUserTelegramData(user *entities.User) (*entities.User, error)
}
type service struct {
repository Repository
}
func NewService(r Repository) Service {
return &service{
repository: r,
}
}
func (s *service) CreateUser(user *entities.User) (*entities.User, error) {
return s.repository.CreateUser(user)
}
func (s *service) GetUser(ID string) (*entities.User, error) {
return s.repository.ReadUser(ID)
}
func (s *service) GetUserByEmail(email string) (*entities.User, error) {
return s.repository.ReadUserByEmail(email)
}
func (s *service) GetAllUsers() (*[]presenter.User, error) {
return s.repository.ReadAllUsers()
}
func (s *service) UpdateUser(user *entities.User) (*entities.User, error) {
return s.repository.UpdateUser(user)
}
func (s *service) DeleteUser(ID string) error {
return s.repository.DeleteUser(ID)
}
func (s *service) GetUserByTelegramID(telegramID int64) (*entities.User, error) {
return s.repository.ReadUserByTelegramID(telegramID)
}
func (s *service) UpdateUserTelegramData(user *entities.User) (*entities.User, error) {
return s.repository.UpdateUserTelegramData(user)
}

194
pkg/wish-list/repository.go Normal file
View File

@ -0,0 +1,194 @@
package wishlist
import (
"context"
"time"
"wish-list-api/api/presenter"
"wish-list-api/pkg/entities"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
)
type Repository interface {
CreateWishList(wishList *entities.WishList) (*entities.WishList, error)
ReadWishList(ID string) (*entities.WishList, error)
ReadAllWishLists(userID string) (*[]presenter.WishList, error)
ReadPublicWishLists() (*[]presenter.WishList, error)
UpdateWishList(wishList *entities.WishList) (*entities.WishList, error)
DeleteWishList(ID string) error
CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error)
ReadWishListItem(ID string) (*entities.WishListItem, error)
ReadAllWishListItems(wishListID string) (*[]presenter.WishListItem, error)
UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error)
DeleteWishListItem(ID string) error
}
type repository struct {
WishListCollection *mongo.Collection
WishListItemCollection *mongo.Collection
}
func NewRepo(wishListCollection, wishListItemCollection *mongo.Collection) Repository {
return &repository{
WishListCollection: wishListCollection,
WishListItemCollection: wishListItemCollection,
}
}
func NewMongoRepository(wishListCollection, wishListItemCollection *mongo.Collection) Repository {
return NewRepo(wishListCollection, wishListItemCollection)
}
func (r *repository) CreateWishList(wishList *entities.WishList) (*entities.WishList, error) {
wishList.ID = primitive.NewObjectID().Hex()
wishList.CreatedAt = time.Now()
wishList.UpdatedAt = time.Now()
_, err := r.WishListCollection.InsertOne(context.Background(), wishList)
if err != nil {
return nil, err
}
return wishList, nil
}
func (r *repository) ReadWishList(ID string) (*entities.WishList, error) {
var wishList entities.WishList
err := r.WishListCollection.FindOne(context.Background(), bson.M{"_id": ID}).Decode(&wishList)
if err != nil {
return nil, err
}
return &wishList, nil
}
func (r *repository) ReadAllWishLists(userID string) (*[]presenter.WishList, error) {
var wishLists []presenter.WishList
cursor, err := r.WishListCollection.Find(context.Background(), bson.M{"user_id": userID})
if err != nil {
return nil, err
}
for cursor.Next(context.TODO()) {
var wishList presenter.WishList
_ = cursor.Decode(&wishList)
wishLists = append(wishLists, wishList)
}
return &wishLists, nil
}
func (r *repository) ReadPublicWishLists() (*[]presenter.WishList, error) {
var wishLists []presenter.WishList
cursor, err := r.WishListCollection.Find(context.Background(), bson.M{"is_public": true})
if err != nil {
return nil, err
}
for cursor.Next(context.TODO()) {
var wishList presenter.WishList
_ = cursor.Decode(&wishList)
wishLists = append(wishLists, wishList)
}
return &wishLists, nil
}
func (r *repository) UpdateWishList(wishList *entities.WishList) (*entities.WishList, error) {
wishList.UpdatedAt = time.Now()
_, err := r.WishListCollection.UpdateOne(
context.Background(),
bson.M{"_id": wishList.ID},
bson.M{"$set": wishList},
)
if err != nil {
return nil, err
}
return wishList, nil
}
func (r *repository) DeleteWishList(ID string) error {
_, err := r.WishListCollection.DeleteOne(context.Background(), bson.M{"_id": ID})
if err != nil {
return err
}
_, err = r.WishListItemCollection.DeleteMany(context.Background(), bson.M{"wish_list_id": ID})
if err != nil {
return err
}
return nil
}
func (r *repository) CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) {
item.ID = primitive.NewObjectID().Hex()
item.CreatedAt = time.Now()
item.UpdatedAt = time.Now()
_, err := r.WishListItemCollection.InsertOne(context.Background(), item)
if err != nil {
return nil, err
}
return item, nil
}
func (r *repository) ReadWishListItem(ID string) (*entities.WishListItem, error) {
var item entities.WishListItem
err := r.WishListItemCollection.FindOne(context.Background(), bson.M{"_id": ID}).Decode(&item)
if err != nil {
return nil, err
}
return &item, nil
}
func (r *repository) ReadAllWishListItems(wishListID string) (*[]presenter.WishListItem, error) {
var items []presenter.WishListItem
cursor, err := r.WishListItemCollection.Find(context.Background(), bson.M{"wish_list_id": wishListID})
if err != nil {
return nil, err
}
for cursor.Next(context.TODO()) {
var item presenter.WishListItem
_ = cursor.Decode(&item)
items = append(items, item)
}
return &items, nil
}
func (r *repository) UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) {
item.UpdatedAt = time.Now()
_, err := r.WishListItemCollection.UpdateOne(
context.Background(),
bson.M{"_id": item.ID},
bson.M{"$set": item},
)
if err != nil {
return nil, err
}
return item, nil
}
func (r *repository) DeleteWishListItem(ID string) error {
_, err := r.WishListItemCollection.DeleteOne(context.Background(), bson.M{"_id": ID})
if err != nil {
return err
}
return nil
}

75
pkg/wish-list/service.go Normal file
View File

@ -0,0 +1,75 @@
package wishlist
import (
"wish-list-api/api/presenter"
"wish-list-api/pkg/entities"
)
type Service interface {
CreateWishList(wishList *entities.WishList) (*entities.WishList, error)
GetWishList(ID string) (*entities.WishList, error)
GetAllWishLists(userID string) (*[]presenter.WishList, error)
GetPublicWishLists() (*[]presenter.WishList, error)
UpdateWishList(wishList *entities.WishList) (*entities.WishList, error)
DeleteWishList(ID string) error
CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error)
GetWishListItem(ID string) (*entities.WishListItem, error)
GetAllWishListItems(wishListID string) (*[]presenter.WishListItem, error)
UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error)
DeleteWishListItem(ID string) error
}
type service struct {
repository Repository
}
func NewService(r Repository) Service {
return &service{
repository: r,
}
}
func (s *service) CreateWishList(wishList *entities.WishList) (*entities.WishList, error) {
return s.repository.CreateWishList(wishList)
}
func (s *service) GetWishList(ID string) (*entities.WishList, error) {
return s.repository.ReadWishList(ID)
}
func (s *service) GetAllWishLists(userID string) (*[]presenter.WishList, error) {
return s.repository.ReadAllWishLists(userID)
}
func (s *service) GetPublicWishLists() (*[]presenter.WishList, error) {
return s.repository.ReadPublicWishLists()
}
func (s *service) UpdateWishList(wishList *entities.WishList) (*entities.WishList, error) {
return s.repository.UpdateWishList(wishList)
}
func (s *service) DeleteWishList(ID string) error {
return s.repository.DeleteWishList(ID)
}
func (s *service) CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) {
return s.repository.CreateWishListItem(item)
}
func (s *service) GetWishListItem(ID string) (*entities.WishListItem, error) {
return s.repository.ReadWishListItem(ID)
}
func (s *service) GetAllWishListItems(wishListID string) (*[]presenter.WishListItem, error) {
return s.repository.ReadAllWishListItems(wishListID)
}
func (s *service) UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) {
return s.repository.UpdateWishListItem(item)
}
func (s *service) DeleteWishListItem(ID string) error {
return s.repository.DeleteWishListItem(ID)
}

61
run-docker-tests.sh Executable file
View File

@ -0,0 +1,61 @@
#!/bin/bash
# Цвета для вывода
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
# Используем -e для интерпретации escape-последовательностей
echo -e "${YELLOW}===== Запуск тестов в Docker =====${NC}"
# Проверка существования директории для отчетов о покрытии
if [ ! -d "./coverage" ]; then
mkdir -p ./coverage
echo -e "Создана директория ./coverage для хранения отчетов о покрытии тестами"
fi
# Проверка запущен ли MongoDB, если нет - запускаем
if ! docker ps | grep -q mongodb; then
echo -e "${YELLOW}MongoDB не запущена, запускаем контейнер...${NC}"
docker compose up -d mongo
# Даем больше времени на запуск MongoDB
echo -e "Ожидаем запуск MongoDB..."
sleep 10
echo -e "Проверяем готовность MongoDB..."
# Попытка подключения к MongoDB для проверки её готовности
docker exec mongodb mongosh --quiet --eval "db.stats()"
if [ $? -eq 0 ]; then
echo -e "${GREEN}MongoDB успешно запущена и готова к работе${NC}"
else
echo -e "${YELLOW}Ожидаем дополнительное время для полной инициализации MongoDB...${NC}"
sleep 5
fi
fi
# Запускаем тесты в Docker
echo -e "${YELLOW}Запускаем контейнер с тестами...${NC}"
docker compose up --build tests
# Получаем код выхода контейнера с тестами
exit_code=$(docker inspect -f '{{.State.ExitCode}}' wish-list-api-tests)
# Останавливаем контейнер с тестами
docker compose stop tests
if [ $exit_code -eq 0 ]; then
echo -e "${GREEN}Все тесты успешно пройдены${NC}"
echo -e "${YELLOW}Отчеты о покрытии тестами сохранены в директории ./coverage${NC}"
else
echo -e "${RED}✗ Некоторые тесты содержат ошибки (код выхода: $exit_code)${NC}"
fi
# Спрашиваем пользователя, хочет ли он остановить MongoDB
read -p "Остановить MongoDB? (y/n): " stop_mongo
if [ "$stop_mongo" = "y" ] || [ "$stop_mongo" = "Y" ]; then
docker compose stop mongo
echo -e "MongoDB остановлена"
fi
exit $exit_code

77
scripts/run_tests.sh Executable file
View File

@ -0,0 +1,77 @@
#!/bin/sh
# Цвета для вывода
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
echo -e "${YELLOW}===== Запуск всех тестов =====${NC}"
# Создаем директорию для отчетов о покрытии, если ее нет
mkdir -p ./coverage
# Функция для выполнения и отчета о результатах тестов
run_tests() {
test_type=$1
test_path=$2
echo -e "${YELLOW}===== Запуск $test_type тестов =====${NC}"
# Запускаем тесты с генерацией отчета о покрытии
if go test -v -coverprofile=./coverage/coverage_$test_type.out $test_path; then
echo -e "${GREEN}$test_type тесты успешно пройдены${NC}"
# Генерация HTML-отчета о покрытии
go tool cover -html=./coverage/coverage_$test_type.out -o ./coverage/coverage_$test_type.html
return 0
else
echo -e "${RED}$test_type тесты содержат ошибки${NC}"
return 1
fi
}
# Запускаем unit-тесты
run_tests "unit" "./tests/unit/..."
UNIT_RESULT=$?
# Функция для проверки доступности MongoDB
check_mongodb() {
# В Docker контейнере MongoDB доступен по имени хоста 'mongo'
if [ -n "$MONGODB_URI" ]; then
# Если задана переменная окружения MONGODB_URI, считаем что MongoDB доступен
return 0
else
# Проверяем доступность MongoDB
if nc -z mongo 27017 2>/dev/null; then
return 0
elif nc -z localhost 27017 2>/dev/null; then
return 0
else
return 1
fi
fi
}
# Если настроена переменная окружения для запуска интеграционных тестов
if [ "$RUN_INTEGRATION_TESTS" = "true" ]; then
# Запускаем интеграционные тесты, если MongoDB доступна
if check_mongodb; then
run_tests "integration" "./tests/integration/..."
INTEGRATION_RESULT=$?
else
echo -e "${YELLOW}MongoDB не доступна, интеграционные тесты пропущены${NC}"
INTEGRATION_RESULT=0
fi
else
echo -e "${YELLOW}Интеграционные тесты пропущены (RUN_INTEGRATION_TESTS не установлена)${NC}"
INTEGRATION_RESULT=0
fi
# Проверяем общий результат
if [ $UNIT_RESULT -eq 0 ] && [ $INTEGRATION_RESULT -eq 0 ]; then
echo -e "${GREEN}Все тесты успешно пройдены${NC}"
exit 0
else
echo -e "${RED}✗ Некоторые тесты содержат ошибки${NC}"
exit 1
fi

View File

@ -0,0 +1,398 @@
package integration
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"wish-list-api/api/handlers"
"wish-list-api/api/middleware"
"wish-list-api/pkg/auth"
"wish-list-api/pkg/entities"
"wish-list-api/pkg/user"
wishlist "wish-list-api/pkg/wish-list"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var (
app *fiber.App
testDB *mongo.Database
client *mongo.Client
authSvc auth.Service
)
func TestMain(m *testing.M) {
setup()
exitCode := m.Run()
teardown()
os.Exit(exitCode)
}
func setup() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var err error
mongoURI := os.Getenv("MONGODB_URI")
if mongoURI == "" {
mongoURI = "mongodb://mongo_user:mongo_password@localhost:27017/admin"
}
client, err = mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
if err != nil {
log.Fatalf("Error connecting to MongoDB: %v", err)
}
err = client.Ping(ctx, nil)
if err != nil {
log.Fatalf("Could not connect to MongoDB: %v", err)
}
dbName := fmt.Sprintf("wishlist_test_%d", time.Now().UnixNano())
testDB = client.Database(dbName)
userCollection := testDB.Collection("users")
wishlistCollection := testDB.Collection("wishlists")
wishlistItemCollection := testDB.Collection("wishlist_items")
userRepo := user.NewMongoRepository(userCollection)
wishlistRepo := wishlist.NewMongoRepository(wishlistCollection, wishlistItemCollection)
userService := user.NewService(userRepo)
authSvc = auth.NewService(auth.ServiceConfig{
UserService: userService,
})
wishlistSvc := wishlist.NewService(wishlistRepo)
app = fiber.New()
api := app.Group("/api")
api.Post("/auth/register", handlers.Register(authSvc))
api.Post("/auth/login", handlers.Login(authSvc))
api.Post("/auth/refresh", handlers.RefreshToken(authSvc))
wishListHandler := handlers.NewWishListHandler(wishlistSvc, authSvc)
wishList := api.Group("/wishlist")
wishList.Get("/:id", wishListHandler.GetWishList)
wishList.Get("/user/:userId", wishListHandler.GetUserWishLists)
wishList.Get("/:wishlistId/items", wishListHandler.GetWishListItems)
wishList.Get("/item/:id", wishListHandler.GetWishListItem)
wishList.Post("/", middleware.Protected(authSvc), wishListHandler.CreateWishList)
wishList.Put("/:id", middleware.Protected(authSvc), wishListHandler.UpdateWishList)
wishList.Delete("/:id", middleware.Protected(authSvc), wishListHandler.DeleteWishList)
wishList.Post("/item", middleware.Protected(authSvc), wishListHandler.CreateWishListItem)
wishList.Put("/item/:id", middleware.Protected(authSvc), wishListHandler.UpdateWishListItem)
wishList.Delete("/item/:id", middleware.Protected(authSvc), wishListHandler.DeleteWishListItem)
}
func teardown() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := testDB.Drop(ctx); err != nil {
log.Printf("Error dropping test database: %v", err)
}
if err := client.Disconnect(ctx); err != nil {
log.Printf("Error disconnecting from MongoDB: %v", err)
}
}
func registerTestUser(t *testing.T) (string, primitive.ObjectID) {
userData := entities.RegisterRequest{
Email: fmt.Sprintf("test%d@example.com", time.Now().UnixNano()),
Password: "password123",
}
jsonBody, _ := json.Marshal(userData)
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var response struct {
Status bool `json:"status"`
Data entities.TokenPair `json:"data"`
Error *string `json:"error"`
}
err = json.NewDecoder(resp.Body).Decode(&response)
assert.NoError(t, err)
claims, err := authSvc.ValidateToken(response.Data.AccessToken)
assert.NoError(t, err)
mapClaims := claims.Claims.(jwt.MapClaims)
userIDStr := mapClaims["user_id"].(string)
userID, err := primitive.ObjectIDFromHex(userIDStr)
assert.NoError(t, err)
return response.Data.AccessToken, userID
}
func TestRegister(t *testing.T) {
userData := entities.RegisterRequest{
Email: fmt.Sprintf("test%d@example.com", time.Now().UnixNano()),
Password: "password123",
}
jsonBody, _ := json.Marshal(userData)
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
req = httptest.NewRequest("POST", "/api/auth/register", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusConflict, resp.StatusCode)
}
func TestLogin(t *testing.T) {
email := fmt.Sprintf("test%d@example.com", time.Now().UnixNano())
userData := entities.RegisterRequest{
Email: email,
Password: "password123",
}
jsonBody, _ := json.Marshal(userData)
req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
loginData := entities.LoginRequest{
Email: email,
Password: "password123",
}
jsonBody, _ = json.Marshal(loginData)
req = httptest.NewRequest("POST", "/api/auth/login", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
loginData.Password = "wrongpassword"
jsonBody, _ = json.Marshal(loginData)
req = httptest.NewRequest("POST", "/api/auth/login", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}
func TestWishListCRUD(t *testing.T) {
token, userID := registerTestUser(t)
wishlistData := entities.WishList{
Title: "Test Wishlist",
Description: "Integration test wishlist",
IsPublic: true,
}
jsonBody, _ := json.Marshal(wishlistData)
req := httptest.NewRequest("POST", "/api/wishlist", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
var createResponse struct {
Status bool `json:"status"`
Data entities.WishList `json:"data"`
Error *string `json:"error"`
}
err = json.NewDecoder(resp.Body).Decode(&createResponse)
assert.NoError(t, err)
wishlistID := createResponse.Data.ID
req = httptest.NewRequest("GET", "/api/wishlist/"+wishlistID, nil)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
updatedData := entities.WishList{
Title: "Updated Title",
Description: "Updated description",
IsPublic: false,
}
jsonBody, _ = json.Marshal(updatedData)
req = httptest.NewRequest("PUT", "/api/wishlist/"+wishlistID, bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
req = httptest.NewRequest("GET", "/api/wishlist/"+wishlistID, nil)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
req = httptest.NewRequest("GET", "/api/wishlist/"+wishlistID, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
req = httptest.NewRequest("GET", "/api/wishlist/user/"+userID.Hex(), nil)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
req = httptest.NewRequest("DELETE", "/api/wishlist/"+wishlistID, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
req = httptest.NewRequest("GET", "/api/wishlist/"+wishlistID, nil)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}
func TestWishListItemsCRUD(t *testing.T) {
token, _ := registerTestUser(t)
wishlistData := entities.WishList{
Title: "Test Wishlist for Items",
Description: "Testing wishlist items",
IsPublic: true,
}
jsonBody, _ := json.Marshal(wishlistData)
req := httptest.NewRequest("POST", "/api/wishlist", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
var createResponse struct {
Status bool `json:"status"`
Data entities.WishList `json:"data"`
Error *string `json:"error"`
}
err = json.NewDecoder(resp.Body).Decode(&createResponse)
assert.NoError(t, err)
wishlistID := createResponse.Data.ID
itemData := entities.WishListItem{
Title: "Test Item",
Description: "Test item description",
URL: "https://example.com",
Cost: 99.99,
WishListID: wishlistID,
}
jsonBody, _ = json.Marshal(itemData)
req = httptest.NewRequest("POST", "/api/wishlist/item", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
var itemResponse struct {
Status bool `json:"status"`
Data entities.WishListItem `json:"data"`
Error *string `json:"error"`
}
err = json.NewDecoder(resp.Body).Decode(&itemResponse)
assert.NoError(t, err)
itemID := itemResponse.Data.ID
req = httptest.NewRequest("GET", "/api/wishlist/item/"+itemID, nil)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
updatedItemData := entities.WishListItem{
Title: "Updated Item Title",
Description: "Updated item description",
URL: "https://example.com",
Cost: 149.99,
}
jsonBody, _ = json.Marshal(updatedItemData)
req = httptest.NewRequest("PUT", "/api/wishlist/item/"+itemID, bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
req = httptest.NewRequest("GET", "/api/wishlist/"+wishlistID+"/items", nil)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var itemsResponse struct {
Status bool `json:"status"`
Data []entities.WishListItem `json:"data"`
Error *string `json:"error"`
}
err = json.NewDecoder(resp.Body).Decode(&itemsResponse)
assert.NoError(t, err)
assert.Equal(t, 1, len(itemsResponse.Data))
req = httptest.NewRequest("DELETE", "/api/wishlist/item/"+itemID, nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
req = httptest.NewRequest("GET", "/api/wishlist/item/"+itemID, nil)
resp, err = app.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
}

View File

@ -0,0 +1,45 @@
package integration
import (
"context"
"fmt"
"log"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func TestMongoDBConnection(t *testing.T) {
mongoURI := os.Getenv("MONGODB_URI")
if mongoURI == "" {
mongoURI = "mongodb://mongo_user:mongo_password@localhost:27017/admin"
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
assert.NoError(t, err, "Ошибка подключения к MongoDB")
defer client.Disconnect(ctx)
err = client.Ping(ctx, nil)
assert.NoError(t, err, "Не удается пропинговать MongoDB")
dbName := fmt.Sprintf("test_db_%d", time.Now().UnixNano())
collection := client.Database(dbName).Collection("test_collection")
doc := bson.M{"name": "test", "value": "success"}
result, err := collection.InsertOne(ctx, doc)
assert.NoError(t, err, "Ошибка при вставке документа")
assert.NotNil(t, result.InsertedID, "ID вставленного документа не должен быть nil")
err = client.Database(dbName).Drop(ctx)
assert.NoError(t, err, "Ошибка при удалении тестовой БД")
log.Println("MongoDB подключение успешно проверено!")
}

View File

@ -0,0 +1,347 @@
package unit
import (
"errors"
"testing"
"time"
"wish-list-api/api/presenter"
"wish-list-api/pkg/auth"
"wish-list-api/pkg/entities"
"wish-list-api/pkg/user"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
)
var ErrUserNotFound = errors.New("user not found")
type MockUserRepository struct {
users map[string]*entities.User
email map[string]*entities.User
telegram map[int64]*entities.User
}
func NewMockUserRepository() user.Repository {
return &MockUserRepository{
users: make(map[string]*entities.User),
email: make(map[string]*entities.User),
telegram: make(map[int64]*entities.User),
}
}
func (r *MockUserRepository) CreateUser(user *entities.User) (*entities.User, error) {
if user.ID.IsZero() {
user.ID = primitive.NewObjectID()
}
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
r.users[user.ID.Hex()] = user
r.email[user.Email] = user
if user.TelegramID != 0 {
r.telegram[user.TelegramID] = user
}
return user, nil
}
func (r *MockUserRepository) ReadUser(id string) (*entities.User, error) {
if user, ok := r.users[id]; ok {
return user, nil
}
return nil, ErrUserNotFound
}
func (r *MockUserRepository) ReadUserByEmail(email string) (*entities.User, error) {
if user, ok := r.email[email]; ok {
return user, nil
}
return nil, ErrUserNotFound
}
func (r *MockUserRepository) ReadAllUsers() (*[]presenter.User, error) {
var users []presenter.User
for _, u := range r.users {
users = append(users, presenter.User{
ID: u.ID,
Email: u.Email,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
})
}
return &users, nil
}
func (r *MockUserRepository) UpdateUser(user *entities.User) (*entities.User, error) {
if _, ok := r.users[user.ID.Hex()]; !ok {
return nil, ErrUserNotFound
}
user.UpdatedAt = time.Now()
r.users[user.ID.Hex()] = user
for email, u := range r.email {
if u.ID == user.ID && email != user.Email {
delete(r.email, email)
break
}
}
r.email[user.Email] = user
if user.TelegramID != 0 {
r.telegram[user.TelegramID] = user
}
return user, nil
}
func (r *MockUserRepository) DeleteUser(id string) error {
user, ok := r.users[id]
if !ok {
return ErrUserNotFound
}
delete(r.users, id)
delete(r.email, user.Email)
if user.TelegramID != 0 {
delete(r.telegram, user.TelegramID)
}
return nil
}
func (r *MockUserRepository) ReadUserByTelegramID(telegramID int64) (*entities.User, error) {
if user, ok := r.telegram[telegramID]; ok {
return user, nil
}
return nil, ErrUserNotFound
}
func (r *MockUserRepository) UpdateUserTelegramData(user *entities.User) (*entities.User, error) {
if _, ok := r.users[user.ID.Hex()]; !ok {
return nil, ErrUserNotFound
}
existingUser := r.users[user.ID.Hex()]
existingUser.TelegramID = user.TelegramID
existingUser.TelegramUsername = user.TelegramUsername
existingUser.FirstName = user.FirstName
existingUser.LastName = user.LastName
existingUser.PhotoURL = user.PhotoURL
existingUser.UpdatedAt = time.Now()
r.users[user.ID.Hex()] = existingUser
if user.TelegramID != 0 {
for tgID, u := range r.telegram {
if u.ID == user.ID && tgID != user.TelegramID {
delete(r.telegram, tgID)
break
}
}
r.telegram[user.TelegramID] = existingUser
}
return existingUser, nil
}
func TestRegister(t *testing.T) {
mockRepo := NewMockUserRepository()
mockUserService := user.NewService(mockRepo)
serviceConfig := auth.ServiceConfig{
UserService: mockUserService,
}
service := auth.NewService(serviceConfig)
t.Run("Register_Success", func(t *testing.T) {
userData := &entities.RegisterRequest{
Email: "test@example.com",
Password: "password123",
}
user, err := service.Register(userData)
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, userData.Email, user.Email)
assert.NotEqual(t, userData.Password, user.Password)
})
t.Run("Register_DuplicateEmail", func(t *testing.T) {
userData := &entities.RegisterRequest{
Email: "test@example.com",
Password: "different_password",
}
user, err := service.Register(userData)
assert.Error(t, err)
assert.Nil(t, user)
})
}
func TestLogin(t *testing.T) {
mockRepo := NewMockUserRepository()
mockUserService := user.NewService(mockRepo)
serviceConfig := auth.ServiceConfig{
UserService: mockUserService,
}
service := auth.NewService(serviceConfig)
registerData := &entities.RegisterRequest{
Email: "login_test@example.com",
Password: "password123",
}
_, err := service.Register(registerData)
assert.NoError(t, err)
t.Run("Login_Success", func(t *testing.T) {
loginData := &entities.LoginRequest{
Email: "login_test@example.com",
Password: "password123",
}
tokens, err := service.Login(loginData)
assert.NoError(t, err)
assert.NotNil(t, tokens)
assert.NotEmpty(t, tokens.AccessToken)
assert.NotEmpty(t, tokens.RefreshToken)
})
t.Run("Login_InvalidEmail", func(t *testing.T) {
loginData := &entities.LoginRequest{
Email: "nonexistent@example.com",
Password: "password123",
}
tokens, err := service.Login(loginData)
assert.Error(t, err)
assert.Nil(t, tokens)
})
t.Run("Login_InvalidPassword", func(t *testing.T) {
loginData := &entities.LoginRequest{
Email: "login_test@example.com",
Password: "wrong_password",
}
tokens, err := service.Login(loginData)
assert.Error(t, err)
assert.Nil(t, tokens)
})
}
func TestRefreshToken(t *testing.T) {
mockRepo := NewMockUserRepository()
mockUserService := user.NewService(mockRepo)
serviceConfig := auth.ServiceConfig{
UserService: mockUserService,
}
service := auth.NewService(serviceConfig)
registerData := &entities.RegisterRequest{
Email: "refresh_test@example.com",
Password: "password123",
}
_, err := service.Register(registerData)
assert.NoError(t, err)
loginData := &entities.LoginRequest{
Email: "refresh_test@example.com",
Password: "password123",
}
tokens, err := service.Login(loginData)
assert.NoError(t, err)
t.Run("RefreshToken_Success", func(t *testing.T) {
newTokens, err := service.RefreshToken(tokens.RefreshToken)
assert.NoError(t, err)
assert.NotNil(t, newTokens)
assert.NotEmpty(t, newTokens.AccessToken)
assert.NotEmpty(t, newTokens.RefreshToken)
})
t.Run("RefreshToken_Invalid", func(t *testing.T) {
newTokens, err := service.RefreshToken("invalid_refresh_token")
assert.Error(t, err)
assert.Nil(t, newTokens)
})
}
func TestValidateToken(t *testing.T) {
mockRepo := NewMockUserRepository()
mockUserService := user.NewService(mockRepo)
serviceConfig := auth.ServiceConfig{
UserService: mockUserService,
}
service := auth.NewService(serviceConfig)
registerData := &entities.RegisterRequest{
Email: "validate_test@example.com",
Password: "password123",
}
_, err := service.Register(registerData)
assert.NoError(t, err)
loginData := &entities.LoginRequest{
Email: "validate_test@example.com",
Password: "password123",
}
tokens, err := service.Login(loginData)
assert.NoError(t, err)
t.Run("ValidateToken_Success", func(t *testing.T) {
token, err := service.ValidateToken(tokens.AccessToken)
assert.NoError(t, err)
assert.NotNil(t, token)
})
t.Run("ValidateToken_Invalid", func(t *testing.T) {
token, err := service.ValidateToken("invalid_token")
assert.Error(t, err)
assert.Nil(t, token)
})
}
func TestGetUserIDFromToken(t *testing.T) {
mockRepo := NewMockUserRepository()
mockUserService := user.NewService(mockRepo)
serviceConfig := auth.ServiceConfig{
UserService: mockUserService,
}
service := auth.NewService(serviceConfig)
registerData := &entities.RegisterRequest{
Email: "userid_test@example.com",
Password: "password123",
}
user, err := service.Register(registerData)
assert.NoError(t, err)
loginData := &entities.LoginRequest{
Email: "userid_test@example.com",
Password: "password123",
}
tokens, err := service.Login(loginData)
assert.NoError(t, err)
t.Run("GetUserIDFromToken_Success", func(t *testing.T) {
userID, err := service.GetUserIDFromToken(tokens.AccessToken)
assert.NoError(t, err)
assert.Equal(t, user.ID.Hex(), userID)
})
t.Run("GetUserIDFromToken_Invalid", func(t *testing.T) {
userID, err := service.GetUserIDFromToken("invalid_token")
assert.Error(t, err)
assert.Empty(t, userID)
})
}

121
tests/unit/mock_auth.go Normal file
View File

@ -0,0 +1,121 @@
package unit
import (
"errors"
"time"
"wish-list-api/pkg/auth"
"wish-list-api/pkg/entities"
"github.com/golang-jwt/jwt/v5"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type MockAuthService struct {
users map[primitive.ObjectID]*entities.User
tokens map[string]primitive.ObjectID
}
func NewMockAuthService() auth.Service {
return &MockAuthService{
users: make(map[primitive.ObjectID]*entities.User),
tokens: make(map[string]primitive.ObjectID),
}
}
func (m *MockAuthService) AddMockUser(user *entities.User, token string) {
m.users[user.ID] = user
m.tokens[token] = user.ID
}
func (m *MockAuthService) Login(credentials *entities.LoginRequest) (*entities.TokenPair, error) {
for _, user := range m.users {
if user.Email == credentials.Email && m.ComparePasswords(user.Password, credentials.Password) {
token := "mock-token-for-" + user.ID.Hex()
m.tokens[token] = user.ID
return &entities.TokenPair{
AccessToken: token,
RefreshToken: "refresh-" + token,
}, nil
}
}
return nil, errors.New("invalid credentials")
}
func (m *MockAuthService) Register(userData *entities.RegisterRequest) (*entities.User, error) {
for _, existingUser := range m.users {
if existingUser.Email == userData.Email {
return nil, errors.New("email already exists")
}
}
hashedPassword, _ := m.HashPassword(userData.Password)
user := &entities.User{
ID: primitive.NewObjectID(),
Email: userData.Email,
Password: hashedPassword,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
m.users[user.ID] = user
return user, nil
}
func (m *MockAuthService) RefreshToken(refreshToken string) (*entities.TokenPair, error) {
if len(refreshToken) < 8 {
return nil, errors.New("invalid refresh token")
}
accessToken := refreshToken[8:]
userID, ok := m.tokens[accessToken]
if !ok {
return nil, errors.New("invalid refresh token")
}
newToken := "mock-token-for-" + userID.Hex() + "-refreshed"
m.tokens[newToken] = userID
return &entities.TokenPair{
AccessToken: newToken,
RefreshToken: "refresh-" + newToken,
}, nil
}
func (m *MockAuthService) ValidateToken(tokenString string) (*jwt.Token, error) {
if len(tokenString) > 7 && tokenString[:7] == "Bearer " {
tokenString = tokenString[7:]
}
userID, ok := m.tokens[tokenString]
if !ok {
return nil, errors.New("invalid token")
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userID.Hex(),
"exp": time.Now().Add(time.Hour).Unix(),
})
return token, nil
}
func (m *MockAuthService) GetUserIDFromToken(tokenString string) (string, error) {
if len(tokenString) > 7 && tokenString[:7] == "Bearer " {
tokenString = tokenString[7:]
}
userID, ok := m.tokens[tokenString]
if !ok {
return "", errors.New("invalid token")
}
return userID.Hex(), nil
}
func (m *MockAuthService) HashPassword(password string) (string, error) {
return "hashed_" + password, nil
}
func (m *MockAuthService) ComparePasswords(hashedPassword string, plainPassword string) bool {
return hashedPassword == "hashed_"+plainPassword
}

View File

@ -0,0 +1,167 @@
package unit
import (
"errors"
"time"
"wish-list-api/api/presenter"
"wish-list-api/pkg/entities"
wishlist "wish-list-api/pkg/wish-list"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type MockWishListRepository struct {
wishLists map[string]*entities.WishList
wishListItems map[string]*entities.WishListItem
}
func NewMockWishListRepository() wishlist.Repository {
return &MockWishListRepository{
wishLists: make(map[string]*entities.WishList),
wishListItems: make(map[string]*entities.WishListItem),
}
}
func (r *MockWishListRepository) CreateWishList(wishList *entities.WishList) (*entities.WishList, error) {
if wishList.ID == "" {
wishList.ID = "mock-id-" + time.Now().String()
}
wishList.CreatedAt = time.Now()
wishList.UpdatedAt = time.Now()
r.wishLists[wishList.ID] = wishList
return wishList, nil
}
func (r *MockWishListRepository) ReadWishList(ID string) (*entities.WishList, error) {
if wishList, ok := r.wishLists[ID]; ok {
return wishList, nil
}
return nil, errors.New("wishlist not found")
}
func (r *MockWishListRepository) ReadAllWishLists(userID string) (*[]presenter.WishList, error) {
var result []presenter.WishList
for _, wl := range r.wishLists {
if wl.UserID == userID {
objID, _ := primitive.ObjectIDFromHex(wl.ID)
result = append(result, presenter.WishList{
ID: objID,
Title: wl.Title,
UserID: wl.UserID,
Description: wl.Description,
IsPublic: wl.IsPublic,
PhotoURL: wl.PhotoURL,
CreatedAt: wl.CreatedAt,
UpdatedAt: wl.UpdatedAt,
})
}
}
return &result, nil
}
func (r *MockWishListRepository) ReadPublicWishLists() (*[]presenter.WishList, error) {
var result []presenter.WishList
for _, wl := range r.wishLists {
if wl.IsPublic {
objID, _ := primitive.ObjectIDFromHex(wl.ID)
result = append(result, presenter.WishList{
ID: objID,
Title: wl.Title,
UserID: wl.UserID,
Description: wl.Description,
IsPublic: wl.IsPublic,
PhotoURL: wl.PhotoURL,
CreatedAt: wl.CreatedAt,
UpdatedAt: wl.UpdatedAt,
})
}
}
return &result, nil
}
func (r *MockWishListRepository) UpdateWishList(wishList *entities.WishList) (*entities.WishList, error) {
if _, ok := r.wishLists[wishList.ID]; !ok {
return nil, errors.New("wishlist not found")
}
wishList.UpdatedAt = time.Now()
r.wishLists[wishList.ID] = wishList
return wishList, nil
}
func (r *MockWishListRepository) DeleteWishList(ID string) error {
if _, ok := r.wishLists[ID]; !ok {
return errors.New("wishlist not found")
}
delete(r.wishLists, ID)
for id, item := range r.wishListItems {
if item.WishListID == ID {
delete(r.wishListItems, id)
}
}
return nil
}
func (r *MockWishListRepository) CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) {
if _, ok := r.wishLists[item.WishListID]; !ok {
return nil, errors.New("wishlist not found")
}
if item.ID == "" {
item.ID = "mock-item-id-" + time.Now().String()
}
item.CreatedAt = time.Now()
item.UpdatedAt = time.Now()
r.wishListItems[item.ID] = item
return item, nil
}
func (r *MockWishListRepository) ReadWishListItem(ID string) (*entities.WishListItem, error) {
if item, ok := r.wishListItems[ID]; ok {
return item, nil
}
return nil, errors.New("wishlist item not found")
}
func (r *MockWishListRepository) ReadAllWishListItems(wishListID string) (*[]presenter.WishListItem, error) {
if _, ok := r.wishLists[wishListID]; !ok {
return nil, errors.New("wishlist not found")
}
var result []presenter.WishListItem
for _, item := range r.wishListItems {
if item.WishListID == wishListID {
objID, _ := primitive.ObjectIDFromHex(item.ID)
result = append(result, presenter.WishListItem{
ID: objID,
Title: item.Title,
URL: item.URL,
Cost: item.Cost,
WishListID: item.WishListID,
Description: item.Description,
PhotoURL: item.PhotoURL,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
})
}
}
return &result, nil
}
func (r *MockWishListRepository) UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) {
if _, ok := r.wishListItems[item.ID]; !ok {
return nil, errors.New("wishlist item not found")
}
item.UpdatedAt = time.Now()
r.wishListItems[item.ID] = item
return item, nil
}
func (r *MockWishListRepository) DeleteWishListItem(ID string) error {
if _, ok := r.wishListItems[ID]; !ok {
return errors.New("wishlist item not found")
}
delete(r.wishListItems, ID)
return nil
}

View File

@ -0,0 +1,283 @@
package unit
import (
"bytes"
"encoding/json"
"net/http/httptest"
"testing"
"wish-list-api/api/handlers"
"wish-list-api/pkg/entities"
wishlist "wish-list-api/pkg/wish-list"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
)
func setupTestApp() (*fiber.App, *handlers.WishListHandler, wishlist.Repository, *MockAuthService) {
app := fiber.New()
mockRepo := NewMockWishListRepository()
mockAuth := NewMockAuthService().(*MockAuthService)
userID := primitive.NewObjectID()
user := &entities.User{
ID: userID,
Email: "test@example.com",
}
mockAuth.AddMockUser(user, "valid-token")
service := wishlist.NewService(mockRepo)
handler := handlers.NewWishListHandler(service, mockAuth)
return app, handler, mockRepo, mockAuth
}
func TestCreateWishListHandler(t *testing.T) {
app, handler, _, _ := setupTestApp()
app.Post("/wishlist", handler.CreateWishList)
t.Run("CreateWishList_Success", func(t *testing.T) {
wishList := entities.WishList{
Title: "Test Wishlist",
Description: "Test Description",
IsPublic: true,
}
jsonBody, _ := json.Marshal(wishList)
req := httptest.NewRequest("POST", "/wishlist", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer valid-token")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 201, resp.StatusCode)
})
t.Run("CreateWishList_Unauthorized", func(t *testing.T) {
wishList := entities.WishList{
Title: "Test Wishlist",
Description: "Test Description",
IsPublic: true,
}
jsonBody, _ := json.Marshal(wishList)
req := httptest.NewRequest("POST", "/wishlist", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
})
t.Run("CreateWishList_InvalidToken", func(t *testing.T) {
wishList := entities.WishList{
Title: "Test Wishlist",
Description: "Test Description",
IsPublic: true,
}
jsonBody, _ := json.Marshal(wishList)
req := httptest.NewRequest("POST", "/wishlist", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer invalid-token")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
})
}
func TestGetWishListHandler(t *testing.T) {
app, handler, repo, mockAuth := setupTestApp()
app.Get("/wishlist/:id", handler.GetWishList)
userObjID := primitive.NewObjectID()
userID := userObjID.Hex()
wishList := &entities.WishList{
ID: "test-wishlist-id",
Title: "Test Public Wishlist",
UserID: userID,
IsPublic: true,
}
repo.CreateWishList(wishList)
privateWishList := &entities.WishList{
ID: "private-wishlist-id",
Title: "Test Private Wishlist",
UserID: userID,
IsPublic: false,
}
repo.CreateWishList(privateWishList)
mockAuth.tokens["valid-token"] = userObjID
t.Run("GetWishList_Public_Success", func(t *testing.T) {
req := httptest.NewRequest("GET", "/wishlist/test-wishlist-id", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
})
t.Run("GetWishList_Private_Unauthorized", func(t *testing.T) {
req := httptest.NewRequest("GET", "/wishlist/private-wishlist-id", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
})
t.Run("GetWishList_Private_AuthorizedOwner", func(t *testing.T) {
req := httptest.NewRequest("GET", "/wishlist/private-wishlist-id", nil)
req.Header.Set("Authorization", "Bearer valid-token")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
})
t.Run("GetWishList_NotFound", func(t *testing.T) {
req := httptest.NewRequest("GET", "/wishlist/non-existent-id", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 404, resp.StatusCode)
})
}
func TestUpdateWishListHandler(t *testing.T) {
app, handler, repo, mockAuth := setupTestApp()
app.Put("/wishlist/:id", handler.UpdateWishList)
userID := primitive.NewObjectID()
wishList := &entities.WishList{
ID: "test-wishlist-id",
Title: "Original Title",
UserID: userID.Hex(),
IsPublic: true,
}
repo.CreateWishList(wishList)
otherUserWishList := &entities.WishList{
ID: "other-user-wishlist-id",
Title: "Other User's Wishlist",
UserID: "other-user-id",
IsPublic: true,
}
repo.CreateWishList(otherUserWishList)
mockAuth.tokens["valid-token"] = userID
t.Run("UpdateWishList_Success", func(t *testing.T) {
updatedWishList := entities.WishList{
Title: "Updated Title",
Description: "Updated Description",
IsPublic: false,
}
jsonBody, _ := json.Marshal(updatedWishList)
req := httptest.NewRequest("PUT", "/wishlist/test-wishlist-id", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer valid-token")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
updatedList, _ := repo.ReadWishList("test-wishlist-id")
assert.Equal(t, "Updated Title", updatedList.Title)
assert.Equal(t, "Updated Description", updatedList.Description)
assert.Equal(t, false, updatedList.IsPublic)
})
t.Run("UpdateWishList_Unauthorized", func(t *testing.T) {
updatedWishList := entities.WishList{
Title: "Updated Title",
Description: "Updated Description",
}
jsonBody, _ := json.Marshal(updatedWishList)
req := httptest.NewRequest("PUT", "/wishlist/test-wishlist-id", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
})
t.Run("UpdateWishList_NotOwner", func(t *testing.T) {
updatedWishList := entities.WishList{
Title: "Updated Title",
Description: "Updated Description",
}
jsonBody, _ := json.Marshal(updatedWishList)
req := httptest.NewRequest("PUT", "/wishlist/other-user-wishlist-id", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer valid-token")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
})
}
func TestDeleteWishListHandler(t *testing.T) {
app, handler, repo, mockAuth := setupTestApp()
app.Delete("/wishlist/:id", handler.DeleteWishList)
userID := primitive.NewObjectID()
wishList := &entities.WishList{
ID: "test-wishlist-id",
Title: "Test Wishlist",
UserID: userID.Hex(),
IsPublic: true,
}
repo.CreateWishList(wishList)
otherUserWishList := &entities.WishList{
ID: "other-user-wishlist-id",
Title: "Other User's Wishlist",
UserID: "other-user-id",
IsPublic: true,
}
repo.CreateWishList(otherUserWishList)
mockAuth.tokens["valid-token"] = userID
t.Run("DeleteWishList_Success", func(t *testing.T) {
req := httptest.NewRequest("DELETE", "/wishlist/test-wishlist-id", nil)
req.Header.Set("Authorization", "Bearer valid-token")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
_, err = repo.ReadWishList("test-wishlist-id")
assert.Error(t, err)
})
t.Run("DeleteWishList_Unauthorized", func(t *testing.T) {
req := httptest.NewRequest("DELETE", "/wishlist/other-user-wishlist-id", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
})
t.Run("DeleteWishList_NotOwner", func(t *testing.T) {
req := httptest.NewRequest("DELETE", "/wishlist/other-user-wishlist-id", nil)
req.Header.Set("Authorization", "Bearer valid-token")
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
})
}

View File

@ -0,0 +1,282 @@
package unit
import (
"testing"
"time"
"wish-list-api/pkg/entities"
wishlist "wish-list-api/pkg/wish-list"
)
func TestCreateWishList(t *testing.T) {
mockRepo := NewMockWishListRepository()
service := wishlist.NewService(mockRepo)
t.Run("CreateWishList_Success", func(t *testing.T) {
wishList := &entities.WishList{
Title: "Test WishList",
UserID: "user123",
Description: "Test Description",
IsPublic: true,
}
result, err := service.CreateWishList(wishList)
if err != nil {
t.Errorf("Ошибка при создании списка желаний: %v", err)
}
if result.Title != wishList.Title {
t.Errorf("Ожидалось название '%s', получено '%s'", wishList.Title, result.Title)
}
if result.UserID != wishList.UserID {
t.Errorf("Ожидался UserID '%s', получен '%s'", wishList.UserID, result.UserID)
}
if result.ID == "" {
t.Error("ID не должен быть пустым")
}
if result.CreatedAt.IsZero() {
t.Error("CreatedAt не должен быть пустым")
}
})
}
func TestGetWishList(t *testing.T) {
mockRepo := NewMockWishListRepository()
service := wishlist.NewService(mockRepo)
wishList := &entities.WishList{
ID: "test-id-123",
Title: "Test WishList",
UserID: "user123",
Description: "Test Description",
IsPublic: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
createdWishList, _ := service.CreateWishList(wishList)
t.Run("GetWishList_Success", func(t *testing.T) {
result, err := service.GetWishList(createdWishList.ID)
if err != nil {
t.Errorf("Ошибка при получении списка желаний: %v", err)
}
if result.ID != createdWishList.ID {
t.Errorf("Ожидался ID '%s', получен '%s'", createdWishList.ID, result.ID)
}
if result.Title != wishList.Title {
t.Errorf("Ожидалось название '%s', получено '%s'", wishList.Title, result.Title)
}
})
t.Run("GetWishList_NotFound", func(t *testing.T) {
_, err := service.GetWishList("non-existent-id")
if err == nil {
t.Error("Должна быть ошибка при поиске несуществующего списка")
}
})
}
func TestUpdateWishList(t *testing.T) {
mockRepo := NewMockWishListRepository()
service := wishlist.NewService(mockRepo)
wishList := &entities.WishList{
Title: "Original Title",
UserID: "user123",
Description: "Original Description",
IsPublic: true,
}
createdWishList, _ := service.CreateWishList(wishList)
t.Run("UpdateWishList_Success", func(t *testing.T) {
updatedWishList := &entities.WishList{
ID: createdWishList.ID,
Title: "Updated Title",
UserID: "user123",
Description: "Updated Description",
IsPublic: false,
}
result, err := service.UpdateWishList(updatedWishList)
if err != nil {
t.Errorf("Ошибка при обновлении списка желаний: %v", err)
}
if result.Title != "Updated Title" {
t.Errorf("Название не обновилось. Ожидалось 'Updated Title', получено '%s'", result.Title)
}
if result.Description != "Updated Description" {
t.Errorf("Описание не обновилось. Ожидалось 'Updated Description', получено '%s'", result.Description)
}
if result.IsPublic != false {
t.Error("Флаг IsPublic не обновился. Ожидалось false")
}
})
t.Run("UpdateWishList_NotFound", func(t *testing.T) {
nonExistentWishList := &entities.WishList{
ID: "non-existent-id",
Title: "Test Title",
UserID: "user123",
Description: "Test Description",
}
_, err := service.UpdateWishList(nonExistentWishList)
if err == nil {
t.Error("Должна быть ошибка при обновлении несуществующего списка")
}
})
}
func TestDeleteWishList(t *testing.T) {
mockRepo := NewMockWishListRepository()
service := wishlist.NewService(mockRepo)
wishList := &entities.WishList{
Title: "Test WishList",
UserID: "user123",
Description: "Test Description",
IsPublic: true,
}
createdWishList, _ := service.CreateWishList(wishList)
t.Run("DeleteWishList_Success", func(t *testing.T) {
err := service.DeleteWishList(createdWishList.ID)
if err != nil {
t.Errorf("Ошибка при удалении списка желаний: %v", err)
}
_, err = service.GetWishList(createdWishList.ID)
if err == nil {
t.Error("Список желаний не был удален")
}
})
t.Run("DeleteWishList_NotFound", func(t *testing.T) {
err := service.DeleteWishList("non-existent-id")
if err == nil {
t.Error("Должна быть ошибка при удалении несуществующего списка")
}
})
}
func TestCreateWishListItem(t *testing.T) {
mockRepo := NewMockWishListRepository()
service := wishlist.NewService(mockRepo)
wishList := &entities.WishList{
Title: "Test WishList",
UserID: "user123",
Description: "Test Description",
IsPublic: true,
}
createdWishList, _ := service.CreateWishList(wishList)
t.Run("CreateWishListItem_Success", func(t *testing.T) {
item := &entities.WishListItem{
Title: "Test Item",
URL: "https://example.com",
Cost: 100.50,
WishListID: createdWishList.ID,
Description: "Test Item Description",
}
result, err := service.CreateWishListItem(item)
if err != nil {
t.Errorf("Ошибка при создании элемента списка желаний: %v", err)
}
if result.Title != item.Title {
t.Errorf("Ожидалось название '%s', получено '%s'", item.Title, result.Title)
}
if result.WishListID != createdWishList.ID {
t.Errorf("Ожидался WishListID '%s', получен '%s'", createdWishList.ID, result.WishListID)
}
if result.ID == "" {
t.Error("ID не должен быть пустым")
}
})
t.Run("CreateWishListItem_InvalidWishListID", func(t *testing.T) {
item := &entities.WishListItem{
Title: "Test Item",
URL: "https://example.com",
Cost: 100.50,
WishListID: "non-existent-id",
Description: "Test Item Description",
}
_, err := service.CreateWishListItem(item)
if err == nil {
t.Error("Должна быть ошибка при создании элемента для несуществующего списка")
}
})
}
func TestGetWishListItems(t *testing.T) {
mockRepo := NewMockWishListRepository()
service := wishlist.NewService(mockRepo)
wishList := &entities.WishList{
Title: "Test WishList",
UserID: "user123",
Description: "Test Description",
IsPublic: true,
}
createdWishList, _ := service.CreateWishList(wishList)
for i := 0; i < 3; i++ {
item := &entities.WishListItem{
Title: "Item",
URL: "https://example.com",
Cost: 100.50,
WishListID: createdWishList.ID,
Description: "Item Description",
}
service.CreateWishListItem(item)
}
t.Run("GetAllWishListItems_Success", func(t *testing.T) {
items, err := service.GetAllWishListItems(createdWishList.ID)
if err != nil {
t.Errorf("Ошибка при получении элементов списка желаний: %v", err)
}
if len(*items) != 3 {
t.Errorf("Ожидалось 3 элемента, получено %d", len(*items))
}
})
t.Run("GetAllWishListItems_EmptyList", func(t *testing.T) {
emptyWishList := &entities.WishList{
Title: "Empty WishList",
UserID: "user123",
Description: "Empty List",
IsPublic: true,
}
createdEmptyWishList, _ := service.CreateWishList(emptyWishList)
items, err := service.GetAllWishListItems(createdEmptyWishList.ID)
if err != nil {
t.Errorf("Ошибка при получении элементов пустого списка: %v", err)
}
if len(*items) != 0 {
t.Errorf("Ожидалось 0 элементов, получено %d", len(*items))
}
})
}