Restructured project
- backend moved to backend directory - added and initialized frontend with vue - moved infrastructure files to infra directory
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/api/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type AuthRouter struct {
|
||||
service services.AuthService
|
||||
}
|
||||
|
||||
func NewAuthRouter(s services.AuthService) *AuthRouter {
|
||||
return &AuthRouter{service: s}
|
||||
}
|
||||
|
||||
func (router *AuthRouter) RegisterRouter(r *gin.RouterGroup) {
|
||||
auth := r.Group("/auth")
|
||||
{
|
||||
auth.POST("", router.Auth)
|
||||
}
|
||||
}
|
||||
|
||||
func (router *AuthRouter) Auth(c *gin.Context) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/api/services"
|
||||
"FamilyHub/src/domain"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type FamiliesRouter struct {
|
||||
service services.FamilyService
|
||||
}
|
||||
|
||||
func NewFamiliesRouter(s services.FamilyService) *FamiliesRouter {
|
||||
return &FamiliesRouter{service: s}
|
||||
}
|
||||
|
||||
func (router *FamiliesRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||
families := r.Group("/families")
|
||||
{
|
||||
families.POST("", router.Create)
|
||||
families.GET("/:id", router.GetByID)
|
||||
families.PATCH("/:id", router.Update)
|
||||
families.DELETE("/:id", router.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
// Create GoDoc
|
||||
// @Summary Создать семью
|
||||
// @Description Создает новую семью
|
||||
// @Tags Families
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param family body domain.CreateFamilyRequest true "Family info"
|
||||
// @Success 201 {object} domain.FamilyResponse
|
||||
// @Failure 400 {object} map[string]string "invalid body"
|
||||
// @Failure 500 {object} map[string]string "internal server error"
|
||||
// @Router /families [post]
|
||||
func (router *FamiliesRouter) Create(c *gin.Context) {
|
||||
var req domain.CreateFamilyRequest
|
||||
var resp domain.FamilyResponse
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
family, err := router.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleFamilyError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, resp.ModelToResponse(family))
|
||||
}
|
||||
|
||||
// GetByID GoDoc
|
||||
// @Summary Получить семью по ID
|
||||
// @Description Возвращает семью по ее внутреннему ID
|
||||
// @Tags Families
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Family ID"
|
||||
// @Success 200 {object} domain.FamilyResponse
|
||||
// @Failure 400 {object} map[string]string "invalid id"
|
||||
// @Failure 404 {object} map[string]string "family not found"
|
||||
// @Failure 500 {object} map[string]string "internal server error"
|
||||
// @Router /families/{id} [get]
|
||||
func (router *FamiliesRouter) GetByID(c *gin.Context) {
|
||||
var resp domain.FamilyResponse
|
||||
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
family, err := router.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleFamilyError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp.ModelToResponse(family))
|
||||
}
|
||||
|
||||
// Update GoDoc
|
||||
// @Summary Обновить семью
|
||||
// @Description Частично обновляет данные семьи по ID
|
||||
// @Tags Families
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Family ID"
|
||||
// @Param family body domain.UpdateFamilyRequest true "Данные для обновления"
|
||||
// @Success 200 {object} domain.FamilyResponse
|
||||
// @Failure 400 {object} map[string]string "invalid id or invalid body"
|
||||
// @Failure 400 {object} map[string]string "name is required"
|
||||
// @Failure 404 {object} map[string]string "family not found"
|
||||
// @Failure 500 {object} map[string]string "internal server error"
|
||||
// @Router /families/{id} [patch]
|
||||
func (router *FamiliesRouter) Update(c *gin.Context) {
|
||||
var resp domain.FamilyResponse
|
||||
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req domain.UpdateFamilyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Name == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
family, err := router.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleFamilyError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp.ModelToResponse(family))
|
||||
}
|
||||
|
||||
// Delete GoDoc
|
||||
// @Summary Удалить семью
|
||||
// @Description Удаляет семью по ее ID
|
||||
// @Tags Families
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Family ID"
|
||||
// @Success 204 {string} string "no content"
|
||||
// @Failure 400 {object} map[string]string "invalid id"
|
||||
// @Failure 404 {object} map[string]string "family not found"
|
||||
// @Failure 500 {object} map[string]string "internal server error"
|
||||
// @Router /families/{id} [delete]
|
||||
func (router *FamiliesRouter) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := router.service.Delete(c.Request.Context(), id); err != nil {
|
||||
handleFamilyError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func handleFamilyError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrFamilyNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "family not found"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/api/services"
|
||||
"FamilyHub/src/domain"
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type familyServiceMock struct {
|
||||
createFn func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error)
|
||||
getByIDFn func(ctx context.Context, id int64) (*domain.Family, error)
|
||||
updateFn func(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error)
|
||||
deleteFn func(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
func (m *familyServiceMock) Create(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
|
||||
if m.createFn != nil {
|
||||
return m.createFn(ctx, req)
|
||||
}
|
||||
return nil, errors.New("mock create is not configured")
|
||||
}
|
||||
|
||||
func (m *familyServiceMock) GetByID(ctx context.Context, id int64) (*domain.Family, error) {
|
||||
if m.getByIDFn != nil {
|
||||
return m.getByIDFn(ctx, id)
|
||||
}
|
||||
return nil, errors.New("mock getByID is not configured")
|
||||
}
|
||||
|
||||
func (m *familyServiceMock) Update(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) {
|
||||
if m.updateFn != nil {
|
||||
return m.updateFn(ctx, id, req)
|
||||
}
|
||||
return nil, errors.New("mock update is not configured")
|
||||
}
|
||||
|
||||
func (m *familyServiceMock) Delete(ctx context.Context, id int64) error {
|
||||
if m.deleteFn != nil {
|
||||
return m.deleteFn(ctx, id)
|
||||
}
|
||||
return errors.New("mock delete is not configured")
|
||||
}
|
||||
|
||||
func setupFamiliesRouter(mock services.FamilyService) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
apiV1 := r.Group("/api/v1")
|
||||
router := NewFamiliesRouter(mock)
|
||||
router.RegisterRoutes(apiV1)
|
||||
return r
|
||||
}
|
||||
|
||||
func sampleFamily() *domain.Family {
|
||||
return &domain.Family{
|
||||
ID: 7,
|
||||
Name: "Belan",
|
||||
OwnerID: 10,
|
||||
TelegramChatID: 12345,
|
||||
TelegramChatName: "Family Chat",
|
||||
CreatedAt: time.Date(2026, time.January, 21, 10, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2026, time.January, 21, 11, 0, 0, 0, time.UTC),
|
||||
}
|
||||
}
|
||||
|
||||
func TestFamiliesRouter_Create(t *testing.T) {
|
||||
t.Run("bad request on malformed body", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "error")
|
||||
})
|
||||
|
||||
t.Run("internal error", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
|
||||
return nil, errors.New("db unavailable")
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":"Belan","owner_id":10,"telegram_chat_id":12345,"telegram_chat_name":"Family Chat"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "internal server error")
|
||||
})
|
||||
|
||||
t.Run("created", func(t *testing.T) {
|
||||
expected := sampleFamily()
|
||||
r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
|
||||
assert.Equal(t, "Belan", req.Name)
|
||||
return expected, nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":"Belan","owner_id":10,"telegram_chat_id":12345,"telegram_chat_name":"Family Chat"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Belan")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFamiliesRouter_GetByID(t *testing.T) {
|
||||
t.Run("bad request on invalid id", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/families/abc", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "invalid id")
|
||||
})
|
||||
|
||||
t.Run("not found on service error", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.Family, error) {
|
||||
return nil, services.ErrFamilyNotFound
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/families/7", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
assert.Contains(t, w.Body.String(), services.ErrFamilyNotFound.Error())
|
||||
})
|
||||
|
||||
t.Run("not found on sql no rows", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.Family, error) {
|
||||
return nil, sql.ErrNoRows
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/families/7", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "family not found")
|
||||
})
|
||||
|
||||
t.Run("ok", func(t *testing.T) {
|
||||
expected := sampleFamily()
|
||||
r := setupFamiliesRouter(&familyServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.Family, error) {
|
||||
assert.Equal(t, int64(7), id)
|
||||
return expected, nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/families/7", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "Belan")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFamiliesRouter_Update(t *testing.T) {
|
||||
t.Run("bad request on invalid id", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/abc", bytes.NewBufferString(`{"name":"Belan"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "invalid id")
|
||||
})
|
||||
|
||||
t.Run("bad request on malformed body", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"name":`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "error")
|
||||
})
|
||||
|
||||
t.Run("bad request on missing name", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"telegram_chat_name":"Updated"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "name is required")
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) {
|
||||
return nil, services.ErrFamilyNotFound
|
||||
}})
|
||||
name := "Belan Updated"
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"name":"`+name+`","telegram_chat_name":"Updated"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
assert.Contains(t, w.Body.String(), services.ErrFamilyNotFound.Error())
|
||||
})
|
||||
|
||||
t.Run("ok", func(t *testing.T) {
|
||||
expected := sampleFamily()
|
||||
updatedName := "Belan Updated"
|
||||
expected.Name = updatedName
|
||||
r := setupFamiliesRouter(&familyServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateFamilyRequest) (*domain.Family, error) {
|
||||
assert.Equal(t, int64(7), id)
|
||||
require.NotNil(t, req.Name)
|
||||
assert.Equal(t, updatedName, *req.Name)
|
||||
assert.Equal(t, "Updated", req.TelegramChatName)
|
||||
return expected, nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/families/7", bytes.NewBufferString(`{"name":"`+updatedName+`","telegram_chat_name":"Updated"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), updatedName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFamiliesRouter_Delete(t *testing.T) {
|
||||
t.Run("bad request on invalid id", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/families/abc", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "invalid id")
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
r := setupFamiliesRouter(&familyServiceMock{deleteFn: func(ctx context.Context, id int64) error {
|
||||
return sql.ErrNoRows
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/families/7", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "family not found")
|
||||
})
|
||||
|
||||
t.Run("no content", func(t *testing.T) {
|
||||
called := false
|
||||
r := setupFamiliesRouter(&familyServiceMock{deleteFn: func(ctx context.Context, id int64) error {
|
||||
called = true
|
||||
assert.Equal(t, int64(7), id)
|
||||
return nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/families/7", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusNoContent, w.Code)
|
||||
assert.True(t, called)
|
||||
assert.Empty(t, strings.TrimSpace(w.Body.String()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFamiliesRouter_Create_ResponseShape(t *testing.T) {
|
||||
expected := sampleFamily()
|
||||
r := setupFamiliesRouter(&familyServiceMock{createFn: func(ctx context.Context, req domain.CreateFamilyRequest) (*domain.Family, error) {
|
||||
return expected, nil
|
||||
}})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":"Belan","owner_id":10,"telegram_chat_id":12345,"telegram_chat_name":"Family Chat"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
var resp domain.FamilyResponse
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected.ID, resp.ID)
|
||||
assert.Equal(t, expected.Name, resp.Name)
|
||||
assert.Equal(t, expected.OwnerID, resp.OwnerID)
|
||||
assert.Equal(t, expected.TelegramChatID, resp.TelegramChatID)
|
||||
assert.Equal(t, expected.TelegramChatName, resp.TelegramChatName)
|
||||
assert.Equal(t, expected.CreatedAt.Format(time.RFC3339), resp.CreatedAt)
|
||||
assert.Equal(t, expected.UpdatedAt.Format(time.RFC3339), resp.UpdatedAt)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/api/dto"
|
||||
"FamilyHub/src/domain"
|
||||
"FamilyHub/src/utils"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type receiptService interface {
|
||||
GetReceipt(ctx context.Context, date string, number string) (*domain.Receipt, error)
|
||||
}
|
||||
|
||||
type ReceiptRouter struct {
|
||||
service receiptService
|
||||
}
|
||||
|
||||
func NewReceiptRouter(s receiptService) *ReceiptRouter {
|
||||
return &ReceiptRouter{service: s}
|
||||
}
|
||||
func (router *ReceiptRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||
receipts := r.Group("/receipts")
|
||||
{
|
||||
receipts.POST("", router.AddReceipt)
|
||||
}
|
||||
}
|
||||
|
||||
func (router *ReceiptRouter) AddReceipt(context_ *gin.Context) {
|
||||
var req domain.AddReceiptRequest
|
||||
if err := context_.ShouldBindJSON(&req); err != nil {
|
||||
log.Println("bind error:", err)
|
||||
context_.JSON(http.StatusBadRequest, dto.ErrorResponse{Message: err.Error()})
|
||||
return
|
||||
}
|
||||
isoDate, err := utils.NormalizeDateToISO(req.Date)
|
||||
if err != nil {
|
||||
context_.JSON(400, gin.H{"error": "invalid date format"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
receipt, err := router.service.GetReceipt(ctx, isoDate, req.Number)
|
||||
if err != nil {
|
||||
context_.JSON(400, gin.H{"error": err.Error()})
|
||||
log.Printf("API error, %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
resp := domain.AddReceiptResponse{
|
||||
ID: 1,
|
||||
Number: receipt.ReceiptNumber,
|
||||
Date: receipt.IssuedAt,
|
||||
}
|
||||
context_.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/domain"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type receiptServiceMock struct {
|
||||
getReceiptFn func(ctx context.Context, date string, number string) (*domain.Receipt, error)
|
||||
}
|
||||
|
||||
func (m *receiptServiceMock) GetReceipt(ctx context.Context, date string, number string) (*domain.Receipt, error) {
|
||||
if m.getReceiptFn != nil {
|
||||
return m.getReceiptFn(ctx, date, number)
|
||||
}
|
||||
return nil, errors.New("mock is not configured")
|
||||
}
|
||||
|
||||
func TestReceiptRouter_AddReceipt(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
validNumber := strings.Repeat("1", 24)
|
||||
validDate := "21.01.2026"
|
||||
expectedDate := "2026-01-21"
|
||||
now := time.Date(2026, time.January, 21, 10, 11, 12, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
mock *receiptServiceMock
|
||||
expectedStatus int
|
||||
expectedContains string
|
||||
}{
|
||||
{
|
||||
name: "bad request on invalid body",
|
||||
body: `{"date":"21.01.2026"}`,
|
||||
mock: &receiptServiceMock{},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedContains: "Number",
|
||||
},
|
||||
{
|
||||
name: "bad request on invalid date format",
|
||||
body: `{"number":"` + validNumber + `","date":"2026-01-21"}`,
|
||||
mock: &receiptServiceMock{},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedContains: "invalid date format",
|
||||
},
|
||||
{
|
||||
name: "bad request on service error",
|
||||
body: `{"number":"` + validNumber + `","date":"` + validDate + `"}`,
|
||||
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, date string, number string) (*domain.Receipt, error) {
|
||||
assert.Equal(t, expectedDate, date)
|
||||
assert.Equal(t, validNumber, number)
|
||||
return nil, errors.New("receipt not found")
|
||||
}},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedContains: "receipt not found",
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
body: `{"number":"` + validNumber + `","date":"` + validDate + `"}`,
|
||||
mock: &receiptServiceMock{getReceiptFn: func(ctx context.Context, date string, number string) (*domain.Receipt, error) {
|
||||
assert.Equal(t, expectedDate, date)
|
||||
assert.Equal(t, validNumber, number)
|
||||
return &domain.Receipt{ReceiptNumber: validNumber, IssuedAt: now}, nil
|
||||
}},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedContains: validNumber,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := gin.New()
|
||||
apiV1 := r.Group("/api/v1")
|
||||
router := NewReceiptRouter(tc.mock)
|
||||
router.RegisterRoutes(apiV1)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/receipts", bytes.NewBufferString(tc.body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, tc.expectedStatus, w.Code)
|
||||
assert.Contains(t, w.Body.String(), tc.expectedContains)
|
||||
|
||||
if tc.expectedStatus == http.StatusOK {
|
||||
var resp struct {
|
||||
ID int32 `json:"id"`
|
||||
Number string `json:"number"`
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int32(1), resp.ID)
|
||||
assert.Equal(t, validNumber, resp.Number)
|
||||
assert.Equal(t, now, resp.Date)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/api/services"
|
||||
"FamilyHub/src/domain"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type UsersRouter struct {
|
||||
service services.UserService
|
||||
}
|
||||
|
||||
func NewUsersRouter(s services.UserService) *UsersRouter {
|
||||
return &UsersRouter{service: s}
|
||||
}
|
||||
|
||||
func (router *UsersRouter) RegisterRoutes(r *gin.RouterGroup) {
|
||||
users := r.Group("/users")
|
||||
{
|
||||
users.POST("", router.CreateUser)
|
||||
users.GET("/:id", router.GetByID)
|
||||
users.GET("/by-telegram/:telegramId", router.GetByTelegramID)
|
||||
users.PATCH("/:id", router.Update)
|
||||
users.DELETE("/:id", router.Delete)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser GoDoc
|
||||
// @Summary Создать пользователя
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user body domain.CreateUserRequest true "User info"
|
||||
// @Success 201 {object} domain.UserResponse
|
||||
// @Failure 400 {object} domain.UserErrorResponse
|
||||
// @Failure 500 {object} domain.UserErrorResponse
|
||||
// @Router /users [post]
|
||||
func (router *UsersRouter) CreateUser(c *gin.Context) {
|
||||
var req domain.CreateUserRequest
|
||||
var resp domain.UserResponse
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := router.service.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, resp.ModelToResponse(user))
|
||||
}
|
||||
|
||||
// GetByID GoDoc
|
||||
// @Summary Получить пользователя по ID
|
||||
// @Description Возвращает пользователя по его внутреннему ID
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "User ID"
|
||||
// @Success 200 {object} domain.UserResponse
|
||||
// @Failure 400 {object} domain.UserErrorResponse "invalid id"
|
||||
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
||||
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
||||
// @Router /users/{id} [get]
|
||||
func (router *UsersRouter) GetByID(c *gin.Context) {
|
||||
var resp domain.UserResponse
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := router.service.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp.ModelToResponse(user))
|
||||
}
|
||||
|
||||
// GetByTelegramID GoDoc
|
||||
// @Summary Получить пользователя по Telegram ID
|
||||
// @Description Возвращает пользователя по его Telegram ID
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param telegramId path int true "Telegram ID"
|
||||
// @Success 200 {object} domain.UserResponse
|
||||
// @Failure 400 {object} domain.UserErrorResponse "invalid telegram id"
|
||||
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
||||
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
||||
// @Router /users/by-telegram/{telegramId} [get]
|
||||
func (router *UsersRouter) GetByTelegramID(c *gin.Context) {
|
||||
var resp domain.UserResponse
|
||||
telegramID, err := strconv.ParseInt(c.Param("telegramId"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid telegram id"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := router.service.GetByTelegramID(c.Request.Context(), telegramID)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp.ModelToResponse(user))
|
||||
}
|
||||
|
||||
// Update GoDoc
|
||||
// @Summary Обновить пользователя
|
||||
// @Description Частично обновляет данные пользователя по ID
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "User ID"
|
||||
// @Param user body domain.UpdateUserRequest true "Данные для обновления"
|
||||
// @Success 200 {object} domain.UserResponse
|
||||
// @Failure 400 {object} domain.UserErrorResponse "invalid id or invalid body"
|
||||
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
||||
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
||||
// @Router /users/{id} [patch]
|
||||
func (router *UsersRouter) Update(c *gin.Context) {
|
||||
var resp domain.UserResponse
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
var req domain.UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := router.service.Update(c.Request.Context(), id, req)
|
||||
if err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp.ModelToResponse(user))
|
||||
}
|
||||
|
||||
// Delete GoDoc
|
||||
// @Summary Удалить пользователя
|
||||
// @Description Удаляет пользователя по его ID
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "User ID"
|
||||
// @Success 204 {string} string "no content"
|
||||
// @Failure 400 {object} domain.UserErrorResponse "invalid id"
|
||||
// @Failure 404 {object} domain.UserErrorResponse "user not found"
|
||||
// @Failure 500 {object} domain.UserErrorResponse "internal server error"
|
||||
// @Router /users/{id} [delete]
|
||||
func (router *UsersRouter) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := router.service.Delete(c.Request.Context(), id); err != nil {
|
||||
handleError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func handleError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, services.ErrUserNotFound):
|
||||
c.JSON(http.StatusNotFound, domain.UserErrorResponse{Error: err.Error()})
|
||||
case errors.Is(err, services.ErrInvalidPatch):
|
||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
|
||||
case errors.Is(err, services.ErrTelegramIDMissing):
|
||||
c.JSON(http.StatusBadRequest, domain.UserErrorResponse{Error: err.Error()})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, domain.UserErrorResponse{Error: "internal server error"})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/api/services"
|
||||
"FamilyHub/src/domain"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type userServiceMock struct {
|
||||
createFn func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error)
|
||||
getByIDFn func(ctx context.Context, id int64) (*domain.UserModel, error)
|
||||
getByTelegramIDFn func(ctx context.Context, telegramID int64) (*domain.UserModel, error)
|
||||
updateFn func(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error)
|
||||
deleteFn func(ctx context.Context, id int64) error
|
||||
}
|
||||
|
||||
func (m *userServiceMock) Create(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) {
|
||||
if m.createFn != nil {
|
||||
return m.createFn(ctx, req)
|
||||
}
|
||||
return nil, errors.New("mock create is not configured")
|
||||
}
|
||||
|
||||
func (m *userServiceMock) GetByID(ctx context.Context, id int64) (*domain.UserModel, error) {
|
||||
if m.getByIDFn != nil {
|
||||
return m.getByIDFn(ctx, id)
|
||||
}
|
||||
return nil, errors.New("mock getByID is not configured")
|
||||
}
|
||||
|
||||
func (m *userServiceMock) GetByTelegramID(ctx context.Context, telegramID int64) (*domain.UserModel, error) {
|
||||
if m.getByTelegramIDFn != nil {
|
||||
return m.getByTelegramIDFn(ctx, telegramID)
|
||||
}
|
||||
return nil, errors.New("mock getByTelegramID is not configured")
|
||||
}
|
||||
|
||||
func (m *userServiceMock) Update(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) {
|
||||
if m.updateFn != nil {
|
||||
return m.updateFn(ctx, id, req)
|
||||
}
|
||||
return nil, errors.New("mock update is not configured")
|
||||
}
|
||||
|
||||
func (m *userServiceMock) Delete(ctx context.Context, id int64) error {
|
||||
if m.deleteFn != nil {
|
||||
return m.deleteFn(ctx, id)
|
||||
}
|
||||
return errors.New("mock delete is not configured")
|
||||
}
|
||||
|
||||
func setupUsersRouter(mock services.UserService) *gin.Engine {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
apiV1 := r.Group("/api/v1")
|
||||
router := NewUsersRouter(mock)
|
||||
router.RegisterRoutes(apiV1)
|
||||
return r
|
||||
}
|
||||
|
||||
func sampleUser() *domain.UserModel {
|
||||
username := "john"
|
||||
firstName := "John"
|
||||
lastName := "Doe"
|
||||
languageCode := "en"
|
||||
|
||||
return &domain.UserModel{
|
||||
ID: 10,
|
||||
TelegramID: 100500,
|
||||
Username: &username,
|
||||
FirstName: &firstName,
|
||||
LastName: &lastName,
|
||||
LanguageCode: &languageCode,
|
||||
CreatedAt: time.Date(2026, time.January, 21, 10, 11, 12, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2026, time.January, 21, 12, 11, 12, 0, time.UTC),
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsersRouter_CreateUser(t *testing.T) {
|
||||
t.Run("bad request on malformed body", func(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "error")
|
||||
})
|
||||
|
||||
t.Run("bad request on domain validation error", func(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{createFn: func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) {
|
||||
return nil, services.ErrTelegramIDMissing
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":1,"first_name":"A"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), services.ErrTelegramIDMissing.Error())
|
||||
})
|
||||
|
||||
t.Run("created", func(t *testing.T) {
|
||||
expected := sampleUser()
|
||||
r := setupUsersRouter(&userServiceMock{createFn: func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) {
|
||||
assert.Equal(t, int64(100500), req.TelegramID)
|
||||
require.NotNil(t, req.FirstName)
|
||||
assert.Equal(t, "John", *req.FirstName)
|
||||
return expected, nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":100500,"first_name":"John"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "100500")
|
||||
assert.Contains(t, w.Body.String(), "John")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsersRouter_GetByID(t *testing.T) {
|
||||
t.Run("bad request on invalid id", func(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/abc", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "invalid id")
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.UserModel, error) {
|
||||
return nil, services.ErrUserNotFound
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
assert.Contains(t, w.Body.String(), services.ErrUserNotFound.Error())
|
||||
})
|
||||
|
||||
t.Run("ok", func(t *testing.T) {
|
||||
expected := sampleUser()
|
||||
r := setupUsersRouter(&userServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.UserModel, error) {
|
||||
assert.Equal(t, int64(10), id)
|
||||
return expected, nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/10", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "100500")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsersRouter_GetByTelegramID(t *testing.T) {
|
||||
t.Run("bad request on invalid telegram id", func(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-telegram/abc", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "invalid telegram id")
|
||||
})
|
||||
|
||||
t.Run("ok", func(t *testing.T) {
|
||||
expected := sampleUser()
|
||||
r := setupUsersRouter(&userServiceMock{getByTelegramIDFn: func(ctx context.Context, telegramID int64) (*domain.UserModel, error) {
|
||||
assert.Equal(t, int64(100500), telegramID)
|
||||
return expected, nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/by-telegram/100500", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "John")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsersRouter_Update(t *testing.T) {
|
||||
t.Run("bad request on invalid id", func(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/abc", bytes.NewBufferString(`{"first_name":"John"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "invalid id")
|
||||
})
|
||||
|
||||
t.Run("bad request on malformed body", func(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/10", bytes.NewBufferString(`{"first_name":`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "error")
|
||||
})
|
||||
|
||||
t.Run("bad request on invalid patch", func(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) {
|
||||
return nil, services.ErrInvalidPatch
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/10", bytes.NewBufferString(`{"first_name":"John"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), services.ErrInvalidPatch.Error())
|
||||
})
|
||||
|
||||
t.Run("ok", func(t *testing.T) {
|
||||
expected := sampleUser()
|
||||
r := setupUsersRouter(&userServiceMock{updateFn: func(ctx context.Context, id int64, req domain.UpdateUserRequest) (*domain.UserModel, error) {
|
||||
assert.Equal(t, int64(10), id)
|
||||
require.NotNil(t, req.FirstName)
|
||||
assert.Equal(t, "John", *req.FirstName)
|
||||
return expected, nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodPatch, "/api/v1/users/10", bytes.NewBufferString(`{"first_name":"John"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "100500")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsersRouter_Delete(t *testing.T) {
|
||||
t.Run("bad request on invalid id", func(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{})
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/users/abc", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "invalid id")
|
||||
})
|
||||
|
||||
t.Run("not found", func(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{deleteFn: func(ctx context.Context, id int64) error {
|
||||
return services.ErrUserNotFound
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/users/1", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
assert.Contains(t, w.Body.String(), services.ErrUserNotFound.Error())
|
||||
})
|
||||
|
||||
t.Run("no content", func(t *testing.T) {
|
||||
called := false
|
||||
r := setupUsersRouter(&userServiceMock{deleteFn: func(ctx context.Context, id int64) error {
|
||||
called = true
|
||||
assert.Equal(t, int64(10), id)
|
||||
return nil
|
||||
}})
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/users/10", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusNoContent, w.Code)
|
||||
assert.True(t, called)
|
||||
assert.Empty(t, strings.TrimSpace(w.Body.String()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsersRouter_CreateUser_ResponseShape(t *testing.T) {
|
||||
expected := sampleUser()
|
||||
r := setupUsersRouter(&userServiceMock{createFn: func(ctx context.Context, req domain.CreateUserRequest) (*domain.UserModel, error) {
|
||||
return expected, nil
|
||||
}})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/users", bytes.NewBufferString(`{"telegram_id":100500,"first_name":"John"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
var resp domain.UserResponse
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expected.ID, resp.ID)
|
||||
assert.Equal(t, expected.TelegramID, resp.TelegramID)
|
||||
assert.Equal(t, expected.FirstName, resp.FirstName)
|
||||
assert.Equal(t, expected.CreatedAt.Format(time.RFC3339), resp.CreatedAt)
|
||||
assert.Equal(t, expected.UpdatedAt.Format(time.RFC3339), resp.UpdatedAt)
|
||||
}
|
||||
|
||||
func TestUsersRouter_GetByID_UsesPathID(t *testing.T) {
|
||||
r := setupUsersRouter(&userServiceMock{getByIDFn: func(ctx context.Context, id int64) (*domain.UserModel, error) {
|
||||
assert.Equal(t, int64(42), id)
|
||||
u := sampleUser()
|
||||
u.ID = id
|
||||
return u, nil
|
||||
}})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/42", nil)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), strconv.FormatInt(42, 10))
|
||||
}
|
||||
Reference in New Issue
Block a user