327 lines
11 KiB
Go
327 lines
11 KiB
Go
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"
|
|
)
|
|
|
|
func int64Ptr(v int64) *int64 {
|
|
return &v
|
|
}
|
|
|
|
func stringPtr(v string) *string {
|
|
return &v
|
|
}
|
|
|
|
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: int64Ptr(12345),
|
|
TelegramChatName: stringPtr("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(), "message")
|
|
})
|
|
|
|
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)
|
|
assert.Nil(t, req.TelegramChatID)
|
|
assert.Nil(t, req.TelegramChatName)
|
|
return expected, nil
|
|
}})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/families", bytes.NewBufferString(`{"name":"Belan","owner_id":10}`))
|
|
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(), "message")
|
|
})
|
|
|
|
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)
|
|
require.NotNil(t, req.TelegramChatName)
|
|
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)
|
|
}
|